From 51f46e797c3787046bac9fad13bb28d3a5e83175 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Fri, 22 Mar 2024 18:32:03 +0100 Subject: [PATCH 001/351] [PM-5571] Migrate enableDDG to state provider framework (#8384) Migrate enableDuckDuckGo to state provider framework. --- .github/CODEOWNERS | 1 + .../src/app/accounts/settings.component.ts | 9 ++-- .../src/app/services/services.module.ts | 6 +++ .../desktop-autofill-settings.service.ts | 29 ++++++++++++ apps/desktop/src/main.ts | 10 ++-- apps/desktop/src/main/messaging.main.ts | 4 -- .../desktop/src/main/native-messaging.main.ts | 2 +- .../native-message-handler.service.ts | 6 ++- .../platform/abstractions/state.service.ts | 5 -- .../platform/models/domain/global-state.ts | 1 - .../src/platform/services/state.service.ts | 21 --------- libs/common/src/state-migrations/migrate.ts | 7 ++- .../48-move-ddg-to-state-provider.spec.ts | 47 +++++++++++++++++++ .../48-move-ddg-to-state-provider.ts | 40 ++++++++++++++++ 14 files changed, 147 insertions(+), 41 deletions(-) create mode 100644 apps/desktop/src/autofill/services/desktop-autofill-settings.service.ts create mode 100644 libs/common/src/state-migrations/migrations/48-move-ddg-to-state-provider.spec.ts create mode 100644 libs/common/src/state-migrations/migrations/48-move-ddg-to-state-provider.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index bfce3c1547..bfad3f2628 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -83,6 +83,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/apps/desktop/src/app/accounts/settings.component.ts b/apps/desktop/src/app/accounts/settings.component.ts index 4e6af9bff4..60aa2ebae8 100644 --- a/apps/desktop/src/app/accounts/settings.component.ts +++ b/apps/desktop/src/app/accounts/settings.component.ts @@ -23,6 +23,7 @@ import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-stat import { DialogService } from "@bitwarden/components"; import { SetPinComponent } from "../../auth/components/set-pin.component"; +import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service"; import { flagEnabled } from "../../platform/flags"; import { DesktopSettingsService } from "../../platform/services/desktop-settings.service"; @@ -122,6 +123,7 @@ export class SettingsComponent implements OnInit { private userVerificationService: UserVerificationServiceAbstraction, private desktopSettingsService: DesktopSettingsService, private biometricStateService: BiometricStateService, + private desktopAutofillSettingsService: DesktopAutofillSettingsService, ) { const isMac = this.platformUtilsService.getDevice() === DeviceType.MacOsDesktop; @@ -262,11 +264,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$), }; @@ -640,7 +643,7 @@ export class SettingsComponent implements OnInit { } async saveDdgBrowserIntegration() { - await this.stateService.setEnableDuckDuckGoBrowserIntegration( + await this.desktopAutofillSettingsService.setEnableDuckDuckGoBrowserIntegration( this.form.value.enableDuckDuckGoBrowserIntegration, ); diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index bed8f21bbe..495d6abcf1 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -53,6 +53,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"; @@ -218,6 +219,11 @@ const RELOAD_CALLBACK = new InjectionToken<() => any>("RELOAD_CALLBACK"); useClass: DesktopSettingsService, deps: [StateProvider], }, + { + provide: DesktopAutofillSettingsService, + useClass: DesktopAutofillSettingsService, + deps: [StateProvider], + }, ], }) export class ServicesModule {} 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/main.ts b/apps/desktop/src/main.ts index bae1cc0c06..5cb6abac58 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -26,6 +26,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"; @@ -71,6 +72,7 @@ export class Main { biometricsService: BiometricsServiceAbstraction; nativeMessagingMain: NativeMessagingMain; clipboardMain: ClipboardMain; + desktopAutofillSettingsService: DesktopAutofillSettingsService; constructor() { // Set paths for portable builds @@ -233,6 +235,8 @@ export class Main { app.getPath("exe"), ); + this.desktopAutofillSettingsService = new DesktopAutofillSettingsService(stateProvider); + this.clipboardMain = new ClipboardMain(); this.clipboardMain.init(); @@ -268,10 +272,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(); } 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/services/native-message-handler.service.ts b/apps/desktop/src/services/native-message-handler.service.ts index c90271d25c..785b65195a 100644 --- a/apps/desktop/src/services/native-message-handler.service.ts +++ b/apps/desktop/src/services/native-message-handler.service.ts @@ -12,6 +12,7 @@ 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/libs/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index b795db73fc..514689313f 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -188,11 +188,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 }, diff --git a/libs/common/src/platform/models/domain/global-state.ts b/libs/common/src/platform/models/domain/global-state.ts index 3fd6f38200..7e35606e26 100644 --- a/libs/common/src/platform/models/domain/global-state.ts +++ b/libs/common/src/platform/models/domain/global-state.ts @@ -13,6 +13,5 @@ export class GlobalState { mainWindowSize?: number; enableBrowserIntegration?: boolean; enableBrowserIntegrationFingerprint?: boolean; - enableDuckDuckGoBrowserIntegration?: boolean; deepLinkRedirectUrl?: string; } diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index 6ff1c63b50..56fb91dd52 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -867,27 +867,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 ( diff --git a/libs/common/src/state-migrations/migrate.ts b/libs/common/src/state-migrations/migrate.ts index 3c03854780..1b057fda4d 100644 --- a/libs/common/src/state-migrations/migrate.ts +++ b/libs/common/src/state-migrations/migrate.ts @@ -43,6 +43,7 @@ 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 { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-keys"; import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key"; import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account"; @@ -51,7 +52,8 @@ 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 = 48; + export type MinVersion = typeof MIN_VERSION; export function createMigrationBuilder() { @@ -100,7 +102,8 @@ 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, CURRENT_VERSION); } export async function currentVersion( 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); + } +} From 78e8f9c587dc23d4d452f2df2167e26558a260ec Mon Sep 17 00:00:00 2001 From: Will Martin Date: Fri, 22 Mar 2024 13:43:02 -0400 Subject: [PATCH 002/351] exclude bit-dialog from global header styles (#8441) --- apps/browser/src/popup/scss/base.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/browser/src/popup/scss/base.scss b/apps/browser/src/popup/scss/base.scss index 85983c6391..142152c339 100644 --- a/apps/browser/src/popup/scss/base.scss +++ b/apps/browser/src/popup/scss/base.scss @@ -169,7 +169,7 @@ cdk-virtual-scroll-viewport::-webkit-scrollbar-thumb, } } -header:not(bit-callout header) { +header:not(bit-callout header, bit-dialog header) { height: 44px; display: flex; From 7df9c597af176208fab816e2be0b25052b0aa3b3 Mon Sep 17 00:00:00 2001 From: Robyn MacCallum Date: Fri, 22 Mar 2024 13:56:42 -0400 Subject: [PATCH 003/351] Fix isAdmin check (#8432) --- .../src/app/secrets-manager/layout/navigation.component.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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( From 905d17787373ca7e4c41bd57457b50837fc7cb1d Mon Sep 17 00:00:00 2001 From: Cesar Gonzalez Date: Fri, 22 Mar 2024 13:45:33 -0500 Subject: [PATCH 004/351] [PM-4791] Injected content scripts prevent proper XML file display and disrupt XML responses (#8214) * [PM-4791] Injected content scripts prevent proper XML file display and disrupt XML responses * [PM-4791] Adjsuting reference for Fido2 script injection to ensure it only triggers on https protocol types --- apps/browser/src/manifest.json | 20 +++++------ apps/browser/src/manifest.v3.json | 33 ++++++++----------- .../src/vault/fido2/content/content-script.ts | 7 ++-- 3 files changed, 28 insertions(+), 32 deletions(-) diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index e3f6e6353f..7bdf92a597 100644 --- a/apps/browser/src/manifest.json +++ b/apps/browser/src/manifest.json @@ -18,23 +18,24 @@ { "all_frames": false, "js": ["content/content-message-handler.js"], - "matches": ["http://*/*", "https://*/*", "file:///*"], + "matches": ["*://*/*", "file:///*"], + "exclude_matches": ["*://*/*.xml*", "file:///*.xml*"], "run_at": "document_start" }, { "all_frames": true, - "js": [ - "content/trigger-autofill-script-injection.js", - "content/fido2/trigger-fido2-content-script-injection.js" - ], - "matches": ["http://*/*", "https://*/*", "file:///*"], + "js": ["content/fido2/trigger-fido2-content-script-injection.js"], + "matches": ["https://*/*"], + "exclude_matches": ["https://*/*.xml*"], "run_at": "document_start" }, { "all_frames": true, "css": ["content/autofill.css"], - "matches": ["http://*/*", "https://*/*", "file:///*"], - "run_at": "document_end" + "js": ["content/trigger-autofill-script-injection.js"], + "matches": ["*://*/*", "file:///*"], + "exclude_matches": ["*://*/*.xml*", "file:///*.xml*"], + "run_at": "document_start" }, { "all_frames": false, @@ -57,6 +58,7 @@ }, "permissions": [ "", + "*://*/*", "tabs", "contextMenus", "storage", @@ -64,8 +66,6 @@ "clipboardRead", "clipboardWrite", "idle", - "http://*/*", - "https://*/*", "webRequest", "webRequestBlocking" ], diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index 25215597f8..86510347d4 100644 --- a/apps/browser/src/manifest.v3.json +++ b/apps/browser/src/manifest.v3.json @@ -19,16 +19,23 @@ { "all_frames": false, "js": ["content/content-message-handler.js"], - "matches": ["http://*/*", "https://*/*", "file:///*"], + "matches": ["*://*/*", "file:///*"], + "exclude_matches": ["*://*/*.xml*", "file:///*.xml*"], "run_at": "document_start" }, { "all_frames": true, - "js": [ - "content/trigger-autofill-script-injection.js", - "content/fido2/trigger-fido2-content-script-injection.js" - ], - "matches": ["http://*/*", "https://*/*", "file:///*"], + "js": ["content/fido2/trigger-fido2-content-script-injection.js"], + "matches": ["https://*/*"], + "exclude_matches": ["https://*/*.xml*"], + "run_at": "document_start" + }, + { + "all_frames": true, + "css": ["content/autofill.css"], + "js": ["content/trigger-autofill-script-injection.js", "content/misc-utils.js"], + "matches": ["*://*/*", "file:///*"], + "exclude_matches": ["*://*/*.xml*", "file:///*.xml*"], "run_at": "document_start" }, { @@ -36,18 +43,6 @@ "js": ["content/lp-fileless-importer.js"], "matches": ["https://lastpass.com/export.php"], "run_at": "document_start" - }, - { - "all_frames": true, - "css": ["content/autofill.css"], - "matches": ["http://*/*", "https://*/*", "file:///*"], - "run_at": "document_end" - }, - { - "all_frames": true, - "js": ["content/misc-utils.js"], - "matches": ["http://*/*", "https://*/*", "file:///*"], - "run_at": "document_end" } ], "background": { @@ -76,7 +71,7 @@ "offscreen" ], "optional_permissions": ["nativeMessaging", "privacy"], - "host_permissions": ["http://*/*", "https://*/*"], + "host_permissions": ["*://*/*"], "content_security_policy": { "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'", "sandbox": "sandbox allow-scripts; script-src 'self'" 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(); +} From bac0874dc0331661a0af194d7937a72509736483 Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Fri, 22 Mar 2024 13:16:29 -0700 Subject: [PATCH 005/351] [PM-2383] Bulk collection assignment (#8429) * [PM-2383] Add bulkUpdateCollectionsWithServer method to CipherService * [PM-2383] Introduce bulk-collection-assignment-dialog.component * [PM-2383] Add bulk assign collections option to org vault --- .../vault-items/vault-item-event.ts | 3 +- .../vault-items/vault-items.component.html | 9 + .../vault-items/vault-items.component.ts | 14 ++ ...ollection-assignment-dialog.component.html | 66 ++++++ ...-collection-assignment-dialog.component.ts | 191 ++++++++++++++++++ .../index.ts | 1 + .../app/vault/org-vault/vault.component.html | 1 + .../app/vault/org-vault/vault.component.ts | 42 ++++ apps/web/src/locales/en/messages.json | 37 ++++ libs/common/src/types/guid.ts | 1 + .../src/vault/abstractions/cipher.service.ts | 14 ++ .../cipher-bulk-update-collections.request.ts | 19 ++ .../src/vault/services/cipher.service.ts | 47 ++++- 13 files changed, 443 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/bulk-collection-assignment-dialog.component.html create mode 100644 apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/bulk-collection-assignment-dialog.component.ts create mode 100644 apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/index.ts create mode 100644 libs/common/src/vault/models/request/cipher-bulk-update-collections.request.ts diff --git a/apps/web/src/app/vault/components/vault-items/vault-item-event.ts b/apps/web/src/app/vault/components/vault-items/vault-item-event.ts index b12076e359..f1f5cbc8c0 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-item-event.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-item-event.ts @@ -15,4 +15,5 @@ export type VaultItemEvent = | { type: "delete"; items: VaultItem[] } | { type: "copyField"; item: CipherView; field: "username" | "password" | "totp" } | { type: "moveToFolder"; items: CipherView[] } - | { type: "moveToOrganization"; items: CipherView[] }; + | { type: "moveToOrganization"; items: CipherView[] } + | { type: "assignToCollections"; items: CipherView[] }; diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.component.html b/apps/web/src/app/vault/components/vault-items/vault-items.component.html index ee284d0517..c63273fabd 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.component.html +++ b/apps/web/src/app/vault/components/vault-items/vault-items.component.html @@ -46,6 +46,15 @@ {{ "access" | i18n }} + + + + + + {{ "noCollectionsAssigned" | i18n }} + + + + + + + + + + + diff --git a/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/bulk-collection-assignment-dialog.component.ts b/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/bulk-collection-assignment-dialog.component.ts new file mode 100644 index 0000000000..04edce8543 --- /dev/null +++ b/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/bulk-collection-assignment-dialog.component.ts @@ -0,0 +1,191 @@ +import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; +import { Component, Inject, OnDestroy, OnInit } from "@angular/core"; +import { Subject } from "rxjs"; + +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { CipherId, CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; +import { DialogService, SelectItemView } from "@bitwarden/components"; + +import { SharedModule } from "../../../shared"; + +export interface BulkCollectionAssignmentDialogParams { + organizationId: OrganizationId; + + /** + * The ciphers to be assigned to the collections selected in the dialog. + */ + ciphers: CipherView[]; + + /** + * The collections available to assign the ciphers to. + */ + availableCollections: CollectionView[]; + + /** + * The currently filtered collection. Selected by default. If the user deselects it in the dialog then it will be + * removed from the ciphers upon submission. + */ + activeCollection?: CollectionView; +} + +export enum BulkCollectionAssignmentDialogResult { + Saved = "saved", + Canceled = "canceled", +} + +@Component({ + imports: [SharedModule], + selector: "app-bulk-collection-assignment-dialog", + templateUrl: "./bulk-collection-assignment-dialog.component.html", + standalone: true, +}) +export class BulkCollectionAssignmentDialogComponent implements OnDestroy, OnInit { + protected totalItemCount: number; + protected editableItemCount: number; + protected readonlyItemCount: number; + protected availableCollections: SelectItemView[] = []; + protected selectedCollections: SelectItemView[] = []; + + private editableItems: CipherView[] = []; + private destroy$ = new Subject(); + + protected pluralize = (count: number, singular: string, plural: string) => + `${count} ${this.i18nService.t(count === 1 ? singular : plural)}`; + + constructor( + @Inject(DIALOG_DATA) private params: BulkCollectionAssignmentDialogParams, + private dialogRef: DialogRef, + private cipherService: CipherService, + private i18nService: I18nService, + private platformUtilsService: PlatformUtilsService, + private configService: ConfigServiceAbstraction, + private organizationService: OrganizationService, + ) {} + + async ngOnInit() { + const v1FCEnabled = await this.configService.getFeatureFlag( + FeatureFlag.FlexibleCollectionsV1, + false, + ); + const org = await this.organizationService.get(this.params.organizationId); + + if (org.canEditAllCiphers(v1FCEnabled)) { + this.editableItems = this.params.ciphers; + } else { + this.editableItems = this.params.ciphers.filter((c) => c.edit); + } + + this.editableItemCount = this.editableItems.length; + + // If no ciphers are editable, close the dialog + if (this.editableItemCount == 0) { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + this.i18nService.t("nothingSelected"), + ); + this.dialogRef.close(BulkCollectionAssignmentDialogResult.Canceled); + } + + this.totalItemCount = this.params.ciphers.length; + this.readonlyItemCount = this.totalItemCount - this.editableItemCount; + + this.availableCollections = this.params.availableCollections.map((c) => ({ + icon: "bwi-collection", + id: c.id, + labelName: c.name, + listName: c.name, + })); + + // If the active collection is set, select it by default + if (this.params.activeCollection) { + this.selectCollections([ + { + icon: "bwi-collection", + id: this.params.activeCollection.id, + labelName: this.params.activeCollection.name, + listName: this.params.activeCollection.name, + }, + ]); + } + } + + private sortItems = (a: SelectItemView, b: SelectItemView) => + this.i18nService.collator.compare(a.labelName, b.labelName); + + selectCollections(items: SelectItemView[]) { + this.selectedCollections = [...this.selectedCollections, ...items].sort(this.sortItems); + + this.availableCollections = this.availableCollections.filter( + (item) => !items.find((i) => i.id === item.id), + ); + } + + unselectCollection(i: number) { + const removed = this.selectedCollections.splice(i, 1); + this.availableCollections = [...this.availableCollections, ...removed].sort(this.sortItems); + } + + get isValid() { + return this.params.activeCollection != null || this.selectedCollections.length > 0; + } + + submit = async () => { + if (!this.isValid) { + return; + } + + const cipherIds = this.editableItems.map((i) => i.id as CipherId); + + if (this.selectedCollections.length > 0) { + await this.cipherService.bulkUpdateCollectionsWithServer( + this.params.organizationId, + cipherIds, + this.selectedCollections.map((i) => i.id as CollectionId), + false, + ); + } + + if ( + this.params.activeCollection != null && + this.selectedCollections.find((c) => c.id === this.params.activeCollection.id) == null + ) { + await this.cipherService.bulkUpdateCollectionsWithServer( + this.params.organizationId, + cipherIds, + [this.params.activeCollection.id as CollectionId], + true, + ); + } + + this.platformUtilsService.showToast( + "success", + null, + this.i18nService.t("successfullyAssignedCollections"), + ); + + this.dialogRef.close(BulkCollectionAssignmentDialogResult.Saved); + }; + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + static open( + dialogService: DialogService, + config: DialogConfig, + ) { + return dialogService.open< + BulkCollectionAssignmentDialogResult, + BulkCollectionAssignmentDialogParams + >(BulkCollectionAssignmentDialogComponent, config); + } +} diff --git a/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/index.ts b/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/index.ts new file mode 100644 index 0000000000..44042e3267 --- /dev/null +++ b/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/index.ts @@ -0,0 +1 @@ +export * from "./bulk-collection-assignment-dialog.component"; diff --git a/apps/web/src/app/vault/org-vault/vault.component.html b/apps/web/src/app/vault/org-vault/vault.component.html index 08bf77be37..242a03b995 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.html +++ b/apps/web/src/app/vault/org-vault/vault.component.html @@ -54,6 +54,7 @@ [showBulkEditCollectionAccess]=" (showBulkEditCollectionAccess$ | async) && organization?.flexibleCollections " + [showBulkAddToCollections]="organization?.flexibleCollections" [viewingOrgVault]="true" > diff --git a/apps/web/src/app/vault/org-vault/vault.component.ts b/apps/web/src/app/vault/org-vault/vault.component.ts index ecec349482..6691404b3d 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.ts +++ b/apps/web/src/app/vault/org-vault/vault.component.ts @@ -46,6 +46,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 { Utils } from "@bitwarden/common/platform/misc/utils"; +import { OrganizationId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; @@ -86,6 +87,10 @@ import { getNestedCollectionTree } from "../utils/collection-utils"; import { AddEditComponent } from "./add-edit.component"; import { AttachmentsComponent } from "./attachments.component"; +import { + BulkCollectionAssignmentDialogComponent, + BulkCollectionAssignmentDialogResult, +} from "./bulk-collection-assignment-dialog"; import { BulkCollectionsDialogComponent, BulkCollectionsDialogResult, @@ -631,6 +636,8 @@ export class VaultComponent implements OnInit, OnDestroy { await this.editCollection(event.item, CollectionDialogTabType.Access); } else if (event.type === "bulkEditCollectionAccess") { await this.bulkEditCollectionAccess(event.items); + } else if (event.type === "assignToCollections") { + await this.bulkAssignToCollections(event.items); } else if (event.type === "viewEvents") { await this.viewEvents(event.item); } @@ -1092,6 +1099,41 @@ export class VaultComponent implements OnInit, OnDestroy { } } + async bulkAssignToCollections(items: CipherView[]) { + if (items.length === 0) { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + this.i18nService.t("nothingSelected"), + ); + return; + } + + let availableCollections: CollectionView[]; + + if (this.flexibleCollectionsV1Enabled) { + availableCollections = await firstValueFrom(this.editableCollections$); + } else { + availableCollections = ( + await firstValueFrom(this.vaultFilterService.filteredCollections$) + ).filter((c) => c.id != Unassigned); + } + + const dialog = BulkCollectionAssignmentDialogComponent.open(this.dialogService, { + data: { + ciphers: items, + organizationId: this.organization?.id as OrganizationId, + availableCollections, + activeCollection: this.activeFilter?.selectedCollectionNode?.node, + }, + }); + + const result = await lastValueFrom(dialog.closed); + if (result === BulkCollectionAssignmentDialogResult.Saved) { + this.refresh(); + } + } + async viewEvents(cipher: CipherView) { await openEntityEventsDialog(this.dialogService, { data: { diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 34e3c5d754..95d1b03e72 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "You cannot add yourself to collections." + }, + "assign": { + "message": "Assign" + }, + "assignToCollections": { + "message": "Assign to collections" + }, + "assignToTheseCollections": { + "message": "Assign to these collections" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "noCollectionsAssigned": { + "message": "No collections have been assigned" + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Items" } } diff --git a/libs/common/src/types/guid.ts b/libs/common/src/types/guid.ts index 7f88c82a9e..714f5dffc3 100644 --- a/libs/common/src/types/guid.ts +++ b/libs/common/src/types/guid.ts @@ -7,3 +7,4 @@ export type OrganizationId = Opaque; export type CollectionId = Opaque; export type ProviderId = Opaque; export type PolicyId = Opaque; +export type CipherId = Opaque; diff --git a/libs/common/src/vault/abstractions/cipher.service.ts b/libs/common/src/vault/abstractions/cipher.service.ts index 30b518612d..a8a0a25e9b 100644 --- a/libs/common/src/vault/abstractions/cipher.service.ts +++ b/libs/common/src/vault/abstractions/cipher.service.ts @@ -1,5 +1,6 @@ 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"; @@ -63,6 +64,19 @@ export abstract class CipherService { admin?: boolean, ) => Promise; saveCollectionsWithServer: (cipher: Cipher) => Promise; + /** + * 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; upsert: (cipher: CipherData | CipherData[]) => Promise; replace: (ciphers: { [id: string]: CipherData }) => Promise; clear: (userId: string) => Promise; 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/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 8a86d9aa05..4293e56728 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -21,7 +21,8 @@ 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 { UserKey, OrgKey } from "../../types/key"; +import { CipherId, CollectionId, OrganizationId } from "../../types/guid"; +import { OrgKey, UserKey } 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"; @@ -42,6 +43,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"; @@ -685,6 +687,49 @@ export class CipherService implements CipherServiceAbstraction { 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 { + 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 this.stateService.getEncryptedCiphers(); + + 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.stateService.setEncryptedCiphers(ciphers); + } + async upsert(cipher: CipherData | CipherData[]): Promise { let ciphers = await this.stateService.getEncryptedCiphers(); if (ciphers == null) { From 2a6f21200343daaa9af97b08578cd5b1f9d56252 Mon Sep 17 00:00:00 2001 From: Will Martin Date: Fri, 22 Mar 2024 19:36:09 -0400 Subject: [PATCH 006/351] [PM-4406] fix select overlay visibility when popup is zoomed (#8442) --- apps/browser/src/popup/scss/base.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/browser/src/popup/scss/base.scss b/apps/browser/src/popup/scss/base.scss index 142152c339..73da455941 100644 --- a/apps/browser/src/popup/scss/base.scss +++ b/apps/browser/src/popup/scss/base.scss @@ -17,6 +17,8 @@ body { body { width: 375px !important; height: 600px !important; + position: relative; + min-height: 100vh; overflow: hidden; color: $text-color; background-color: $background-color; From eea4d5407d0373c9808038f4887fa8cec9761881 Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Mon, 25 Mar 2024 11:12:32 +0100 Subject: [PATCH 007/351] Add the missing languages to the translation.service (#8459) This enables it being shown as anm option with the language selector on the individual clients Co-authored-by: Daniel James Smith --- apps/browser/src/platform/services/i18n.service.ts | 7 +++++++ apps/desktop/src/platform/services/i18n.main.service.ts | 8 ++++++++ .../src/platform/services/i18n.renderer.service.ts | 8 ++++++++ libs/common/src/platform/services/translation.service.ts | 7 +++++++ 4 files changed, 30 insertions(+) diff --git a/apps/browser/src/platform/services/i18n.service.ts b/apps/browser/src/platform/services/i18n.service.ts index 334ad8dc6c..a27c3935d7 100644 --- a/apps/browser/src/platform/services/i18n.service.ts +++ b/apps/browser/src/platform/services/i18n.service.ts @@ -25,6 +25,7 @@ export default class I18nService extends BaseI18nService { "bs", "ca", "cs", + "cy", "da", "de", "el", @@ -37,6 +38,7 @@ export default class I18nService extends BaseI18nService { "fi", "fil", "fr", + "gl", "he", "hi", "hr", @@ -51,9 +53,13 @@ export default class I18nService extends BaseI18nService { "lt", "lv", "ml", + "mr", + "my", "nb", + "ne", "nl", "nn", + "or", "pl", "pt-BR", "pt-PT", @@ -64,6 +70,7 @@ export default class I18nService extends BaseI18nService { "sl", "sr", "sv", + "te", "th", "tr", "uk", 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/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", "українська"], From 864e585cba13d651a2a7a0b54c3f872eeb5aed12 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 25 Mar 2024 10:24:20 +0000 Subject: [PATCH 008/351] Autosync the updated translations (#8463) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/web/src/locales/af/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/ar/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/az/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/be/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/bg/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/bn/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/bs/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/ca/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/cs/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/cy/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/da/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/de/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/el/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/en_GB/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/en_IN/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/eo/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/es/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/et/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/eu/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/fa/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/fi/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/fil/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/fr/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/gl/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/he/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/hi/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/hr/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/hu/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/id/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/it/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/ja/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/ka/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/km/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/kn/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/ko/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/lv/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/ml/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/mr/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/my/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/nb/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/ne/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/nl/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/nn/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/or/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/pl/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/pt_BR/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/pt_PT/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/ro/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/ru/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/si/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/sk/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/sl/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/sr/messages.json | 43 ++++++++++++++++++++++-- apps/web/src/locales/sr_CS/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/sv/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/te/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/th/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/tr/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/uk/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/vi/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/zh_CN/messages.json | 37 ++++++++++++++++++++ apps/web/src/locales/zh_TW/messages.json | 37 ++++++++++++++++++++ 62 files changed, 2297 insertions(+), 3 deletions(-) diff --git a/apps/web/src/locales/af/messages.json b/apps/web/src/locales/af/messages.json index 1900c9cf8b..3677e54c1c 100644 --- a/apps/web/src/locales/af/messages.json +++ b/apps/web/src/locales/af/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "You cannot add yourself to collections." + }, + "assign": { + "message": "Assign" + }, + "assignToCollections": { + "message": "Assign to collections" + }, + "assignToTheseCollections": { + "message": "Assign to these collections" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "noCollectionsAssigned": { + "message": "No collections have been assigned" + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Items" } } diff --git a/apps/web/src/locales/ar/messages.json b/apps/web/src/locales/ar/messages.json index ba9a6f75a0..f5353f2234 100644 --- a/apps/web/src/locales/ar/messages.json +++ b/apps/web/src/locales/ar/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "You cannot add yourself to collections." + }, + "assign": { + "message": "Assign" + }, + "assignToCollections": { + "message": "Assign to collections" + }, + "assignToTheseCollections": { + "message": "Assign to these collections" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "noCollectionsAssigned": { + "message": "No collections have been assigned" + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Items" } } diff --git a/apps/web/src/locales/az/messages.json b/apps/web/src/locales/az/messages.json index cc3391432e..bc30012efe 100644 --- a/apps/web/src/locales/az/messages.json +++ b/apps/web/src/locales/az/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "Özünüzü kolleksiyalara əlavə edə bilməzsiniz." + }, + "assign": { + "message": "Assign" + }, + "assignToCollections": { + "message": "Assign to collections" + }, + "assignToTheseCollections": { + "message": "Assign to these collections" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "noCollectionsAssigned": { + "message": "No collections have been assigned" + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Items" } } diff --git a/apps/web/src/locales/be/messages.json b/apps/web/src/locales/be/messages.json index 795a1df673..2ec27ae4be 100644 --- a/apps/web/src/locales/be/messages.json +++ b/apps/web/src/locales/be/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "You cannot add yourself to collections." + }, + "assign": { + "message": "Assign" + }, + "assignToCollections": { + "message": "Assign to collections" + }, + "assignToTheseCollections": { + "message": "Assign to these collections" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "noCollectionsAssigned": { + "message": "No collections have been assigned" + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Items" } } diff --git a/apps/web/src/locales/bg/messages.json b/apps/web/src/locales/bg/messages.json index 3bfcd00498..a0d1781736 100644 --- a/apps/web/src/locales/bg/messages.json +++ b/apps/web/src/locales/bg/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "Не може да добавяте себе си към колекции." + }, + "assign": { + "message": "Свързване" + }, + "assignToCollections": { + "message": "Свързване с колекции" + }, + "assignToTheseCollections": { + "message": "Свързване с тези колекции" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Изберете колекциите, с които да бъдат споделени тези елементи. Когато даден елемент бъде променен в една колекция, промяната ще бъде отразена във всички колекции. Само членовете на организацията с достъп до тези колекции ще могат да виждат елементите." + }, + "selectCollectionsToAssign": { + "message": "Изберете колекции за свързване" + }, + "noCollectionsAssigned": { + "message": "Няма свързани колекции" + }, + "successfullyAssignedCollections": { + "message": "Успешно свързване на колекциите" + }, + "bulkCollectionAssignmentWarning": { + "message": "Избрали сте $TOTAL_COUNT$ елемента Не можете да промените $READONLY_COUNT$ от елементите, тъй като нямате право за редактиране.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Елементи" } } diff --git a/apps/web/src/locales/bn/messages.json b/apps/web/src/locales/bn/messages.json index bcdb6b09d5..9d0ccd3b50 100644 --- a/apps/web/src/locales/bn/messages.json +++ b/apps/web/src/locales/bn/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "You cannot add yourself to collections." + }, + "assign": { + "message": "Assign" + }, + "assignToCollections": { + "message": "Assign to collections" + }, + "assignToTheseCollections": { + "message": "Assign to these collections" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "noCollectionsAssigned": { + "message": "No collections have been assigned" + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Items" } } diff --git a/apps/web/src/locales/bs/messages.json b/apps/web/src/locales/bs/messages.json index efb1d1a564..84084847d4 100644 --- a/apps/web/src/locales/bs/messages.json +++ b/apps/web/src/locales/bs/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "You cannot add yourself to collections." + }, + "assign": { + "message": "Assign" + }, + "assignToCollections": { + "message": "Assign to collections" + }, + "assignToTheseCollections": { + "message": "Assign to these collections" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "noCollectionsAssigned": { + "message": "No collections have been assigned" + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Items" } } diff --git a/apps/web/src/locales/ca/messages.json b/apps/web/src/locales/ca/messages.json index 976fdecda7..e23f7acec4 100644 --- a/apps/web/src/locales/ca/messages.json +++ b/apps/web/src/locales/ca/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "No podeu afegir-vos a les col·leccions." + }, + "assign": { + "message": "Assign" + }, + "assignToCollections": { + "message": "Assign to collections" + }, + "assignToTheseCollections": { + "message": "Assign to these collections" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "noCollectionsAssigned": { + "message": "No collections have been assigned" + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Items" } } diff --git a/apps/web/src/locales/cs/messages.json b/apps/web/src/locales/cs/messages.json index 41956ad180..523f1faa10 100644 --- a/apps/web/src/locales/cs/messages.json +++ b/apps/web/src/locales/cs/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "Do kolekcí nemůžete přidat sami sebe." + }, + "assign": { + "message": "Přiřadit" + }, + "assignToCollections": { + "message": "Přiřadit ke kolekcím" + }, + "assignToTheseCollections": { + "message": "Přiřadit k těmto kolekcím" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Vyberte kolekce, se kterými budou položky sdíleny. Jakmile bude položka aktualizována v jedné kolekci, bude zobrazena ve všech kolekcích. Jen členové organizace s přístupem k těmto kolekcím budou moci vidět položky." + }, + "selectCollectionsToAssign": { + "message": "Vyberte kolekce pro přiřazení" + }, + "noCollectionsAssigned": { + "message": "Nebyly přiřazeny žádné kolekce" + }, + "successfullyAssignedCollections": { + "message": "Kolekce byly úspěšně přiřazeny" + }, + "bulkCollectionAssignmentWarning": { + "message": "Vybrali jste $TOTAL_COUNT$ položek. Nemůžete aktualizovat $READONLY_COUNT$ položek, protože nemáte oprávnění k úpravám.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Položky" } } diff --git a/apps/web/src/locales/cy/messages.json b/apps/web/src/locales/cy/messages.json index cb19cb8780..4781d9d3b6 100644 --- a/apps/web/src/locales/cy/messages.json +++ b/apps/web/src/locales/cy/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "You cannot add yourself to collections." + }, + "assign": { + "message": "Assign" + }, + "assignToCollections": { + "message": "Assign to collections" + }, + "assignToTheseCollections": { + "message": "Assign to these collections" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "noCollectionsAssigned": { + "message": "No collections have been assigned" + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Items" } } diff --git a/apps/web/src/locales/da/messages.json b/apps/web/src/locales/da/messages.json index c8fab2d0e3..bcb1c16235 100644 --- a/apps/web/src/locales/da/messages.json +++ b/apps/web/src/locales/da/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "Man kan ikke føje sig selv til samlinger." + }, + "assign": { + "message": "Tildel" + }, + "assignToCollections": { + "message": "Tildel til samlinger" + }, + "assignToTheseCollections": { + "message": "Tildel til samlinger" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Vælg de samlinger som emnerne vil blive delt med. Når et emne er opdateret i en samling, vil det blive afspejlet i alle samlinger. Kun organisationsmedlemmer med adgang til disse samlinger vil kunne se emnerne." + }, + "selectCollectionsToAssign": { + "message": "Vælg samlinger at tildele" + }, + "noCollectionsAssigned": { + "message": "Ingen samlinger er blevet tildelt" + }, + "successfullyAssignedCollections": { + "message": "Samlinger hermed tildelt" + }, + "bulkCollectionAssignmentWarning": { + "message": "Der er valgt $TOTAL_COUNT$ emner. $READONLY_COUNT$ af emnerne kan ikke opdateres, da du ikke har redigeringsrettigheder.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Emner" } } diff --git a/apps/web/src/locales/de/messages.json b/apps/web/src/locales/de/messages.json index e71663444b..c3e5a9bba5 100644 --- a/apps/web/src/locales/de/messages.json +++ b/apps/web/src/locales/de/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "Du kannst dich nicht selbst zu Sammlungen hinzufügen." + }, + "assign": { + "message": "Zuweisen" + }, + "assignToCollections": { + "message": "Sammlungen zuweisen" + }, + "assignToTheseCollections": { + "message": "Diesen Sammlungen zuweisen" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Wähle die Sammlungen, mit denen die Einträge geteilt werden. Sobald ein Eintrag in einer Sammlung aktualisiert wird, wird es in allen Sammlungen reflektiert. Nur Mitglieder von Organisationen mit Zugang zu diesen Sammlungen können die Einträge sehen." + }, + "selectCollectionsToAssign": { + "message": "Zu zuweisende Sammlungen auswählen" + }, + "noCollectionsAssigned": { + "message": "Es wurden keine Sammlungen zugewiesen" + }, + "successfullyAssignedCollections": { + "message": "Sammlungen erfolgreich zugewiesen" + }, + "bulkCollectionAssignmentWarning": { + "message": "Du hast $TOTAL_COUNT$ Einträge ausgewählt. Du kannst $READONLY_COUNT$ der Einträge nicht aktualisieren, da du keine Bearbeitungsrechte hast.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Einträge" } } diff --git a/apps/web/src/locales/el/messages.json b/apps/web/src/locales/el/messages.json index 3fd8501c6e..0babeee15d 100644 --- a/apps/web/src/locales/el/messages.json +++ b/apps/web/src/locales/el/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "You cannot add yourself to collections." + }, + "assign": { + "message": "Assign" + }, + "assignToCollections": { + "message": "Assign to collections" + }, + "assignToTheseCollections": { + "message": "Assign to these collections" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "noCollectionsAssigned": { + "message": "No collections have been assigned" + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Items" } } diff --git a/apps/web/src/locales/en_GB/messages.json b/apps/web/src/locales/en_GB/messages.json index 9478c628c0..d70d6cff23 100644 --- a/apps/web/src/locales/en_GB/messages.json +++ b/apps/web/src/locales/en_GB/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "You cannot add yourself to collections." + }, + "assign": { + "message": "Assign" + }, + "assignToCollections": { + "message": "Assign to collections" + }, + "assignToTheseCollections": { + "message": "Assign to these collections" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "noCollectionsAssigned": { + "message": "No collections have been assigned" + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Items" } } diff --git a/apps/web/src/locales/en_IN/messages.json b/apps/web/src/locales/en_IN/messages.json index 9040d7714a..561d267cab 100644 --- a/apps/web/src/locales/en_IN/messages.json +++ b/apps/web/src/locales/en_IN/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "You cannot add yourself to collections." + }, + "assign": { + "message": "Assign" + }, + "assignToCollections": { + "message": "Assign to collections" + }, + "assignToTheseCollections": { + "message": "Assign to these collections" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "noCollectionsAssigned": { + "message": "No collections have been assigned" + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Items" } } diff --git a/apps/web/src/locales/eo/messages.json b/apps/web/src/locales/eo/messages.json index 92c4329e01..d3a9ad414a 100644 --- a/apps/web/src/locales/eo/messages.json +++ b/apps/web/src/locales/eo/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "You cannot add yourself to collections." + }, + "assign": { + "message": "Assign" + }, + "assignToCollections": { + "message": "Assign to collections" + }, + "assignToTheseCollections": { + "message": "Assign to these collections" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "noCollectionsAssigned": { + "message": "No collections have been assigned" + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Items" } } diff --git a/apps/web/src/locales/es/messages.json b/apps/web/src/locales/es/messages.json index 30baca8b3d..250614565d 100644 --- a/apps/web/src/locales/es/messages.json +++ b/apps/web/src/locales/es/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "You cannot add yourself to collections." + }, + "assign": { + "message": "Assign" + }, + "assignToCollections": { + "message": "Assign to collections" + }, + "assignToTheseCollections": { + "message": "Assign to these collections" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "noCollectionsAssigned": { + "message": "No collections have been assigned" + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Items" } } diff --git a/apps/web/src/locales/et/messages.json b/apps/web/src/locales/et/messages.json index c67fefe605..1866c649e4 100644 --- a/apps/web/src/locales/et/messages.json +++ b/apps/web/src/locales/et/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "You cannot add yourself to collections." + }, + "assign": { + "message": "Assign" + }, + "assignToCollections": { + "message": "Assign to collections" + }, + "assignToTheseCollections": { + "message": "Assign to these collections" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "noCollectionsAssigned": { + "message": "No collections have been assigned" + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Items" } } diff --git a/apps/web/src/locales/eu/messages.json b/apps/web/src/locales/eu/messages.json index f1d508c102..0c35039424 100644 --- a/apps/web/src/locales/eu/messages.json +++ b/apps/web/src/locales/eu/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "You cannot add yourself to collections." + }, + "assign": { + "message": "Assign" + }, + "assignToCollections": { + "message": "Assign to collections" + }, + "assignToTheseCollections": { + "message": "Assign to these collections" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "noCollectionsAssigned": { + "message": "No collections have been assigned" + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Items" } } diff --git a/apps/web/src/locales/fa/messages.json b/apps/web/src/locales/fa/messages.json index 86f441e0be..2c3d10c6a8 100644 --- a/apps/web/src/locales/fa/messages.json +++ b/apps/web/src/locales/fa/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "You cannot add yourself to collections." + }, + "assign": { + "message": "Assign" + }, + "assignToCollections": { + "message": "Assign to collections" + }, + "assignToTheseCollections": { + "message": "Assign to these collections" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "noCollectionsAssigned": { + "message": "No collections have been assigned" + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Items" } } diff --git a/apps/web/src/locales/fi/messages.json b/apps/web/src/locales/fi/messages.json index a4a45076d6..43bbf01847 100644 --- a/apps/web/src/locales/fi/messages.json +++ b/apps/web/src/locales/fi/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "Et voi lisätä itseäsi kokoelmiin." + }, + "assign": { + "message": "Määritä" + }, + "assignToCollections": { + "message": "Määritä kokoelmiin" + }, + "assignToTheseCollections": { + "message": "Määritä näihin kokoelmiin" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Valitse kokoelmat, joihin kohteet jaetaan. Kun kohdetta muokataan yhdessä kokoelmassa, päivittyy muutos kaikkiin kokoelmiin. Kohteet näkyvät vain niille organisaation jäsenille, joilla on näiden kokoelmien käyttöoikeus." + }, + "selectCollectionsToAssign": { + "message": "Valitse määritettävät kokoelmat" + }, + "noCollectionsAssigned": { + "message": "Kokoelmia ei ole määritetty" + }, + "successfullyAssignedCollections": { + "message": "Kokoelmat on määritetty" + }, + "bulkCollectionAssignmentWarning": { + "message": "Olet valinnut $TOTAL_COUNT$ kohdetta. Et voi muuttaa näistä $READONLY_COUNT$ kohdetta, koska käyttöoikeutesi eivät riitä niiden muokkaukseen.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Kohteet" } } diff --git a/apps/web/src/locales/fil/messages.json b/apps/web/src/locales/fil/messages.json index 4c1ca9787a..bfcdccc7a2 100644 --- a/apps/web/src/locales/fil/messages.json +++ b/apps/web/src/locales/fil/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "You cannot add yourself to collections." + }, + "assign": { + "message": "Assign" + }, + "assignToCollections": { + "message": "Assign to collections" + }, + "assignToTheseCollections": { + "message": "Assign to these collections" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "noCollectionsAssigned": { + "message": "No collections have been assigned" + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Items" } } diff --git a/apps/web/src/locales/fr/messages.json b/apps/web/src/locales/fr/messages.json index 271c1d3b2e..9cc40e4133 100644 --- a/apps/web/src/locales/fr/messages.json +++ b/apps/web/src/locales/fr/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "Vous ne pouvez vous ajoutez vous-même aux collections." + }, + "assign": { + "message": "Assigner" + }, + "assignToCollections": { + "message": "Assigner aux collections" + }, + "assignToTheseCollections": { + "message": "Assigner à ces collections" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Sélectionnez les collections avec lesquelles les éléments seront partagés. Une fois qu'un élément est mis à jour dans une collection, il le sera aussi dans toutes ces collections. Seuls les membres de l'organisation ayant accès à ces collections pourront voir les éléments." + }, + "selectCollectionsToAssign": { + "message": "Sélectionnez les collections à assigner" + }, + "noCollectionsAssigned": { + "message": "Aucune collection n'a été assignée" + }, + "successfullyAssignedCollections": { + "message": "Collections assignées avec succès" + }, + "bulkCollectionAssignmentWarning": { + "message": "Vous avez sélectionné $TOTAL_COUNT$ éléments. Vous ne pouvez pas mettre à jour $READONLY_COUNT$ de ces éléments parce que vous n'avez pas les autorisations pour les éditer.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Éléments" } } diff --git a/apps/web/src/locales/gl/messages.json b/apps/web/src/locales/gl/messages.json index 0562f4bf84..ad51cf605f 100644 --- a/apps/web/src/locales/gl/messages.json +++ b/apps/web/src/locales/gl/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "You cannot add yourself to collections." + }, + "assign": { + "message": "Assign" + }, + "assignToCollections": { + "message": "Assign to collections" + }, + "assignToTheseCollections": { + "message": "Assign to these collections" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "noCollectionsAssigned": { + "message": "No collections have been assigned" + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Items" } } diff --git a/apps/web/src/locales/he/messages.json b/apps/web/src/locales/he/messages.json index 2f82b1b71d..0c8316fe49 100644 --- a/apps/web/src/locales/he/messages.json +++ b/apps/web/src/locales/he/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "You cannot add yourself to collections." + }, + "assign": { + "message": "Assign" + }, + "assignToCollections": { + "message": "Assign to collections" + }, + "assignToTheseCollections": { + "message": "Assign to these collections" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "noCollectionsAssigned": { + "message": "No collections have been assigned" + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Items" } } diff --git a/apps/web/src/locales/hi/messages.json b/apps/web/src/locales/hi/messages.json index a583f273b5..b00c912d7a 100644 --- a/apps/web/src/locales/hi/messages.json +++ b/apps/web/src/locales/hi/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "You cannot add yourself to collections." + }, + "assign": { + "message": "Assign" + }, + "assignToCollections": { + "message": "Assign to collections" + }, + "assignToTheseCollections": { + "message": "Assign to these collections" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "noCollectionsAssigned": { + "message": "No collections have been assigned" + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Items" } } diff --git a/apps/web/src/locales/hr/messages.json b/apps/web/src/locales/hr/messages.json index 446e511517..4d47c12e86 100644 --- a/apps/web/src/locales/hr/messages.json +++ b/apps/web/src/locales/hr/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "You cannot add yourself to collections." + }, + "assign": { + "message": "Assign" + }, + "assignToCollections": { + "message": "Assign to collections" + }, + "assignToTheseCollections": { + "message": "Assign to these collections" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "noCollectionsAssigned": { + "message": "No collections have been assigned" + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Items" } } diff --git a/apps/web/src/locales/hu/messages.json b/apps/web/src/locales/hu/messages.json index 0540b6e6ba..e1f3682bbd 100644 --- a/apps/web/src/locales/hu/messages.json +++ b/apps/web/src/locales/hu/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "Nem adhadjuk magunkat a gyűjteményhez." + }, + "assign": { + "message": "Hozzárendelés" + }, + "assignToCollections": { + "message": "Hozzárendelés gyűjteményekhez" + }, + "assignToTheseCollections": { + "message": "Hozzárendelés ezen gyűjteményekhez" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Válasszuk ki azokat a gyűjteményeket, amelyekkel az elemek megosztásra kerülnek. Ha egy elem egy gyűjteményben frissítésre kerül, az az összes gyűjteményben megjelenik. Csak az ezekhez a gyűjteményekhez hozzáféréssel rendelkező szervezeti tagok láthatják az elemeket." + }, + "selectCollectionsToAssign": { + "message": "Hozzárendelendő gyűjtemények kiválasztása" + }, + "noCollectionsAssigned": { + "message": "Nem lettek gyűjtemények hozzárendelve." + }, + "successfullyAssignedCollections": { + "message": "A gyűjtemények sikeresen hozzárendelésre kerültek." + }, + "bulkCollectionAssignmentWarning": { + "message": "$TOTAL_COUNT$ elem lett kiválasztva. Az elemek közül $READONLY_COUNT$ nem frissíthető, mert nincs szerkesztési jogosultság.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Elemek" } } diff --git a/apps/web/src/locales/id/messages.json b/apps/web/src/locales/id/messages.json index 6ea4a4d7b3..83d6c42673 100644 --- a/apps/web/src/locales/id/messages.json +++ b/apps/web/src/locales/id/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "You cannot add yourself to collections." + }, + "assign": { + "message": "Assign" + }, + "assignToCollections": { + "message": "Assign to collections" + }, + "assignToTheseCollections": { + "message": "Assign to these collections" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "noCollectionsAssigned": { + "message": "No collections have been assigned" + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Items" } } diff --git a/apps/web/src/locales/it/messages.json b/apps/web/src/locales/it/messages.json index 7f9509e8d2..c31ff1a5a2 100644 --- a/apps/web/src/locales/it/messages.json +++ b/apps/web/src/locales/it/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "Non puoi aggiungerti da solo alle raccolte." + }, + "assign": { + "message": "Assegna" + }, + "assignToCollections": { + "message": "Assegna alle raccolte" + }, + "assignToTheseCollections": { + "message": "Assegna a queste raccolte" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Seleziona le raccolte con cui questi elementi saranno condivisi. Una volta un elemento è aggiornato in una raccolta, la modifica si rifletterà in tutte le raccolte. Solo i membri dell'organizzazione con accesso a queste raccolte potranno visualizzare gli elementi." + }, + "selectCollectionsToAssign": { + "message": "Seleziona le raccolte da assegnare" + }, + "noCollectionsAssigned": { + "message": "Nessuna raccolta è stata assegnata" + }, + "successfullyAssignedCollections": { + "message": "Raccolte assegnate" + }, + "bulkCollectionAssignmentWarning": { + "message": "Hai selezionato $TOTAL_COUNT$ elementi. Non puoi aggiornare $READONLY_COUNT$ elementi perché non hai l'autorizzazione per modificarli.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Elementi" } } diff --git a/apps/web/src/locales/ja/messages.json b/apps/web/src/locales/ja/messages.json index 95333eebb0..c2e3e77b24 100644 --- a/apps/web/src/locales/ja/messages.json +++ b/apps/web/src/locales/ja/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "コレクションに自分自身を追加することはできません。" + }, + "assign": { + "message": "割り当て" + }, + "assignToCollections": { + "message": "コレクションに割り当てる" + }, + "assignToTheseCollections": { + "message": "これらのコレクションに割り当てる" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "アイテムを共有するコレクションを選択します。1つのコレクションでアイテムが更新されると、すべてのコレクションに反映されます。これらのコレクションにアクセスできる組織メンバーだけがアイテムを見ることができます。" + }, + "selectCollectionsToAssign": { + "message": "割り当てるコレクションを選択" + }, + "noCollectionsAssigned": { + "message": "コレクションが割り当てられていません" + }, + "successfullyAssignedCollections": { + "message": "コレクションの割り当てに成功しました" + }, + "bulkCollectionAssignmentWarning": { + "message": "$TOTAL_COUNT$ アイテムを選択しました。編集権限がないため、$READONLY_COUNT$ アイテムを更新できません。", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "アイテム" } } diff --git a/apps/web/src/locales/ka/messages.json b/apps/web/src/locales/ka/messages.json index ed27946a58..e288311956 100644 --- a/apps/web/src/locales/ka/messages.json +++ b/apps/web/src/locales/ka/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "You cannot add yourself to collections." + }, + "assign": { + "message": "Assign" + }, + "assignToCollections": { + "message": "Assign to collections" + }, + "assignToTheseCollections": { + "message": "Assign to these collections" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "noCollectionsAssigned": { + "message": "No collections have been assigned" + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Items" } } diff --git a/apps/web/src/locales/km/messages.json b/apps/web/src/locales/km/messages.json index 0562f4bf84..ad51cf605f 100644 --- a/apps/web/src/locales/km/messages.json +++ b/apps/web/src/locales/km/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "You cannot add yourself to collections." + }, + "assign": { + "message": "Assign" + }, + "assignToCollections": { + "message": "Assign to collections" + }, + "assignToTheseCollections": { + "message": "Assign to these collections" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "noCollectionsAssigned": { + "message": "No collections have been assigned" + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Items" } } diff --git a/apps/web/src/locales/kn/messages.json b/apps/web/src/locales/kn/messages.json index 48546d4373..02631198e3 100644 --- a/apps/web/src/locales/kn/messages.json +++ b/apps/web/src/locales/kn/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "You cannot add yourself to collections." + }, + "assign": { + "message": "Assign" + }, + "assignToCollections": { + "message": "Assign to collections" + }, + "assignToTheseCollections": { + "message": "Assign to these collections" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "noCollectionsAssigned": { + "message": "No collections have been assigned" + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Items" } } diff --git a/apps/web/src/locales/ko/messages.json b/apps/web/src/locales/ko/messages.json index c2462146c3..1867603a0d 100644 --- a/apps/web/src/locales/ko/messages.json +++ b/apps/web/src/locales/ko/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "You cannot add yourself to collections." + }, + "assign": { + "message": "Assign" + }, + "assignToCollections": { + "message": "Assign to collections" + }, + "assignToTheseCollections": { + "message": "Assign to these collections" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "noCollectionsAssigned": { + "message": "No collections have been assigned" + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Items" } } diff --git a/apps/web/src/locales/lv/messages.json b/apps/web/src/locales/lv/messages.json index 48b9c7abb1..8c2df3012a 100644 --- a/apps/web/src/locales/lv/messages.json +++ b/apps/web/src/locales/lv/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "Tu nevari sevi pievienot krājumiem." + }, + "assign": { + "message": "Piešķirt" + }, + "assignToCollections": { + "message": "Piešķirt krājumiem" + }, + "assignToTheseCollections": { + "message": "Piešķirt šiem krājumiem" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Jāatlasa krājumi, ar kuriem vienumi tiks kopīgoti. Tiklīdz kāds vienums tiks atjaunināts vienā krājumā, tas atspoguļosies visos pārējos. Tikai apvienības dalībnieki ar piekļuvi šiem krājumiem varēs redzēt vienumus." + }, + "selectCollectionsToAssign": { + "message": "Atlasīt krājumus, lai piešķirtu" + }, + "noCollectionsAssigned": { + "message": "Neviens krājums nav piešķirts" + }, + "successfullyAssignedCollections": { + "message": "Krājumi veiksmīgi piešķirti" + }, + "bulkCollectionAssignmentWarning": { + "message": "Ir atlasīti $TOTAL_COUNT$ vienumi. Nevar atjaunināt $READONLY_COUNT$ no vienumiem, jo trūkst labošanas atļaujas.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Vienumi" } } diff --git a/apps/web/src/locales/ml/messages.json b/apps/web/src/locales/ml/messages.json index 270db2fa07..81b6529793 100644 --- a/apps/web/src/locales/ml/messages.json +++ b/apps/web/src/locales/ml/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "You cannot add yourself to collections." + }, + "assign": { + "message": "Assign" + }, + "assignToCollections": { + "message": "Assign to collections" + }, + "assignToTheseCollections": { + "message": "Assign to these collections" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "noCollectionsAssigned": { + "message": "No collections have been assigned" + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Items" } } diff --git a/apps/web/src/locales/mr/messages.json b/apps/web/src/locales/mr/messages.json index 0562f4bf84..ad51cf605f 100644 --- a/apps/web/src/locales/mr/messages.json +++ b/apps/web/src/locales/mr/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "You cannot add yourself to collections." + }, + "assign": { + "message": "Assign" + }, + "assignToCollections": { + "message": "Assign to collections" + }, + "assignToTheseCollections": { + "message": "Assign to these collections" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "noCollectionsAssigned": { + "message": "No collections have been assigned" + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Items" } } diff --git a/apps/web/src/locales/my/messages.json b/apps/web/src/locales/my/messages.json index 0562f4bf84..ad51cf605f 100644 --- a/apps/web/src/locales/my/messages.json +++ b/apps/web/src/locales/my/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "You cannot add yourself to collections." + }, + "assign": { + "message": "Assign" + }, + "assignToCollections": { + "message": "Assign to collections" + }, + "assignToTheseCollections": { + "message": "Assign to these collections" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "noCollectionsAssigned": { + "message": "No collections have been assigned" + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Items" } } diff --git a/apps/web/src/locales/nb/messages.json b/apps/web/src/locales/nb/messages.json index babffc6d3c..8840ef7c85 100644 --- a/apps/web/src/locales/nb/messages.json +++ b/apps/web/src/locales/nb/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "You cannot add yourself to collections." + }, + "assign": { + "message": "Assign" + }, + "assignToCollections": { + "message": "Assign to collections" + }, + "assignToTheseCollections": { + "message": "Assign to these collections" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "noCollectionsAssigned": { + "message": "No collections have been assigned" + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Items" } } diff --git a/apps/web/src/locales/ne/messages.json b/apps/web/src/locales/ne/messages.json index 68fc8b2774..a42b9d71af 100644 --- a/apps/web/src/locales/ne/messages.json +++ b/apps/web/src/locales/ne/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "You cannot add yourself to collections." + }, + "assign": { + "message": "Assign" + }, + "assignToCollections": { + "message": "Assign to collections" + }, + "assignToTheseCollections": { + "message": "Assign to these collections" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "noCollectionsAssigned": { + "message": "No collections have been assigned" + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Items" } } diff --git a/apps/web/src/locales/nl/messages.json b/apps/web/src/locales/nl/messages.json index 21db16911f..0b99351080 100644 --- a/apps/web/src/locales/nl/messages.json +++ b/apps/web/src/locales/nl/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "You cannot add yourself to collections." + }, + "assign": { + "message": "Toewijzen" + }, + "assignToCollections": { + "message": "Toewijzen aan collecties" + }, + "assignToTheseCollections": { + "message": "Aan deze collecties toewijzen" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Selecteer de collecies om de items mee te delen. Zodra een item in een collectie is bijgewerkt, werkt dat door in alle collecties. Alleen organisatieleden met toegang tot deze collecties kunnen de items zien." + }, + "selectCollectionsToAssign": { + "message": "Collecties voor toewijzen selecteren" + }, + "noCollectionsAssigned": { + "message": "Er zijn geen collecties toegewezen" + }, + "successfullyAssignedCollections": { + "message": "Succesvol toegewezen collecties" + }, + "bulkCollectionAssignmentWarning": { + "message": "Je hebt $TOTAL_COUNT$ items geselecteerd. Je kunt $READONLY_COUNT$ items niet bijwerken omdat je geen bewerkrechten hebt.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Items" } } diff --git a/apps/web/src/locales/nn/messages.json b/apps/web/src/locales/nn/messages.json index 888c7b1b9c..7ac2fff5ce 100644 --- a/apps/web/src/locales/nn/messages.json +++ b/apps/web/src/locales/nn/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "You cannot add yourself to collections." + }, + "assign": { + "message": "Assign" + }, + "assignToCollections": { + "message": "Assign to collections" + }, + "assignToTheseCollections": { + "message": "Assign to these collections" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "noCollectionsAssigned": { + "message": "No collections have been assigned" + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Items" } } diff --git a/apps/web/src/locales/or/messages.json b/apps/web/src/locales/or/messages.json index 0562f4bf84..ad51cf605f 100644 --- a/apps/web/src/locales/or/messages.json +++ b/apps/web/src/locales/or/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "You cannot add yourself to collections." + }, + "assign": { + "message": "Assign" + }, + "assignToCollections": { + "message": "Assign to collections" + }, + "assignToTheseCollections": { + "message": "Assign to these collections" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "noCollectionsAssigned": { + "message": "No collections have been assigned" + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Items" } } diff --git a/apps/web/src/locales/pl/messages.json b/apps/web/src/locales/pl/messages.json index f8d9374344..08d6748023 100644 --- a/apps/web/src/locales/pl/messages.json +++ b/apps/web/src/locales/pl/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "You cannot add yourself to collections." + }, + "assign": { + "message": "Przypisz" + }, + "assignToCollections": { + "message": "Przypisz do kolekcji" + }, + "assignToTheseCollections": { + "message": "Przypisz do tych kolekcji" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Wybierz kolekcje, z którymi elementy będą udostępniane. Gdy element zostanie zaktualizowany w jednej kolekcji, zostanie to odzwierciedlone we wszystkich kolekcjach. Tylko członkowie organizacji z dostępem do tych kolekcji będą mogli zobaczyć te elementy." + }, + "selectCollectionsToAssign": { + "message": "Wybierz kolekcje do przypisania" + }, + "noCollectionsAssigned": { + "message": "Nie przypisano kolekcji" + }, + "successfullyAssignedCollections": { + "message": "Pomyślnie przypisano kolekcje" + }, + "bulkCollectionAssignmentWarning": { + "message": "Wybrałeś $TOTAL_COUNT$ elementów. Nie możesz zaktualizować $READONLY_COUNT$ elementów, ponieważ nie masz uprawnień do edycji.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Elementy" } } diff --git a/apps/web/src/locales/pt_BR/messages.json b/apps/web/src/locales/pt_BR/messages.json index 402697de2b..5ff05ed37e 100644 --- a/apps/web/src/locales/pt_BR/messages.json +++ b/apps/web/src/locales/pt_BR/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "Você não pode se adicionar às coleções." + }, + "assign": { + "message": "Assign" + }, + "assignToCollections": { + "message": "Assign to collections" + }, + "assignToTheseCollections": { + "message": "Assign to these collections" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "noCollectionsAssigned": { + "message": "No collections have been assigned" + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Items" } } diff --git a/apps/web/src/locales/pt_PT/messages.json b/apps/web/src/locales/pt_PT/messages.json index 0d163c9f19..0c8c809ffc 100644 --- a/apps/web/src/locales/pt_PT/messages.json +++ b/apps/web/src/locales/pt_PT/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "Não se pode adicionar a si próprio a coleções." + }, + "assign": { + "message": "Atribuir" + }, + "assignToCollections": { + "message": "Atribuir às coleções" + }, + "assignToTheseCollections": { + "message": "Atribuir a estas coleções" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Selecione as coleções com as quais os itens serão partilhados. Assim que um item for atualizado numa coleção, será refletido em todas as coleções. Apenas os membros da organização com acesso a estas coleções poderão ver os itens." + }, + "selectCollectionsToAssign": { + "message": "Selecione as coleções a atribuir" + }, + "noCollectionsAssigned": { + "message": "Não foram atribuídas quaisquer coleções" + }, + "successfullyAssignedCollections": { + "message": "Coleções atribuídas com sucesso" + }, + "bulkCollectionAssignmentWarning": { + "message": "Selecionou $TOTAL_COUNT$ itens. Não pode atualizar $READONLY_COUNT$ dos itens porque não tem permissões de edição.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Itens" } } diff --git a/apps/web/src/locales/ro/messages.json b/apps/web/src/locales/ro/messages.json index 1d92eccc00..244395daaa 100644 --- a/apps/web/src/locales/ro/messages.json +++ b/apps/web/src/locales/ro/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "You cannot add yourself to collections." + }, + "assign": { + "message": "Assign" + }, + "assignToCollections": { + "message": "Assign to collections" + }, + "assignToTheseCollections": { + "message": "Assign to these collections" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "noCollectionsAssigned": { + "message": "No collections have been assigned" + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Items" } } diff --git a/apps/web/src/locales/ru/messages.json b/apps/web/src/locales/ru/messages.json index b5b66777f8..b68b045bbb 100644 --- a/apps/web/src/locales/ru/messages.json +++ b/apps/web/src/locales/ru/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "Нельзя добавить самого себя в коллекции." + }, + "assign": { + "message": "Назначить" + }, + "assignToCollections": { + "message": "Назначить коллекциям" + }, + "assignToTheseCollections": { + "message": "Назначить этим коллекциям" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Выберите коллекции, в которые будут переданы элементы. Если элемент обновлен в одной коллекции, это изменение будет отражено во всех коллекциях. Только члены организации, имеющие доступ к этим коллекциям, смогут видеть элементы." + }, + "selectCollectionsToAssign": { + "message": "Выбрать коллекции для назначения" + }, + "noCollectionsAssigned": { + "message": "Коллекции не назначены" + }, + "successfullyAssignedCollections": { + "message": "Коллекции успешно назначены" + }, + "bulkCollectionAssignmentWarning": { + "message": "Вы выбрали $TOTAL_COUNT$ элемента(-ов). Вы не можете обновить $READONLY_COUNT$ элемента(-ов), поскольку у вас нет прав на редактирование.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Элементы" } } diff --git a/apps/web/src/locales/si/messages.json b/apps/web/src/locales/si/messages.json index 92b3d55665..ce4df3eeda 100644 --- a/apps/web/src/locales/si/messages.json +++ b/apps/web/src/locales/si/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "You cannot add yourself to collections." + }, + "assign": { + "message": "Assign" + }, + "assignToCollections": { + "message": "Assign to collections" + }, + "assignToTheseCollections": { + "message": "Assign to these collections" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "noCollectionsAssigned": { + "message": "No collections have been assigned" + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Items" } } diff --git a/apps/web/src/locales/sk/messages.json b/apps/web/src/locales/sk/messages.json index ef8c8ad20c..74f68fc0cc 100644 --- a/apps/web/src/locales/sk/messages.json +++ b/apps/web/src/locales/sk/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "Seba nemôžete pridať do zbierok." + }, + "assign": { + "message": "Prideliť" + }, + "assignToCollections": { + "message": "Prideliť k zbierkam" + }, + "assignToTheseCollections": { + "message": "Prideliť k týmto zbierkam" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Vyberte zbierky s ktorými budú položky zdieľané. Zmeny položky v jednej zbierke sa prejavia vo všetkých zbierkach. Iba členovia organizácie s prístupom k týmto zbierkam budu položky vidieť." + }, + "selectCollectionsToAssign": { + "message": "Vyberte zbierky na pridelenie" + }, + "noCollectionsAssigned": { + "message": "Neboli pridelené žiadne zbierky" + }, + "successfullyAssignedCollections": { + "message": "Úspešne pridelené zbierky" + }, + "bulkCollectionAssignmentWarning": { + "message": "Vybrali ste $TOTAL_COUNT$ položiek. Nemôžete aktualizovať $READONLY_COUNT$ položiek, pretože nemáte oprávnenie na úpravu.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Položky" } } diff --git a/apps/web/src/locales/sl/messages.json b/apps/web/src/locales/sl/messages.json index 0dc844a469..4982120003 100644 --- a/apps/web/src/locales/sl/messages.json +++ b/apps/web/src/locales/sl/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "You cannot add yourself to collections." + }, + "assign": { + "message": "Assign" + }, + "assignToCollections": { + "message": "Assign to collections" + }, + "assignToTheseCollections": { + "message": "Assign to these collections" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "noCollectionsAssigned": { + "message": "No collections have been assigned" + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Items" } } diff --git a/apps/web/src/locales/sr/messages.json b/apps/web/src/locales/sr/messages.json index 0d57790be7..f5fe041433 100644 --- a/apps/web/src/locales/sr/messages.json +++ b/apps/web/src/locales/sr/messages.json @@ -579,7 +579,7 @@ "message": "Приступ" }, "accessLevel": { - "message": "Access level" + "message": "Ниво приступа" }, "loggedOut": { "message": "Одјављено" @@ -7601,9 +7601,46 @@ "message": "Портал провајдера" }, "restrictedGroupAccess": { - "message": "You cannot add yourself to groups." + "message": "Не можете да се додате у групе." }, "restrictedCollectionAccess": { - "message": "You cannot add yourself to collections." + "message": "Не можете да се додате у колекције." + }, + "assign": { + "message": "Додели" + }, + "assignToCollections": { + "message": "Додели колекцијама" + }, + "assignToTheseCollections": { + "message": "Додели овим колекцијама" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Изаберите колекције са којима ће се ставке делити. Када се ставка ажурира у једној колекцији, она ће се одразити на све колекције. Само чланови организације са приступом овим колекцијама ће моћи да виде ставке." + }, + "selectCollectionsToAssign": { + "message": "Изаберите колекције за доделу" + }, + "noCollectionsAssigned": { + "message": "Није додељена ниједна колекција" + }, + "successfullyAssignedCollections": { + "message": "Успешно додељене колекције" + }, + "bulkCollectionAssignmentWarning": { + "message": "Одабрали сте $TOTAL_COUNT$ ставки. Не можете да ажурирате $READONLY_COUNT$ од ставки јер немате дозволе за уређивање.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Ставке" } } diff --git a/apps/web/src/locales/sr_CS/messages.json b/apps/web/src/locales/sr_CS/messages.json index ef52db983e..4553f85c33 100644 --- a/apps/web/src/locales/sr_CS/messages.json +++ b/apps/web/src/locales/sr_CS/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "You cannot add yourself to collections." + }, + "assign": { + "message": "Assign" + }, + "assignToCollections": { + "message": "Assign to collections" + }, + "assignToTheseCollections": { + "message": "Assign to these collections" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "noCollectionsAssigned": { + "message": "No collections have been assigned" + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Items" } } diff --git a/apps/web/src/locales/sv/messages.json b/apps/web/src/locales/sv/messages.json index 04ca471923..8765baaa3b 100644 --- a/apps/web/src/locales/sv/messages.json +++ b/apps/web/src/locales/sv/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "Du kan inte lägga till dig själv i samlingar." + }, + "assign": { + "message": "Tilldela" + }, + "assignToCollections": { + "message": "Tilldela till samlingar" + }, + "assignToTheseCollections": { + "message": "Assign to these collections" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + }, + "selectCollectionsToAssign": { + "message": "Välj samlingar att tilldela" + }, + "noCollectionsAssigned": { + "message": "Inga samlingar har tilldelats" + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Objekt" } } diff --git a/apps/web/src/locales/te/messages.json b/apps/web/src/locales/te/messages.json index 0562f4bf84..ad51cf605f 100644 --- a/apps/web/src/locales/te/messages.json +++ b/apps/web/src/locales/te/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "You cannot add yourself to collections." + }, + "assign": { + "message": "Assign" + }, + "assignToCollections": { + "message": "Assign to collections" + }, + "assignToTheseCollections": { + "message": "Assign to these collections" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "noCollectionsAssigned": { + "message": "No collections have been assigned" + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Items" } } diff --git a/apps/web/src/locales/th/messages.json b/apps/web/src/locales/th/messages.json index e68a55564e..69ee59eb26 100644 --- a/apps/web/src/locales/th/messages.json +++ b/apps/web/src/locales/th/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "You cannot add yourself to collections." + }, + "assign": { + "message": "Assign" + }, + "assignToCollections": { + "message": "Assign to collections" + }, + "assignToTheseCollections": { + "message": "Assign to these collections" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "noCollectionsAssigned": { + "message": "No collections have been assigned" + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Items" } } diff --git a/apps/web/src/locales/tr/messages.json b/apps/web/src/locales/tr/messages.json index 46458c966a..fb626b82f5 100644 --- a/apps/web/src/locales/tr/messages.json +++ b/apps/web/src/locales/tr/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "Kendinizi koleksiyonlara ekleyemezsiniz." + }, + "assign": { + "message": "Ata" + }, + "assignToCollections": { + "message": "Koleksiyonlara ata" + }, + "assignToTheseCollections": { + "message": "Bu koleksiyonlara ata" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Öğelerin paylaşılacağı koleksiyonları seçin. Bir koleksiyondaki bir öğe güncellendiğinde tüm koleksiyonlara yansıtılacaktır. Öğeleri yalnızca bu koleksiyonlara erişimi olan kuruluş üyeleri görebilir." + }, + "selectCollectionsToAssign": { + "message": "Atanacak koleksiyonları seçin" + }, + "noCollectionsAssigned": { + "message": "Hiçbir koleksiyon atanmadı" + }, + "successfullyAssignedCollections": { + "message": "Başarıyla atanan koleksiyonlar" + }, + "bulkCollectionAssignmentWarning": { + "message": "$TOTAL_COUNT$ öğe seçtiniz. Düzenleme izniniz olmadığı için $READONLY_COUNT$ öğeyi güncelleyemezsiniz.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Ögeler" } } diff --git a/apps/web/src/locales/uk/messages.json b/apps/web/src/locales/uk/messages.json index 502a76066e..e2d2e6163c 100644 --- a/apps/web/src/locales/uk/messages.json +++ b/apps/web/src/locales/uk/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "Ви не можете додати себе до збірок." + }, + "assign": { + "message": "Призначити" + }, + "assignToCollections": { + "message": "Призначити до збірок" + }, + "assignToTheseCollections": { + "message": "Призначити до цих збірок" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Оберіть збірки, в яких поширюватимуться записи. Після оновлення запису в одній збірці зміни буде відображено у всіх збірках. Лише учасники організації з доступом до цих збірок зможуть переглядати записи." + }, + "selectCollectionsToAssign": { + "message": "Оберіть збірки для призначення" + }, + "noCollectionsAssigned": { + "message": "Не призначено жодної збірки" + }, + "successfullyAssignedCollections": { + "message": "Збірки успішно призначено" + }, + "bulkCollectionAssignmentWarning": { + "message": "Ви вибрали $TOTAL_COUNT$ записів. Ви не можете оновити $READONLY_COUNT$ записів, тому що у вас немає доступу на редагування.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Записи" } } diff --git a/apps/web/src/locales/vi/messages.json b/apps/web/src/locales/vi/messages.json index d4202225a4..316704ffdf 100644 --- a/apps/web/src/locales/vi/messages.json +++ b/apps/web/src/locales/vi/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "You cannot add yourself to collections." + }, + "assign": { + "message": "Assign" + }, + "assignToCollections": { + "message": "Assign to collections" + }, + "assignToTheseCollections": { + "message": "Assign to these collections" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "noCollectionsAssigned": { + "message": "No collections have been assigned" + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Items" } } diff --git a/apps/web/src/locales/zh_CN/messages.json b/apps/web/src/locales/zh_CN/messages.json index 7e52bc1f44..f2d8619b32 100644 --- a/apps/web/src/locales/zh_CN/messages.json +++ b/apps/web/src/locales/zh_CN/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "您不能将自己添加到群组。" + }, + "assign": { + "message": "Assign" + }, + "assignToCollections": { + "message": "Assign to collections" + }, + "assignToTheseCollections": { + "message": "Assign to these collections" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "noCollectionsAssigned": { + "message": "No collections have been assigned" + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Items" } } diff --git a/apps/web/src/locales/zh_TW/messages.json b/apps/web/src/locales/zh_TW/messages.json index c90de26bd2..0fa0838f2d 100644 --- a/apps/web/src/locales/zh_TW/messages.json +++ b/apps/web/src/locales/zh_TW/messages.json @@ -7605,5 +7605,42 @@ }, "restrictedCollectionAccess": { "message": "You cannot add yourself to collections." + }, + "assign": { + "message": "Assign" + }, + "assignToCollections": { + "message": "Assign to collections" + }, + "assignToTheseCollections": { + "message": "Assign to these collections" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "noCollectionsAssigned": { + "message": "No collections have been assigned" + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2", + "example": "3" + } + } + }, + "items": { + "message": "Items" } } From 87c933acc8d6a4b9b11b4be29053903bf43c2559 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 25 Mar 2024 10:24:34 +0000 Subject: [PATCH 009/351] Autosync the updated translations (#8461) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/browser/src/_locales/da/messages.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index 750b2e1170..ab4c4e5b6e 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." From 9243bb92c710367b4d0302464d6c7deefab54327 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 25 Mar 2024 10:24:46 +0000 Subject: [PATCH 010/351] Autosync the updated translations (#8460) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/desktop/src/locales/af/messages.json | 9 +++++++ apps/desktop/src/locales/ar/messages.json | 9 +++++++ apps/desktop/src/locales/az/messages.json | 9 +++++++ apps/desktop/src/locales/be/messages.json | 9 +++++++ apps/desktop/src/locales/bg/messages.json | 9 +++++++ apps/desktop/src/locales/bn/messages.json | 9 +++++++ apps/desktop/src/locales/bs/messages.json | 9 +++++++ apps/desktop/src/locales/ca/messages.json | 13 ++++++++-- apps/desktop/src/locales/cs/messages.json | 9 +++++++ apps/desktop/src/locales/cy/messages.json | 9 +++++++ apps/desktop/src/locales/da/messages.json | 23 ++++++++++++------ apps/desktop/src/locales/de/messages.json | 9 +++++++ apps/desktop/src/locales/el/messages.json | 25 +++++++++++++------- apps/desktop/src/locales/en_GB/messages.json | 9 +++++++ apps/desktop/src/locales/en_IN/messages.json | 9 +++++++ apps/desktop/src/locales/eo/messages.json | 9 +++++++ apps/desktop/src/locales/es/messages.json | 9 +++++++ apps/desktop/src/locales/et/messages.json | 9 +++++++ apps/desktop/src/locales/eu/messages.json | 9 +++++++ apps/desktop/src/locales/fa/messages.json | 9 +++++++ apps/desktop/src/locales/fi/messages.json | 9 +++++++ apps/desktop/src/locales/fil/messages.json | 9 +++++++ apps/desktop/src/locales/fr/messages.json | 11 ++++++++- apps/desktop/src/locales/gl/messages.json | 9 +++++++ apps/desktop/src/locales/he/messages.json | 21 +++++++++++----- apps/desktop/src/locales/hi/messages.json | 9 +++++++ apps/desktop/src/locales/hr/messages.json | 9 +++++++ apps/desktop/src/locales/hu/messages.json | 9 +++++++ apps/desktop/src/locales/id/messages.json | 9 +++++++ apps/desktop/src/locales/it/messages.json | 9 +++++++ apps/desktop/src/locales/ja/messages.json | 9 +++++++ apps/desktop/src/locales/ka/messages.json | 9 +++++++ apps/desktop/src/locales/km/messages.json | 9 +++++++ apps/desktop/src/locales/kn/messages.json | 9 +++++++ apps/desktop/src/locales/ko/messages.json | 9 +++++++ apps/desktop/src/locales/lt/messages.json | 9 +++++++ apps/desktop/src/locales/lv/messages.json | 13 ++++++++-- apps/desktop/src/locales/me/messages.json | 9 +++++++ apps/desktop/src/locales/ml/messages.json | 9 +++++++ apps/desktop/src/locales/mr/messages.json | 9 +++++++ apps/desktop/src/locales/my/messages.json | 9 +++++++ apps/desktop/src/locales/nb/messages.json | 9 +++++++ apps/desktop/src/locales/ne/messages.json | 9 +++++++ apps/desktop/src/locales/nl/messages.json | 9 +++++++ apps/desktop/src/locales/nn/messages.json | 9 +++++++ apps/desktop/src/locales/or/messages.json | 9 +++++++ apps/desktop/src/locales/pl/messages.json | 9 +++++++ apps/desktop/src/locales/pt_BR/messages.json | 9 +++++++ apps/desktop/src/locales/pt_PT/messages.json | 9 +++++++ apps/desktop/src/locales/ro/messages.json | 9 +++++++ apps/desktop/src/locales/ru/messages.json | 9 +++++++ apps/desktop/src/locales/si/messages.json | 9 +++++++ apps/desktop/src/locales/sk/messages.json | 9 +++++++ apps/desktop/src/locales/sl/messages.json | 9 +++++++ apps/desktop/src/locales/sr/messages.json | 13 ++++++++-- apps/desktop/src/locales/sv/messages.json | 9 +++++++ apps/desktop/src/locales/te/messages.json | 9 +++++++ apps/desktop/src/locales/th/messages.json | 9 +++++++ apps/desktop/src/locales/tr/messages.json | 9 +++++++ apps/desktop/src/locales/uk/messages.json | 9 +++++++ apps/desktop/src/locales/vi/messages.json | 9 +++++++ apps/desktop/src/locales/zh_CN/messages.json | 9 +++++++ apps/desktop/src/locales/zh_TW/messages.json | 9 +++++++ 63 files changed, 595 insertions(+), 28 deletions(-) diff --git a/apps/desktop/src/locales/af/messages.json b/apps/desktop/src/locales/af/messages.json index 1235230dd3..b1deba9dd9 100644 --- a/apps/desktop/src/locales/af/messages.json +++ b/apps/desktop/src/locales/af/messages.json @@ -2687,5 +2687,14 @@ "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" } } diff --git a/apps/desktop/src/locales/ar/messages.json b/apps/desktop/src/locales/ar/messages.json index d7c405159f..b95501bbfd 100644 --- a/apps/desktop/src/locales/ar/messages.json +++ b/apps/desktop/src/locales/ar/messages.json @@ -2687,5 +2687,14 @@ "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" } } diff --git a/apps/desktop/src/locales/az/messages.json b/apps/desktop/src/locales/az/messages.json index 18f150745b..a18d752620 100644 --- a/apps/desktop/src/locales/az/messages.json +++ b/apps/desktop/src/locales/az/messages.json @@ -2687,5 +2687,14 @@ "commonImportFormats": { "message": "Ortaq formatlar", "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" } } diff --git a/apps/desktop/src/locales/be/messages.json b/apps/desktop/src/locales/be/messages.json index 675f9cd0f2..0529c407a4 100644 --- a/apps/desktop/src/locales/be/messages.json +++ b/apps/desktop/src/locales/be/messages.json @@ -2687,5 +2687,14 @@ "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" } } diff --git a/apps/desktop/src/locales/bg/messages.json b/apps/desktop/src/locales/bg/messages.json index 030dc45683..3217f167ed 100644 --- a/apps/desktop/src/locales/bg/messages.json +++ b/apps/desktop/src/locales/bg/messages.json @@ -2687,5 +2687,14 @@ "commonImportFormats": { "message": "Често използвани формати", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Отстраняване на проблеми" + }, + "disableHardwareAccelerationRestart": { + "message": "Изключете хардуерното ускорение и рестартирайте" + }, + "enableHardwareAccelerationRestart": { + "message": "Включете хардуерното ускорение и рестартирайте" } } diff --git a/apps/desktop/src/locales/bn/messages.json b/apps/desktop/src/locales/bn/messages.json index debefc0e65..9599fc6827 100644 --- a/apps/desktop/src/locales/bn/messages.json +++ b/apps/desktop/src/locales/bn/messages.json @@ -2687,5 +2687,14 @@ "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" } } diff --git a/apps/desktop/src/locales/bs/messages.json b/apps/desktop/src/locales/bs/messages.json index 4c6f2b8744..6e6ca99bac 100644 --- a/apps/desktop/src/locales/bs/messages.json +++ b/apps/desktop/src/locales/bs/messages.json @@ -2687,5 +2687,14 @@ "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" } } diff --git a/apps/desktop/src/locales/ca/messages.json b/apps/desktop/src/locales/ca/messages.json index f4ef70c2eb..3cdedd0274 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,14 @@ "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" } } diff --git a/apps/desktop/src/locales/cs/messages.json b/apps/desktop/src/locales/cs/messages.json index 534438aff2..efa1dccdc1 100644 --- a/apps/desktop/src/locales/cs/messages.json +++ b/apps/desktop/src/locales/cs/messages.json @@ -2687,5 +2687,14 @@ "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" } } diff --git a/apps/desktop/src/locales/cy/messages.json b/apps/desktop/src/locales/cy/messages.json index e266886a90..65ce77b340 100644 --- a/apps/desktop/src/locales/cy/messages.json +++ b/apps/desktop/src/locales/cy/messages.json @@ -2687,5 +2687,14 @@ "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" } } diff --git a/apps/desktop/src/locales/da/messages.json b/apps/desktop/src/locales/da/messages.json index 795c1cc272..0da6c705ca 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,14 @@ "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" } } diff --git a/apps/desktop/src/locales/de/messages.json b/apps/desktop/src/locales/de/messages.json index 4132caa7ad..bacf158023 100644 --- a/apps/desktop/src/locales/de/messages.json +++ b/apps/desktop/src/locales/de/messages.json @@ -2687,5 +2687,14 @@ "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" } } diff --git a/apps/desktop/src/locales/el/messages.json b/apps/desktop/src/locales/el/messages.json index 28e2f9f870..6d6fcaae45 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 με την υπόδειξη του κύριου κωδικού." @@ -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,11 +1546,11 @@ "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": { @@ -2687,5 +2687,14 @@ "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" } } diff --git a/apps/desktop/src/locales/en_GB/messages.json b/apps/desktop/src/locales/en_GB/messages.json index 25f6b61758..9b68b6de49 100644 --- a/apps/desktop/src/locales/en_GB/messages.json +++ b/apps/desktop/src/locales/en_GB/messages.json @@ -2687,5 +2687,14 @@ "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" } } diff --git a/apps/desktop/src/locales/en_IN/messages.json b/apps/desktop/src/locales/en_IN/messages.json index 680586cd71..3ffc46eba1 100644 --- a/apps/desktop/src/locales/en_IN/messages.json +++ b/apps/desktop/src/locales/en_IN/messages.json @@ -2687,5 +2687,14 @@ "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" } } diff --git a/apps/desktop/src/locales/eo/messages.json b/apps/desktop/src/locales/eo/messages.json index 9327dceae4..dcffc08ea4 100644 --- a/apps/desktop/src/locales/eo/messages.json +++ b/apps/desktop/src/locales/eo/messages.json @@ -2687,5 +2687,14 @@ "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" } } diff --git a/apps/desktop/src/locales/es/messages.json b/apps/desktop/src/locales/es/messages.json index f30239ad75..29c345d235 100644 --- a/apps/desktop/src/locales/es/messages.json +++ b/apps/desktop/src/locales/es/messages.json @@ -2687,5 +2687,14 @@ "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" } } diff --git a/apps/desktop/src/locales/et/messages.json b/apps/desktop/src/locales/et/messages.json index c2780c3c97..663cd873e5 100644 --- a/apps/desktop/src/locales/et/messages.json +++ b/apps/desktop/src/locales/et/messages.json @@ -2687,5 +2687,14 @@ "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" } } diff --git a/apps/desktop/src/locales/eu/messages.json b/apps/desktop/src/locales/eu/messages.json index b3426a5146..8970af1350 100644 --- a/apps/desktop/src/locales/eu/messages.json +++ b/apps/desktop/src/locales/eu/messages.json @@ -2687,5 +2687,14 @@ "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" } } diff --git a/apps/desktop/src/locales/fa/messages.json b/apps/desktop/src/locales/fa/messages.json index 5e7b26fbb4..a8a5758a14 100644 --- a/apps/desktop/src/locales/fa/messages.json +++ b/apps/desktop/src/locales/fa/messages.json @@ -2687,5 +2687,14 @@ "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" } } diff --git a/apps/desktop/src/locales/fi/messages.json b/apps/desktop/src/locales/fi/messages.json index b04b859d35..5c069578fa 100644 --- a/apps/desktop/src/locales/fi/messages.json +++ b/apps/desktop/src/locales/fi/messages.json @@ -2687,5 +2687,14 @@ "commonImportFormats": { "message": "Yleiset muodot", "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" } } diff --git a/apps/desktop/src/locales/fil/messages.json b/apps/desktop/src/locales/fil/messages.json index 41cd3e9bce..6e6d0abaa6 100644 --- a/apps/desktop/src/locales/fil/messages.json +++ b/apps/desktop/src/locales/fil/messages.json @@ -2687,5 +2687,14 @@ "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" } } diff --git a/apps/desktop/src/locales/fr/messages.json b/apps/desktop/src/locales/fr/messages.json index edecc8a4d6..90dc4b0f77 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,14 @@ "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" } } diff --git a/apps/desktop/src/locales/gl/messages.json b/apps/desktop/src/locales/gl/messages.json index f5f85b040c..11694b8c9c 100644 --- a/apps/desktop/src/locales/gl/messages.json +++ b/apps/desktop/src/locales/gl/messages.json @@ -2687,5 +2687,14 @@ "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" } } diff --git a/apps/desktop/src/locales/he/messages.json b/apps/desktop/src/locales/he/messages.json index 607c2d81d3..3a9517e8d3 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,14 @@ "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" } } diff --git a/apps/desktop/src/locales/hi/messages.json b/apps/desktop/src/locales/hi/messages.json index 4b5634baf1..86f17c1c83 100644 --- a/apps/desktop/src/locales/hi/messages.json +++ b/apps/desktop/src/locales/hi/messages.json @@ -2687,5 +2687,14 @@ "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" } } diff --git a/apps/desktop/src/locales/hr/messages.json b/apps/desktop/src/locales/hr/messages.json index 9f68d70a87..5af3644eea 100644 --- a/apps/desktop/src/locales/hr/messages.json +++ b/apps/desktop/src/locales/hr/messages.json @@ -2687,5 +2687,14 @@ "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" } } diff --git a/apps/desktop/src/locales/hu/messages.json b/apps/desktop/src/locales/hu/messages.json index f7e4052c17..0b61e5f675 100644 --- a/apps/desktop/src/locales/hu/messages.json +++ b/apps/desktop/src/locales/hu/messages.json @@ -2687,5 +2687,14 @@ "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" } } diff --git a/apps/desktop/src/locales/id/messages.json b/apps/desktop/src/locales/id/messages.json index 10a01a385c..5290fd11de 100644 --- a/apps/desktop/src/locales/id/messages.json +++ b/apps/desktop/src/locales/id/messages.json @@ -2687,5 +2687,14 @@ "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" } } diff --git a/apps/desktop/src/locales/it/messages.json b/apps/desktop/src/locales/it/messages.json index 5c1c750c85..2736a4a46a 100644 --- a/apps/desktop/src/locales/it/messages.json +++ b/apps/desktop/src/locales/it/messages.json @@ -2687,5 +2687,14 @@ "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" } } diff --git a/apps/desktop/src/locales/ja/messages.json b/apps/desktop/src/locales/ja/messages.json index 1a1a0e148b..a4b213a5fd 100644 --- a/apps/desktop/src/locales/ja/messages.json +++ b/apps/desktop/src/locales/ja/messages.json @@ -2687,5 +2687,14 @@ "commonImportFormats": { "message": "一般的な形式", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "トラブルシューティング" + }, + "disableHardwareAccelerationRestart": { + "message": "ハードウェアアクセラレーションを無効にして再起動する" + }, + "enableHardwareAccelerationRestart": { + "message": "ハードウェアアクセラレーションを有効にして再起動する" } } diff --git a/apps/desktop/src/locales/ka/messages.json b/apps/desktop/src/locales/ka/messages.json index f5f85b040c..11694b8c9c 100644 --- a/apps/desktop/src/locales/ka/messages.json +++ b/apps/desktop/src/locales/ka/messages.json @@ -2687,5 +2687,14 @@ "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" } } diff --git a/apps/desktop/src/locales/km/messages.json b/apps/desktop/src/locales/km/messages.json index f5f85b040c..11694b8c9c 100644 --- a/apps/desktop/src/locales/km/messages.json +++ b/apps/desktop/src/locales/km/messages.json @@ -2687,5 +2687,14 @@ "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" } } diff --git a/apps/desktop/src/locales/kn/messages.json b/apps/desktop/src/locales/kn/messages.json index 1707f85a9b..5f7ad11dcd 100644 --- a/apps/desktop/src/locales/kn/messages.json +++ b/apps/desktop/src/locales/kn/messages.json @@ -2687,5 +2687,14 @@ "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" } } diff --git a/apps/desktop/src/locales/ko/messages.json b/apps/desktop/src/locales/ko/messages.json index cd22b12386..c8e6719811 100644 --- a/apps/desktop/src/locales/ko/messages.json +++ b/apps/desktop/src/locales/ko/messages.json @@ -2687,5 +2687,14 @@ "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" } } diff --git a/apps/desktop/src/locales/lt/messages.json b/apps/desktop/src/locales/lt/messages.json index 6177164408..00186771fd 100644 --- a/apps/desktop/src/locales/lt/messages.json +++ b/apps/desktop/src/locales/lt/messages.json @@ -2687,5 +2687,14 @@ "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" } } diff --git a/apps/desktop/src/locales/lv/messages.json b/apps/desktop/src/locales/lv/messages.json index 502f926c78..f82b762d6c 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,14 @@ "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" } } diff --git a/apps/desktop/src/locales/me/messages.json b/apps/desktop/src/locales/me/messages.json index 4aa0c6598a..533ec2ebba 100644 --- a/apps/desktop/src/locales/me/messages.json +++ b/apps/desktop/src/locales/me/messages.json @@ -2687,5 +2687,14 @@ "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" } } diff --git a/apps/desktop/src/locales/ml/messages.json b/apps/desktop/src/locales/ml/messages.json index c025e77c41..cba807216a 100644 --- a/apps/desktop/src/locales/ml/messages.json +++ b/apps/desktop/src/locales/ml/messages.json @@ -2687,5 +2687,14 @@ "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" } } diff --git a/apps/desktop/src/locales/mr/messages.json b/apps/desktop/src/locales/mr/messages.json index f5f85b040c..11694b8c9c 100644 --- a/apps/desktop/src/locales/mr/messages.json +++ b/apps/desktop/src/locales/mr/messages.json @@ -2687,5 +2687,14 @@ "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" } } diff --git a/apps/desktop/src/locales/my/messages.json b/apps/desktop/src/locales/my/messages.json index dc9180ddbf..05e3e7703e 100644 --- a/apps/desktop/src/locales/my/messages.json +++ b/apps/desktop/src/locales/my/messages.json @@ -2687,5 +2687,14 @@ "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" } } diff --git a/apps/desktop/src/locales/nb/messages.json b/apps/desktop/src/locales/nb/messages.json index cda18630e6..a20a6a6267 100644 --- a/apps/desktop/src/locales/nb/messages.json +++ b/apps/desktop/src/locales/nb/messages.json @@ -2687,5 +2687,14 @@ "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" } } diff --git a/apps/desktop/src/locales/ne/messages.json b/apps/desktop/src/locales/ne/messages.json index 4eed6500a6..40e3d11d88 100644 --- a/apps/desktop/src/locales/ne/messages.json +++ b/apps/desktop/src/locales/ne/messages.json @@ -2687,5 +2687,14 @@ "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" } } diff --git a/apps/desktop/src/locales/nl/messages.json b/apps/desktop/src/locales/nl/messages.json index 188a4ab3cb..0ae432ef45 100644 --- a/apps/desktop/src/locales/nl/messages.json +++ b/apps/desktop/src/locales/nl/messages.json @@ -2687,5 +2687,14 @@ "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" } } diff --git a/apps/desktop/src/locales/nn/messages.json b/apps/desktop/src/locales/nn/messages.json index d088661002..d3119f6a10 100644 --- a/apps/desktop/src/locales/nn/messages.json +++ b/apps/desktop/src/locales/nn/messages.json @@ -2687,5 +2687,14 @@ "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" } } diff --git a/apps/desktop/src/locales/or/messages.json b/apps/desktop/src/locales/or/messages.json index b67bed747c..20b704ef5e 100644 --- a/apps/desktop/src/locales/or/messages.json +++ b/apps/desktop/src/locales/or/messages.json @@ -2687,5 +2687,14 @@ "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" } } diff --git a/apps/desktop/src/locales/pl/messages.json b/apps/desktop/src/locales/pl/messages.json index 3a8d6dd4ef..60444211be 100644 --- a/apps/desktop/src/locales/pl/messages.json +++ b/apps/desktop/src/locales/pl/messages.json @@ -2687,5 +2687,14 @@ "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" } } diff --git a/apps/desktop/src/locales/pt_BR/messages.json b/apps/desktop/src/locales/pt_BR/messages.json index 8c29fc80d7..4fb7c74889 100644 --- a/apps/desktop/src/locales/pt_BR/messages.json +++ b/apps/desktop/src/locales/pt_BR/messages.json @@ -2687,5 +2687,14 @@ "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" } } diff --git a/apps/desktop/src/locales/pt_PT/messages.json b/apps/desktop/src/locales/pt_PT/messages.json index 26635769ab..7c325bc5f9 100644 --- a/apps/desktop/src/locales/pt_PT/messages.json +++ b/apps/desktop/src/locales/pt_PT/messages.json @@ -2687,5 +2687,14 @@ "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" } } diff --git a/apps/desktop/src/locales/ro/messages.json b/apps/desktop/src/locales/ro/messages.json index b6c0016e67..082a4f590b 100644 --- a/apps/desktop/src/locales/ro/messages.json +++ b/apps/desktop/src/locales/ro/messages.json @@ -2687,5 +2687,14 @@ "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" } } diff --git a/apps/desktop/src/locales/ru/messages.json b/apps/desktop/src/locales/ru/messages.json index 817c707884..5eaad37d0f 100644 --- a/apps/desktop/src/locales/ru/messages.json +++ b/apps/desktop/src/locales/ru/messages.json @@ -2687,5 +2687,14 @@ "commonImportFormats": { "message": "Основные форматы", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Устранение проблем" + }, + "disableHardwareAccelerationRestart": { + "message": "Отключить аппаратное ускорение и перезапустить" + }, + "enableHardwareAccelerationRestart": { + "message": "Включить аппаратное ускорение и перезапустить" } } diff --git a/apps/desktop/src/locales/si/messages.json b/apps/desktop/src/locales/si/messages.json index 92326f4312..c72b2dd0e6 100644 --- a/apps/desktop/src/locales/si/messages.json +++ b/apps/desktop/src/locales/si/messages.json @@ -2687,5 +2687,14 @@ "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" } } diff --git a/apps/desktop/src/locales/sk/messages.json b/apps/desktop/src/locales/sk/messages.json index 0cfa6a98e0..b8777adeb1 100644 --- a/apps/desktop/src/locales/sk/messages.json +++ b/apps/desktop/src/locales/sk/messages.json @@ -2687,5 +2687,14 @@ "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ť" } } diff --git a/apps/desktop/src/locales/sl/messages.json b/apps/desktop/src/locales/sl/messages.json index afdf7fc63a..fac763dec9 100644 --- a/apps/desktop/src/locales/sl/messages.json +++ b/apps/desktop/src/locales/sl/messages.json @@ -2687,5 +2687,14 @@ "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" } } diff --git a/apps/desktop/src/locales/sr/messages.json b/apps/desktop/src/locales/sr/messages.json index 37335b207c..73469b26b4 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,14 @@ "commonImportFormats": { "message": "Уобичајени формати", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Решавање проблема" + }, + "disableHardwareAccelerationRestart": { + "message": "Онемогућите хардверско убрзање и поново покрените" + }, + "enableHardwareAccelerationRestart": { + "message": "Омогућите хардверско убрзање и поново покрените" } } diff --git a/apps/desktop/src/locales/sv/messages.json b/apps/desktop/src/locales/sv/messages.json index 53fe6b58d2..c4c70123ac 100644 --- a/apps/desktop/src/locales/sv/messages.json +++ b/apps/desktop/src/locales/sv/messages.json @@ -2687,5 +2687,14 @@ "commonImportFormats": { "message": "Vanliga format", "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" } } diff --git a/apps/desktop/src/locales/te/messages.json b/apps/desktop/src/locales/te/messages.json index f5f85b040c..11694b8c9c 100644 --- a/apps/desktop/src/locales/te/messages.json +++ b/apps/desktop/src/locales/te/messages.json @@ -2687,5 +2687,14 @@ "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" } } diff --git a/apps/desktop/src/locales/th/messages.json b/apps/desktop/src/locales/th/messages.json index 29b04ef6f2..a664696d5b 100644 --- a/apps/desktop/src/locales/th/messages.json +++ b/apps/desktop/src/locales/th/messages.json @@ -2687,5 +2687,14 @@ "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" } } diff --git a/apps/desktop/src/locales/tr/messages.json b/apps/desktop/src/locales/tr/messages.json index db28be4e5e..ae3f217aa6 100644 --- a/apps/desktop/src/locales/tr/messages.json +++ b/apps/desktop/src/locales/tr/messages.json @@ -2687,5 +2687,14 @@ "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" } } diff --git a/apps/desktop/src/locales/uk/messages.json b/apps/desktop/src/locales/uk/messages.json index e395c4409f..cc5273b1bd 100644 --- a/apps/desktop/src/locales/uk/messages.json +++ b/apps/desktop/src/locales/uk/messages.json @@ -2687,5 +2687,14 @@ "commonImportFormats": { "message": "Поширені формати", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Усунення проблем" + }, + "disableHardwareAccelerationRestart": { + "message": "Вимкнути апаратне прискорення і перезапустити" + }, + "enableHardwareAccelerationRestart": { + "message": "Увімкнути апаратне прискорення і перезапустити" } } diff --git a/apps/desktop/src/locales/vi/messages.json b/apps/desktop/src/locales/vi/messages.json index b97eef4445..f81cb2778d 100644 --- a/apps/desktop/src/locales/vi/messages.json +++ b/apps/desktop/src/locales/vi/messages.json @@ -2687,5 +2687,14 @@ "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" } } diff --git a/apps/desktop/src/locales/zh_CN/messages.json b/apps/desktop/src/locales/zh_CN/messages.json index 4bf1530019..84ffcecbb8 100644 --- a/apps/desktop/src/locales/zh_CN/messages.json +++ b/apps/desktop/src/locales/zh_CN/messages.json @@ -2687,5 +2687,14 @@ "commonImportFormats": { "message": "通用格式", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "故障排除" + }, + "disableHardwareAccelerationRestart": { + "message": "禁用硬件加速并重启" + }, + "enableHardwareAccelerationRestart": { + "message": "启用硬件加速并重启" } } diff --git a/apps/desktop/src/locales/zh_TW/messages.json b/apps/desktop/src/locales/zh_TW/messages.json index a4d280def7..1659344550 100644 --- a/apps/desktop/src/locales/zh_TW/messages.json +++ b/apps/desktop/src/locales/zh_TW/messages.json @@ -2687,5 +2687,14 @@ "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" } } From bc9a88811694512f8076c39f4ff3be0b01d30719 Mon Sep 17 00:00:00 2001 From: Bitwarden DevOps <106330231+bitwarden-devops-bot@users.noreply.github.com> Date: Mon, 25 Mar 2024 10:18:17 -0400 Subject: [PATCH 011/351] Bumped browser,cli,desktop,web version to 2024.3.1 (#8467) --- apps/browser/package.json | 2 +- apps/browser/src/manifest.json | 2 +- apps/browser/src/manifest.v3.json | 2 +- apps/cli/package.json | 2 +- apps/desktop/package.json | 2 +- apps/desktop/src/package-lock.json | 4 ++-- apps/desktop/src/package.json | 2 +- apps/web/package.json | 2 +- package-lock.json | 10 +++++----- 9 files changed, 14 insertions(+), 14 deletions(-) diff --git a/apps/browser/package.json b/apps/browser/package.json index b03469bbb7..d06eadf58d 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/browser", - "version": "2024.3.0", + "version": "2024.3.1", "scripts": { "build": "webpack", "build:mv3": "cross-env MANIFEST_VERSION=3 webpack", diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index 7bdf92a597..271b2c76a2 100644 --- a/apps/browser/src/manifest.json +++ b/apps/browser/src/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "__MSG_extName__", "short_name": "__MSG_appName__", - "version": "2024.3.0", + "version": "2024.3.1", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index 86510347d4..e7b0c0cd1e 100644 --- a/apps/browser/src/manifest.v3.json +++ b/apps/browser/src/manifest.v3.json @@ -3,7 +3,7 @@ "minimum_chrome_version": "102.0", "name": "__MSG_extName__", "short_name": "__MSG_appName__", - "version": "2024.3.0", + "version": "2024.3.1", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/cli/package.json b/apps/cli/package.json index 2873be4242..2de11df4e6 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", diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 6b01918d6b..52dd0fafdb 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.3.2", "keywords": [ "bitwarden", "password", diff --git a/apps/desktop/src/package-lock.json b/apps/desktop/src/package-lock.json index 2862977ca2..b379e13d57 100644 --- a/apps/desktop/src/package-lock.json +++ b/apps/desktop/src/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bitwarden/desktop", - "version": "2024.3.1", + "version": "2024.3.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bitwarden/desktop", - "version": "2024.3.1", + "version": "2024.3.2", "license": "GPL-3.0", "dependencies": { "@bitwarden/desktop-native": "file:../desktop_native" diff --git a/apps/desktop/src/package.json b/apps/desktop/src/package.json index 4c748c4a05..cfc0b9b4e2 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.3.2", "author": "Bitwarden Inc. (https://bitwarden.com)", "homepage": "https://bitwarden.com", "license": "GPL-3.0", diff --git a/apps/web/package.json b/apps/web/package.json index 3fad4b14ae..5b049dcb9d 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.3.1", "scripts": { "build:oss": "webpack", "build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js", diff --git a/package-lock.json b/package-lock.json index b899863568..43f1987c88 100644 --- a/package-lock.json +++ b/package-lock.json @@ -187,17 +187,17 @@ "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.3.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", @@ -233,7 +233,7 @@ }, "apps/desktop": { "name": "@bitwarden/desktop", - "version": "2024.3.1", + "version": "2024.3.2", "hasInstallScript": true, "license": "GPL-3.0" }, @@ -263,7 +263,7 @@ }, "apps/web": { "name": "@bitwarden/web-vault", - "version": "2024.3.0" + "version": "2024.3.1" }, "libs/admin-console": { "name": "@bitwarden/admin-console", From 8639f494f31493127df46820e15635a48ef1c50d Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Mon, 25 Mar 2024 15:22:04 +0100 Subject: [PATCH 012/351] [PM-7048] Disable relaunch on MAS (#8466) --- apps/desktop/src/main/menu/menu.help.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/main/menu/menu.help.ts b/apps/desktop/src/main/menu/menu.help.ts index 46328e2089..7cc0fc2681 100644 --- a/apps/desktop/src/main/menu/menu.help.ts +++ b/apps/desktop/src/main/menu/menu.help.ts @@ -240,7 +240,11 @@ export class HelpMenu implements IMenubarMenu { await this.desktopSettingsService.setHardwareAcceleration( !this.hardwareAccelerationEnabled, ); - app.relaunch(); + // `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(); }, }, From 908d3d165e869b6cf56a462cdfe42c3571dfcad4 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Mon, 25 Mar 2024 09:28:42 -0500 Subject: [PATCH 013/351] [PM-6965] Add `type` Property to `MigrationHelper` (#8411) * Add `type` Property to `MigrationHelper` * Fix Tests * Make `type` parameter required * Fix mockHelper.type * Remove `readonly` from `type` --- .../src/app/platform/web-migration-runner.ts | 2 +- .../migration-builder.service.spec.ts | 1 + .../src/platform/services/migration-runner.ts | 1 + .../migration-builder.spec.ts | 14 +++++++------- .../state-migrations/migration-helper.spec.ts | 12 ++++++++---- .../src/state-migrations/migration-helper.ts | 19 ++++++++++++++++++- .../src/state-migrations/migrator.spec.ts | 2 +- 7 files changed, 37 insertions(+), 14 deletions(-) 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/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/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/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); }); From b81d6a78db1f6d21da7cfcbe778117cb6f5cf043 Mon Sep 17 00:00:00 2001 From: Vince Grassia <593223+vgrassia@users.noreply.github.com> Date: Mon, 25 Mar 2024 15:15:05 +0000 Subject: [PATCH 014/351] Version Bump Workflow - Fix set-final-version-output step (#8468) --- .github/workflows/version-bump.yml | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) 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 From 5bd514745787a8ddf5f998bab22ead29d53e115b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa?= Date: Mon, 25 Mar 2024 17:04:06 +0100 Subject: [PATCH 015/351] Update renovate to group napi packages (#8469) --- .github/renovate.json | 4 ++++ 1 file changed, 4 insertions(+) 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"], From bd0e3dd0aa72c7c51e4ef5d2a7907696d4bbb50d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 25 Mar 2024 17:25:21 +0100 Subject: [PATCH 016/351] [deps] Platform: Update napi (#8470) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- apps/desktop/desktop_native/Cargo.lock | 44 ++++++++++++++++---------- apps/desktop/desktop_native/Cargo.toml | 6 ++-- 2 files changed, 30 insertions(+), 20 deletions(-) diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 89170f4cc3..e99d8b4fc4 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -348,7 +348,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" dependencies = [ - "libloading", + "libloading 0.7.4", ] [[package]] @@ -529,7 +529,7 @@ dependencies = [ "gobject-sys", "libc", "system-deps", - "windows-sys 0.52.0", + "windows-sys", ] [[package]] @@ -669,6 +669,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "libloading" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" +dependencies = [ + "cfg-if", + "windows-targets 0.52.4", +] + [[package]] name = "libsecret" version = "0.5.0" @@ -767,9 +777,9 @@ dependencies = [ [[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", "ctor", @@ -781,29 +791,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 2.0.38", ] [[package]] name = "napi-derive-backend" -version = "1.0.52" +version = "1.0.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20bbc7c69168d06a848f925ec5f0e0997f98e8c8d4f2cc30157f0da51c009e17" +checksum = "f785a8b8d7b83e925f5aa6d2ae3c159d17fe137ac368dc185bef410e7acdaeb4" dependencies = [ "convert_case", "once_cell", @@ -811,16 +821,16 @@ dependencies = [ "quote", "regex", "semver", - "syn 1.0.109", + "syn 2.0.38", ] [[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", + "libloading 0.8.3", ] [[package]] @@ -896,9 +906,9 @@ dependencies = [ [[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" diff --git a/apps/desktop/desktop_native/Cargo.toml b/apps/desktop/desktop_native/Cargo.toml index 48536934ee..cf1082d81c 100644 --- a/apps/desktop/desktop_native/Cargo.toml +++ b/apps/desktop/desktop_native/Cargo.toml @@ -18,8 +18,8 @@ anyhow = "=1.0.80" arboard = { version = "=3.3.0", 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" @@ -28,7 +28,7 @@ thiserror = "=1.0.51" typenum = "=1.17.0" [build-dependencies] -napi-build = "=2.0.1" +napi-build = "=2.1.2" [target.'cfg(windows)'.dependencies] widestring = "=1.0.2" From 2ae6fbe2754f96e6e3ee19023ba96cd5839e32ef Mon Sep 17 00:00:00 2001 From: Alex Urbina <42731074+urbinaalex17@users.noreply.github.com> Date: Mon, 25 Mar 2024 11:05:25 -0600 Subject: [PATCH 017/351] DEVOPS-1843 Fix US DEV Web Vault deploys one commit behind (#8458) * DEVOPS-1843 REFACTOR: Trigger web vault deploy step to send the build-web run-id to deploy-web workflow * DEVOPS-1843 ADD: build-web-run-id input to deploy-web workflow to download specific run_id artifact * DEVOPS-1843 FIX: build-web-run-id input in build-web workflow * DEVOPS-1843 REFACTOR: build-web-run-id parameter type to number * DEVOPS-1843 ADD: build-web-run-id input to deploy-web workflow to workflow_dispatch * DEVOPS-1843 FIX: build-web-run-id type in deploy-web.yml * DEVOPS-1843 REFACTOR: web vault deploy action to use GitHub Run ID * DEVOPS-1843 REFACTOR: cloud asset download steps in deploy-web.yml * DEVOPS-1843 REFACTOR: description for build-web workflow Run ID Co-authored-by: MtnBurrit0 <77340197+mimartin12@users.noreply.github.com> --------- Co-authored-by: MtnBurrit0 <77340197+mimartin12@users.noreply.github.com> --- .github/workflows/build-web.yml | 4 ++-- .github/workflows/deploy-web.yml | 34 ++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) 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 From b180463cc0f5f0603cb5388dd7ec2c838719de2b Mon Sep 17 00:00:00 2001 From: aj-rosado <109146700+aj-rosado@users.noreply.github.com> Date: Mon, 25 Mar 2024 18:10:40 +0000 Subject: [PATCH 018/351] Changed logic to validate if cipher has a collection on targetSelector method (#8422) Added tests to import.service.spec.ts that test if the collectionRelationship and folderRelationship properly adapts --- .../cipher-with-collections.json.ts | 37 ++++++++++++ .../src/services/import.service.spec.ts | 56 +++++++++++++++++++ libs/importer/src/services/import.service.ts | 6 +- 3 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 libs/importer/spec/test-data/bitwarden-json/cipher-with-collections.json.ts 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..eb21f384b5 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"; @@ -207,5 +208,60 @@ describe("ImportService", () => { await expect(setImportTargetMethod).rejects.toThrow("Error assigning target folder"); }); + + 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 = {}) { + 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..62961a77c4 100644 --- a/libs/importer/src/services/import.service.ts +++ b/libs/importer/src/services/import.service.ts @@ -437,8 +437,10 @@ export class ImportService implements ImportServiceAbstraction { 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]); } }); From 5184dd956210e4a4b9b9de446b9e19a0df831f6b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 25 Mar 2024 14:31:31 -0500 Subject: [PATCH 019/351] [deps] AC: Update core-js to v3.36.1 (#8472) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 43f1987c88..c1a2f36237 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", @@ -16583,9 +16583,9 @@ } }, "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", diff --git a/package.json b/package.json index d1276287c1..04e22107ad 100644 --- a/package.json +++ b/package.json @@ -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", From 20ba9fb4bed84b05801ea5f11c8d415a8a6c247c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 25 Mar 2024 14:36:48 -0500 Subject: [PATCH 020/351] [deps] AC: Update html-webpack-plugin to v5.6.0 (#8474) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 17 +++++++++++++---- package.json | 2 +- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index c1a2f36237..5b834a9a87 100644 --- a/package-lock.json +++ b/package-lock.json @@ -149,7 +149,7 @@ "gulp-zip": "6.0.0", "html-loader": "4.2.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", @@ -22361,9 +22361,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 +22380,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": { diff --git a/package.json b/package.json index 04e22107ad..7595b423a4 100644 --- a/package.json +++ b/package.json @@ -110,7 +110,7 @@ "gulp-zip": "6.0.0", "html-loader": "4.2.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", From 0957b54d03ee9b21509c524cdec147f64eab2f84 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 25 Mar 2024 14:38:17 -0500 Subject: [PATCH 021/351] [deps] AC: Update copy-webpack-plugin to v12 (#8478) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 213 ++++++++++++++++++++++++++++++++++++++++------ package.json | 2 +- 2 files changed, 186 insertions(+), 29 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5b834a9a87..c89e78a78f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -124,7 +124,7 @@ "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", @@ -614,6 +614,95 @@ "postcss": "^8.1.0" } }, + "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.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/copy-webpack-plugin/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/copy-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/@angular-devkit/build-angular/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", + "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/copy-webpack-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "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" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/@angular-devkit/build-angular/node_modules/cosmiconfig": { "version": "8.3.6", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", @@ -678,6 +767,25 @@ "node": ">=8.6.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": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -866,6 +974,18 @@ "url": "https://opencollective.com/webpack" } }, + "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", @@ -7250,6 +7370,18 @@ "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", @@ -16516,20 +16648,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", @@ -16552,31 +16684,44 @@ } }, "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/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" }, "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==", + "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": ">=12" + "node": ">=14.16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -33432,9 +33577,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", @@ -33635,9 +33780,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" @@ -36891,6 +37036,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", diff --git a/package.json b/package.json index 7595b423a4..a7a9ee3f2b 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ "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", From 899172722af5826e0a27efb18782a0b13ee7d95a Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Mon, 25 Mar 2024 16:26:27 -0400 Subject: [PATCH 022/351] Auth/PM-5263 - TokenService State provider migration bug fix to avoid persisting tokens in local storage (#8413) * PM-5263 - Update token svc state provider migration to avoid persisting secrets that shouldn't exist in local storage to state provider local storage using new migration helper type. * PM-5263 - TokenSvc migration - tests TODO * write tests for migration * fix tests --------- Co-authored-by: Jake Fink --- ...igrate-token-svc-to-state-provider.spec.ts | 144 +++++++++++------- .../38-migrate-token-svc-to-state-provider.ts | 20 ++- 2 files changed, 109 insertions(+), 55 deletions(-) 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; } From 4873f649a9b13be021f73a6ad526c7ca6fee6f4f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 25 Mar 2024 15:27:45 -0500 Subject: [PATCH 023/351] [deps] AC: Update webpack-dev-server to v5 (#8482) * [deps] AC: Update webpack-dev-server to v5 * Update proxy object to be an array --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Addison Beck --- apps/web/webpack.config.js | 19 +- package-lock.json | 584 ++++++++++++++++++++++++++++++------- package.json | 2 +- 3 files changed, 493 insertions(+), 112 deletions(-) diff --git a/apps/web/webpack.config.js b/apps/web/webpack.config.js index f088aadb75..815a8aff9e 100644 --- a/apps/web/webpack.config.js +++ b/apps/web/webpack.config.js @@ -194,39 +194,44 @@ const devServer = }, }, // host: '192.168.1.9', - proxy: { - "/api": { + proxy: [ + { + context: ["/api"], target: envConfig.dev?.proxyApi, pathRewrite: { "^/api": "" }, secure: false, changeOrigin: true, }, - "/identity": { + { + context: ["/identity"], target: envConfig.dev?.proxyIdentity, pathRewrite: { "^/identity": "" }, secure: false, changeOrigin: true, }, - "/events": { + { + context: ["/events"], target: envConfig.dev?.proxyEvents, pathRewrite: { "^/events": "" }, secure: false, changeOrigin: true, }, - "/notifications": { + { + context: ["/notifications"], target: envConfig.dev?.proxyNotifications, pathRewrite: { "^/notifications": "" }, secure: false, changeOrigin: true, ws: true, }, - "/icons": { + { + context: ["/icons"], target: envConfig.dev?.proxyIcons, pathRewrite: { "^/icons": "" }, secure: false, changeOrigin: true, }, - }, + ], headers: (req) => { if (!req.originalUrl.includes("connector.html")) { return { diff --git a/package-lock.json b/package-lock.json index c89e78a78f..b9e050a22a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -183,7 +183,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" }, "engines": { @@ -767,6 +767,26 @@ "node": ">=8.6.0" } }, + "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": { + "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/@angular-devkit/build-angular/node_modules/globby": { "version": "13.2.2", "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", @@ -834,6 +854,15 @@ "tslib": "^2.1.0" } }, + "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, + "engines": { + "node": ">= 10" + } + }, "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", @@ -902,6 +931,21 @@ "webpack": "^5.0.0" } }, + "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", @@ -1033,6 +1077,162 @@ } } }, + "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": { + "@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/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/webpack-dev-server/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/webpack-dev-server/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": 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/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/@angular-devkit/build-webpack": { "version": "0.1602.11", "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1602.11.tgz", @@ -10977,9 +11177,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": "*" @@ -11017,9 +11217,9 @@ } }, "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": "*", @@ -11125,9 +11325,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": "*", @@ -11619,20 +11819,21 @@ } }, "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": "*" } @@ -11644,9 +11845,9 @@ "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": "*" @@ -11714,6 +11915,15 @@ "integrity": "sha512-D0HJET2/UY6k9L6y3f5BL+IDxZmPkYmPT4+qBrRdmRLYRuV0qNKizMgTvYxXZYn+36zjPeoDZAEYBCM6XB+gww==", "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", @@ -14889,23 +15099,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", @@ -15303,6 +15505,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", @@ -17239,6 +17456,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", @@ -17255,6 +17488,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", @@ -17681,12 +17926,6 @@ "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", @@ -23612,6 +23851,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", @@ -23672,6 +23944,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", @@ -25735,13 +26019,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": { @@ -33347,6 +33631,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", @@ -33617,11 +33913,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": { @@ -38080,54 +38377,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": { @@ -38138,33 +38435,46 @@ } } }, - "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==", - "dev": true, - "dependencies": { - "@types/node": "*" - } + "node_modules/webpack-dev-server/node_modules/@types/retry": { + "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/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==", + "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": { - "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": { @@ -38176,48 +38486,114 @@ "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.0", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.8.0.tgz", + "integrity": "sha512-fcs7trFxZlOMadmTw5nyfOwS3il9pr3y+6xzLfXNwmuR/D0i4wz6rJURxArAbcJDGalbpbMvQ/IFI0NojRZgRg==", + "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/p-retry": { + "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.2", + "is-network-error": "^1.0.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webpack-dev-server/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/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.1.1", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.1.1.tgz", + "integrity": "sha512-NmRVq4AvRQs66dFWyDR4GsFDJggtSi2Yn38MXLk0nffgF9n/AIP4TFBg2TQKYaRAN4sHuKOTiz9BnNCENDLEVA==", "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" + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } } }, "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==", + "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" diff --git a/package.json b/package.json index a7a9ee3f2b..e04d7139dc 100644 --- a/package.json +++ b/package.json @@ -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": { From da14d01062d2987e4599aa4c877f74ab7e7ddb89 Mon Sep 17 00:00:00 2001 From: Jason Ng Date: Mon, 25 Mar 2024 16:50:33 -0400 Subject: [PATCH 024/351] [PM-6927] update date for onboarding component to release (#8487) --- .../vault-onboarding/vault-onboarding.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts b/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts index 22f56a85a9..16f68d6111 100644 --- a/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts @@ -43,7 +43,7 @@ export class VaultOnboardingComponent implements OnInit, OnChanges, OnDestroy { isIndividualPolicyVault: boolean; private destroy$ = new Subject(); isNewAccount: boolean; - private readonly onboardingReleaseDate = new Date("2024-01-01"); + private readonly onboardingReleaseDate = new Date("2024-04-02"); showOnboardingAccess$: Observable; protected currentTasks: VaultOnboardingTasks; From d000f081da274a1928589c7ddae2a858d98b0efe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=A8=20Audrey=20=E2=9C=A8?= Date: Tue, 26 Mar 2024 07:59:45 -0400 Subject: [PATCH 025/351] [PM-6556] reintroduce policy reduction for multi-org accounts (#8409) --- .../generator-strategy.abstraction.ts | 9 ++- .../default-generator.service.spec.ts | 36 ++++++------ .../generator/default-generator.service.ts | 14 ++--- .../passphrase-generator-policy.spec.ts | 51 +++++++++++++++++ .../passphrase/passphrase-generator-policy.ts | 26 +++++++++ .../passphrase-generator-strategy.spec.ts | 34 ++++++------ .../passphrase-generator-strategy.ts | 31 ++++------- .../password-generator-policy.spec.ts | 55 +++++++++++++++++++ .../password/password-generator-policy.ts | 27 +++++++++ .../password-generator-strategy.spec.ts | 34 ++++++------ .../password/password-generator-strategy.ts | 33 ++++------- .../reduce-collection.operator.spec.ts | 33 +++++++++++ .../generator/reduce-collection.operator.ts | 20 +++++++ .../catchall-generator-strategy.spec.ts | 46 ++++++---------- .../username/catchall-generator-strategy.ts | 18 ++---- .../eff-username-generator-strategy.spec.ts | 46 ++++++---------- .../eff-username-generator-strategy.ts | 18 ++---- .../forwarder-generator-strategy.spec.ts | 25 +++++++-- .../username/forwarder-generator-strategy.ts | 9 +-- .../subaddress-generator-strategy.spec.ts | 46 ++++++---------- .../username/subaddress-generator-strategy.ts | 18 ++---- 21 files changed, 388 insertions(+), 241 deletions(-) create mode 100644 libs/common/src/tools/generator/passphrase/passphrase-generator-policy.spec.ts create mode 100644 libs/common/src/tools/generator/password/password-generator-policy.spec.ts create mode 100644 libs/common/src/tools/generator/reduce-collection.operator.spec.ts create mode 100644 libs/common/src/tools/generator/reduce-collection.operator.ts 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..eda02f7cdc 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 @@ -21,13 +23,16 @@ export abstract class GeneratorStrategy { /** 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/default-generator.service.spec.ts b/libs/common/src/tools/generator/default-generator.service.spec.ts index 84b8ff4530..53a46c4963 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); @@ -46,7 +46,9 @@ function mockGeneratorStrategy(config?: { // the value from `config`. durableState: jest.fn(() => durableState), policy: config?.policy ?? PolicyType.DisableSend, - evaluator: jest.fn(() => config?.evaluator ?? mock>()), + toEvaluator: jest.fn(() => + pipe(map(() => config?.evaluator ?? mock>())), + ), }); return strategy; @@ -94,9 +96,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 +112,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 +143,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 +154,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..34aacee695 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 @@ -44,14 +44,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), 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..b7f09bd717 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 @@ -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 @@ -21,17 +22,8 @@ import { 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 +34,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 +45,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", () => { 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..f193b2b326 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,19 @@ +import { 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 { 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 { PassphraseGeneratorOptionsEvaluator } from "./passphrase-generator-options-evaluator"; import { DisabledPassphraseGeneratorPolicy, PassphraseGeneratorPolicy, + leastPrivilege, } from "./passphrase-generator-policy"; const ONE_MINUTE = 60 * 1000; @@ -23,6 +24,7 @@ export class PassphraseGeneratorStrategy { /** instantiates the password generator strategy. * @param legacy generates the passphrase + * @param stateProvider provides durable state */ constructor( private legacy: PasswordGenerationServiceAbstraction, @@ -39,26 +41,17 @@ export class PassphraseGeneratorStrategy 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/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..9bfa5b5f35 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 @@ -24,17 +25,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 +41,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 +56,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", () => { 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..f8d618128b 100644 --- a/libs/common/src/tools/generator/password/password-generator-strategy.ts +++ b/libs/common/src/tools/generator/password/password-generator-strategy.ts @@ -1,11 +1,11 @@ +import { 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 { PASSWORD_SETTINGS } from "../key-definitions"; +import { reduceCollection } from "../reduce-collection.operator"; import { PasswordGenerationOptions } from "./password-generation-options"; import { PasswordGenerationServiceAbstraction } from "./password-generation.service.abstraction"; @@ -13,6 +13,7 @@ import { PasswordGeneratorOptionsEvaluator } from "./password-generator-options- import { DisabledPasswordGeneratorPolicy, PasswordGeneratorPolicy, + leastPrivilege, } from "./password-generator-policy"; const ONE_MINUTE = 60 * 1000; @@ -43,26 +44,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/username/catchall-generator-strategy.spec.ts b/libs/common/src/tools/generator/username/catchall-generator-strategy.spec.ts index dafb55feba..339e4b2720 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 @@ -12,39 +13,26 @@ import { CATCHALL_SETTINGS } from "../key-definitions"; import { CatchallGeneratorStrategy, UsernameGenerationServiceAbstraction } from "."; const SomeUser = "some user" as UserId; +const SomePolicy = mock({ + 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({ - 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({ - 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", () => { 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..6b36ebd50b 100644 --- a/libs/common/src/tools/generator/username/catchall-generator-strategy.ts +++ b/libs/common/src/tools/generator/username/catchall-generator-strategy.ts @@ -1,5 +1,6 @@ +import { 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"; @@ -41,18 +42,9 @@ export class CatchallGeneratorStrategy return ONE_MINUTE; } - /** {@link GeneratorStrategy.evaluator} */ - evaluator(policy: Policy) { - if (!policy) { - return new DefaultPolicyEvaluator(); - } - - if (policy.type !== this.policy) { - const details = `Expected: ${this.policy}. Received: ${policy.type}`; - throw Error("Mismatched policy type. " + details); - } - - return new DefaultPolicyEvaluator(); + /** {@link GeneratorStrategy.toEvaluator} */ + toEvaluator() { + return pipe(map((_) => new DefaultPolicyEvaluator())); } /** {@link GeneratorStrategy.generate} */ 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..821b4bb7dc 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 @@ -12,39 +13,26 @@ import { EFF_USERNAME_SETTINGS } from "../key-definitions"; import { EffUsernameGeneratorStrategy, UsernameGenerationServiceAbstraction } from "."; const SomeUser = "some user" as UserId; +const SomePolicy = mock({ + 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({ - 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({ - 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", () => { 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..133b4e7777 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,5 +1,6 @@ +import { 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"; @@ -41,18 +42,9 @@ export class EffUsernameGeneratorStrategy return ONE_MINUTE; } - /** {@link GeneratorStrategy.evaluator} */ - evaluator(policy: Policy) { - if (!policy) { - return new DefaultPolicyEvaluator(); - } - - if (policy.type !== this.policy) { - const details = `Expected: ${this.policy}. Received: ${policy.type}`; - throw Error("Mismatched policy type. " + details); - } - - return new DefaultPolicyEvaluator(); + /** {@link GeneratorStrategy.toEvaluator} */ + toEvaluator() { + return pipe(map((_) => new DefaultPolicyEvaluator())); } /** {@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..30dd620484 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"; @@ -29,6 +34,12 @@ class TestForwarder extends ForwarderGeneratorStrategy { const SomeUser = "some user" as UserId; const AnotherUser = "another user" as UserId; +const SomePolicy = mock({ + type: PolicyType.PasswordGenerator, + data: { + minLength: 10, + }, +}); describe("ForwarderGeneratorStrategy", () => { const encryptService = mock(); @@ -63,11 +74,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..8b78f22634 100644 --- a/libs/common/src/tools/generator/username/forwarder-generator-strategy.ts +++ b/libs/common/src/tools/generator/username/forwarder-generator-strategy.ts @@ -1,5 +1,6 @@ +import { 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"; @@ -81,8 +82,8 @@ export abstract class ForwarderGeneratorStrategy< /** Determine where forwarder configuration is stored */ protected abstract readonly key: KeyDefinition; - /** {@link GeneratorStrategy.evaluator} */ - evaluator = (_policy: Policy) => { - return new DefaultPolicyEvaluator(); + /** {@link GeneratorStrategy.toEvaluator} */ + toEvaluator = () => { + return pipe(map((_) => new DefaultPolicyEvaluator())); }; } 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..59a2b56172 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 @@ -12,39 +13,26 @@ import { SUBADDRESS_SETTINGS } from "../key-definitions"; import { SubaddressGeneratorStrategy, UsernameGenerationServiceAbstraction } from "."; const SomeUser = "some user" as UserId; +const SomePolicy = mock({ + 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({ - 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({ - 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", () => { 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..1ae0cb9142 100644 --- a/libs/common/src/tools/generator/username/subaddress-generator-strategy.ts +++ b/libs/common/src/tools/generator/username/subaddress-generator-strategy.ts @@ -1,5 +1,6 @@ +import { 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"; @@ -41,18 +42,9 @@ export class SubaddressGeneratorStrategy return ONE_MINUTE; } - /** {@link GeneratorStrategy.evaluator} */ - evaluator(policy: Policy) { - if (!policy) { - return new DefaultPolicyEvaluator(); - } - - if (policy.type !== this.policy) { - const details = `Expected: ${this.policy}. Received: ${policy.type}`; - throw Error("Mismatched policy type. " + details); - } - - return new DefaultPolicyEvaluator(); + /** {@link GeneratorStrategy.toEvaluator} */ + toEvaluator() { + return pipe(map((_) => new DefaultPolicyEvaluator())); } /** {@link GeneratorStrategy.generate} */ From a46767dee2e9c794edc9872693eacd99aca6b335 Mon Sep 17 00:00:00 2001 From: Jake Fink Date: Tue, 26 Mar 2024 09:56:20 -0400 Subject: [PATCH 026/351] add auth status to auth service (#8377) * add auth status to auth service * fix auth service factory --- .../service-factories/auth-service.factory.ts | 4 ++ .../browser/src/background/main.background.ts | 1 + apps/cli/src/bw.ts | 1 + .../src/services/jslib-services.module.ts | 1 + .../src/auth/abstractions/auth.service.ts | 9 ++- .../src/auth/services/auth.service.spec.ts | 61 +++++++++++++++++++ libs/common/src/auth/services/auth.service.ts | 14 ++++- 7 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 libs/common/src/auth/services/auth.service.spec.ts 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..bc4e621bc6 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,9 +23,12 @@ import { stateServiceFactory, } from "../../../platform/background/service-factories/state-service.factory"; +import { AccountServiceInitOptions, accountServiceFactory } from "./account-service.factory"; + type AuthServiceFactoryOptions = FactoryOptions; export type AuthServiceInitOptions = AuthServiceFactoryOptions & + AccountServiceInitOptions & MessagingServiceInitOptions & CryptoServiceInitOptions & ApiServiceInitOptions & @@ -41,6 +44,7 @@ export function authServiceFactory( opts, async () => new AuthService( + await accountServiceFactory(cache, opts), await messagingServiceFactory(cache, opts), await cryptoServiceFactory(cache, opts), await apiServiceFactory(cache, opts), diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 3e84b7544b..51417c16fb 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -568,6 +568,7 @@ export default class MainBackground { ); this.authService = new AuthService( + this.accountService, backgroundMessagingService, this.cryptoService, this.apiService, diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index 7af40b1ebd..5c6423708a 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -494,6 +494,7 @@ export class Main { ); this.authService = new AuthService( + this.accountService, this.messagingService, this.cryptoService, this.apiService, diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index beda7cbf4f..57a3fe63ab 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -345,6 +345,7 @@ const typesafeProviders: Array = [ provide: AuthServiceAbstraction, useClass: AuthService, deps: [ + AccountServiceAbstraction, MessagingServiceAbstraction, CryptoServiceAbstraction, ApiServiceAbstraction, diff --git a/libs/common/src/auth/abstractions/auth.service.ts b/libs/common/src/auth/abstractions/auth.service.ts index dc51e2fdb0..9e4fd3cd0b 100644 --- a/libs/common/src/auth/abstractions/auth.service.ts +++ b/libs/common/src/auth/abstractions/auth.service.ts @@ -1,6 +1,11 @@ +import { Observable } from "rxjs"; + 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; + /** @deprecated use {@link activeAccountStatus$} instead */ + abstract getAuthStatus: (userId?: string) => Promise; + abstract logOut: (callback: () => void) => void; } 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..dd4daf8cfa --- /dev/null +++ b/libs/common/src/auth/services/auth.service.spec.ts @@ -0,0 +1,61 @@ +import { MockProxy, mock } from "jest-mock-extended"; +import { firstValueFrom } from "rxjs"; + +import { FakeAccountService, mockAccountServiceWith } 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 { UserId } from "../../types/guid"; +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; + + const userId = Utils.newGuid() as UserId; + + beforeEach(() => { + accountService = mockAccountServiceWith(userId); + messagingService = mock(); + cryptoService = mock(); + apiService = mock(); + stateService = mock(); + + sut = new AuthService( + accountService, + messagingService, + cryptoService, + apiService, + stateService, + ); + }); + + describe("activeAccountStatus$", () => { + test.each([ + AuthenticationStatus.LoggedOut, + AuthenticationStatus.Locked, + AuthenticationStatus.Unlocked, + ])( + `should emit %p when activeAccount$ emits an account with %p auth status`, + async (status) => { + accountService.activeAccountSubject.next({ + id: userId, + email: "email", + name: "name", + status, + }); + + expect(await firstValueFrom(sut.activeAccountStatus$)).toEqual(status); + }, + ); + }); +}); diff --git a/libs/common/src/auth/services/auth.service.ts b/libs/common/src/auth/services/auth.service.ts index 14d49956a4..ae5dd30a36 100644 --- a/libs/common/src/auth/services/auth.service.ts +++ b/libs/common/src/auth/services/auth.service.ts @@ -1,18 +1,30 @@ +import { Observable, distinctUntilChanged, map, shareReplay } 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 { AccountService } from "../abstractions/account.service"; import { AuthService as AuthServiceAbstraction } from "../abstractions/auth.service"; import { AuthenticationStatus } from "../enums/authentication-status"; export class AuthService implements AuthServiceAbstraction { + activeAccountStatus$: Observable; + constructor( + protected accountService: AccountService, protected messagingService: MessagingService, protected cryptoService: CryptoService, protected apiService: ApiService, protected stateService: StateService, - ) {} + ) { + this.activeAccountStatus$ = this.accountService.activeAccount$.pipe( + map((account) => account.status), + 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 From 7f14ee4994903c78fdc46cd0d551f0518f44597e Mon Sep 17 00:00:00 2001 From: Jake Fink Date: Tue, 26 Mar 2024 12:00:30 -0400 Subject: [PATCH 027/351] add back call to verify by PIN (#8495) --- .../services/user-verification/user-verification.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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..0b4cd96099 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 @@ -140,7 +140,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: { From f7014a973c541662dfce952915f19d6b9ff357e1 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Tue, 26 Mar 2024 11:06:33 -0500 Subject: [PATCH 028/351] [PM-7071] Fallback to Emitting `null` When No Active User (#8486) * Fallback to Emitting `null` When No Active User * Fix Tests * Update Test Names to Follow Convention Co-authored-by: Andreas Coroiu * Fix CLI Build --------- Co-authored-by: Andreas Coroiu --- .../browser/src/background/main.background.ts | 2 +- ...g-account-profile-state-service.factory.ts | 7 +- apps/cli/src/bw.ts | 2 +- .../src/services/jslib-services.module.ts | 2 +- ...ling-account-profile-state.service.spec.ts | 189 ++++++++---------- .../billing-account-profile-state.service.ts | 28 ++- 6 files changed, 108 insertions(+), 122 deletions(-) diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 51417c16fb..452624d77e 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -576,7 +576,7 @@ export default class MainBackground { ); this.billingAccountProfileStateService = new DefaultBillingAccountProfileStateService( - this.activeUserStateProvider, + this.stateProvider, ); this.loginStrategyService = new LoginStrategyService( diff --git a/apps/browser/src/platform/background/service-factories/billing-account-profile-state-service.factory.ts b/apps/browser/src/platform/background/service-factories/billing-account-profile-state-service.factory.ts index 80482eacb6..378707d6be 100644 --- a/apps/browser/src/platform/background/service-factories/billing-account-profile-state-service.factory.ts +++ b/apps/browser/src/platform/background/service-factories/billing-account-profile-state-service.factory.ts @@ -1,9 +1,8 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { DefaultBillingAccountProfileStateService } from "@bitwarden/common/billing/services/account/billing-account-profile-state.service"; -import { activeUserStateProviderFactory } from "./active-user-state-provider.factory"; import { FactoryOptions, CachedServices, factory } from "./factory-options"; -import { StateProviderInitOptions } from "./state-provider.factory"; +import { StateProviderInitOptions, stateProviderFactory } from "./state-provider.factory"; type BillingAccountProfileStateServiceFactoryOptions = FactoryOptions; @@ -21,8 +20,6 @@ export function billingAccountProfileStateServiceFactory( "billingAccountProfileStateService", opts, async () => - new DefaultBillingAccountProfileStateService( - await activeUserStateProviderFactory(cache, opts), - ), + new DefaultBillingAccountProfileStateService(await stateProviderFactory(cache, opts)), ); } diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index 5c6423708a..360ac6ffc4 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -467,7 +467,7 @@ export class Main { ); this.billingAccountProfileStateService = new DefaultBillingAccountProfileStateService( - this.activeUserStateProvider, + this.stateProvider, ); this.loginStrategyService = new LoginStrategyService( diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 57a3fe63ab..cab71631da 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -1067,7 +1067,7 @@ const typesafeProviders: Array = [ safeProvider({ provide: BillingAccountProfileStateService, useClass: DefaultBillingAccountProfileStateService, - deps: [ActiveUserStateProvider], + deps: [StateProvider], }), safeProvider({ provide: OrganizationManagementPreferencesService, 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..336021c993 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,10 +1,10 @@ -import { map, Observable } from "rxjs"; +import { map, Observable, of, switchMap } from "rxjs"; import { ActiveUserState, - ActiveUserStateProvider, BILLING_DISK, KeyDefinition, + StateProvider, } from "../../../platform/state"; import { BillingAccountProfile, @@ -26,24 +26,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, ), ); } From 1cb16543462eeba3111a7f8e2d28f3b4ebfd238c Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Tue, 26 Mar 2024 09:10:28 -0700 Subject: [PATCH 029/351] [PM-7087] Hide bulk assign collections menu item when showBulkAddToCollections is false (#8494) --- .../app/vault/components/vault-items/vault-items.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts index 66d4a559ce..b17eed8ca1 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts @@ -91,7 +91,7 @@ export class VaultItemsComponent { } get bulkAssignToCollectionsAllowed() { - return this.ciphers.length > 0; + return this.showBulkAddToCollections && this.ciphers.length > 0; } protected canEditCollection(collection: CollectionView): boolean { From 2064862afcf05285d94ed0eefc1eeb99bdde4c10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa?= Date: Tue, 26 Mar 2024 17:23:01 +0100 Subject: [PATCH 030/351] [PM-6832][PM-7030] Rollback macos runner version to 11 (#8450) --- .github/workflows/build-desktop.yml | 20 ++++++++++++++++---- .github/workflows/release-desktop-beta.yml | 15 ++++++++++++--- 2 files changed, 28 insertions(+), 7 deletions(-) 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/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 From 69530241d10519b517de984fb909cf8aae0aac6f Mon Sep 17 00:00:00 2001 From: SmithThe4th Date: Tue, 26 Mar 2024 13:00:43 -0400 Subject: [PATCH 031/351] [PM-6532] Admin Console Single Sign on Settings page fields expand too much (#8386) * added class to reduce width of fields * moved class to form --- bitwarden_license/bit-web/src/app/auth/sso/sso.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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..72a073e0c0 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 @@ -9,7 +9,7 @@ {{ "loading" | i18n }} -
+

{{ "ssoPolicyHelpStart" | i18n }} {{ "ssoPolicyHelpAnchor" | i18n }} From 1e75f24671d544f24efe6af792cc68b14206fbea Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Tue, 26 Mar 2024 10:29:50 -0700 Subject: [PATCH 032/351] [PM-7059] Use decryptedCollections$ observable instead of async getAllDecrypted call (#8488) --- apps/web/src/app/vault/individual-vault/vault.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index 6fee59d65b..1dc6fdaf1c 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -246,7 +246,7 @@ export class VaultComponent implements OnInit, OnDestroy { }); const filter$ = this.routedVaultFilterService.filter$; - const allCollections$ = Utils.asyncToObservable(() => this.collectionService.getAllDecrypted()); + const allCollections$ = this.collectionService.decryptedCollections$; const nestedCollections$ = allCollections$.pipe( map((collections) => getNestedCollectionTree(collections)), ); From 7f5583397427e6458f2f69b6f9fd995a32c6815d Mon Sep 17 00:00:00 2001 From: Jason Ng Date: Tue, 26 Mar 2024 15:22:35 -0400 Subject: [PATCH 033/351] [AC-2285] Edit Unassigned Ciphers in AC Bug (#8410) * check if cipher is unassigned and call the proper service between cipherService get and apiService get. also check for custom user permissions --- apps/web/src/app/vault/org-vault/add-edit.component.ts | 10 ++++++++-- .../angular/src/vault/components/add-edit.component.ts | 8 ++++---- libs/common/src/vault/services/cipher.service.ts | 1 - 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/apps/web/src/app/vault/org-vault/add-edit.component.ts b/apps/web/src/app/vault/org-vault/add-edit.component.ts index cb879dfcc7..ba0c65b107 100644 --- a/apps/web/src/app/vault/org-vault/add-edit.component.ts +++ b/apps/web/src/app/vault/org-vault/add-edit.component.ts @@ -105,8 +105,14 @@ export class AddEditComponent extends BaseAddEditComponent { } protected async loadCipher() { - if (!this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) { - return await super.loadCipher(); + // Calling loadCipher first to assess if the cipher is unassigned. If null use apiService getCipherAdmin + const firstCipherCheck = await super.loadCipher(); + + if ( + !this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled) && + firstCipherCheck != null + ) { + return firstCipherCheck; } const response = await this.apiService.getCipherAdmin(this.cipherId); const data = new CipherData(response); diff --git a/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts index 680672514a..83131f8fc5 100644 --- a/libs/angular/src/vault/components/add-edit.component.ts +++ b/libs/angular/src/vault/components/add-edit.component.ts @@ -650,11 +650,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?.canEditAnyCollection; } return this.cipher.id == null diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 4293e56728..829ee5ed4e 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -1387,7 +1387,6 @@ export class CipherService implements CipherServiceAbstraction { cipher.attachments = attachments; }), ]); - return cipher; } From a66e224d3298a6600dc1c0acd0b6b37030c9e1e1 Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Tue, 26 Mar 2024 18:41:14 -0400 Subject: [PATCH 034/351] Auth/PM-7072 - Token Service - Access Token Secure Storage Refactor (#8412) * PM-5263 - TokenSvc - WIP on access token secure storage refactor * PM-5263 - Add key generation svc to token svc. * PM-5263 - TokenSvc - more progress on encrypt access token work. * PM-5263 - TokenSvc TODO cleanup * PM-5263 - TokenSvc - rename * PM-5263 - TokenSvc - decryptAccess token must return null as that is a valid case. * PM-5263 - Add EncryptSvc dep to TokenSvc * PM-5263 - Add secure storage to token service * PM-5263 - TokenSvc - (1) Finish implementing accessTokenKey stored in secure storage + encrypted access token stored on disk (2) Remove no longer necessary migration flag as the presence of the accessTokenKey now serves the same purpose. Co-authored-by: Jake Fink * PM-5263 - TokenSvc - (1) Tweak return structure of decryptAccessToken to be more debuggable (2) Add TODO to add more error handling. * PM-5263 - TODO: update tests * PM-5263 - add temp logs * PM-5263 - TokenSvc - remove logs now that I don't need them. * fix tests for access token * PM-5263 - TokenSvc test cleanup - small tweaks / cleanup * PM-5263 - TokenService - per PR feedback from Justin - add error message to error message if possible. Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> --------- Co-authored-by: Jake Fink Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> --- .../token-service.factory.ts | 20 +- .../browser/src/background/main.background.ts | 3 + apps/cli/src/bw.ts | 7 +- apps/desktop/src/main.ts | 29 +- .../illegal-secure-storage.service.ts | 28 ++ .../src/services/jslib-services.module.ts | 5 +- .../src/auth/services/token.service.spec.ts | 291 +++++++----------- .../common/src/auth/services/token.service.ts | 189 +++++++++--- .../src/auth/services/token.state.spec.ts | 2 - libs/common/src/auth/services/token.state.ts | 8 - 10 files changed, 354 insertions(+), 228 deletions(-) create mode 100644 apps/desktop/src/platform/services/illegal-secure-storage.service.ts 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/background/main.background.ts b/apps/browser/src/background/main.background.ts index 452624d77e..14ded13c3e 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -443,6 +443,9 @@ export default class MainBackground { this.globalStateProvider, this.platformUtilsService.supportsSecureStorage(), this.secureStorageService, + this.keyGenerationService, + this.encryptService, + this.logService, ); const migrationRunner = new MigrationRunner( diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index 360ac6ffc4..e610f39954 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -318,11 +318,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,8 +348,6 @@ export class Main { migrationRunner, ); - this.keyGenerationService = new KeyGenerationService(this.cryptoFunctionService); - this.cryptoService = new CryptoService( this.keyGenerationService, this.cryptoFunctionService, diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 5cb6abac58..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"; @@ -45,6 +49,7 @@ 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"; @@ -62,6 +67,8 @@ export class Main { desktopSettingsService: DesktopSettingsService; migrationRunner: MigrationRunner; tokenService: TokenServiceAbstraction; + keyGenerationService: KeyGenerationServiceAbstraction; + encryptService: EncryptService; windowMain: WindowMain; messagingMain: MessagingMain; @@ -153,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( @@ -239,9 +263,6 @@ export class Main { this.clipboardMain = new ClipboardMain(); this.clipboardMain.init(); - - this.mainCryptoFunctionService = new MainCryptoFunctionService(); - this.mainCryptoFunctionService.init(); } bootstrap() { 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/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index cab71631da..b2aebe20f4 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -503,7 +503,10 @@ const typesafeProviders: Array = [ SingleUserStateProvider, GlobalStateProvider, SUPPORTS_SECURE_STORAGE, - AbstractStorageService, + SECURE_STORAGE, + KeyGenerationServiceAbstraction, + EncryptService, + LogService, ], }), safeProvider({ diff --git a/libs/common/src/auth/services/token.service.spec.ts b/libs/common/src/auth/services/token.service.spec.ts index a7b953f928..63c581910a 100644 --- a/libs/common/src/auth/services/token.service.spec.ts +++ b/libs/common/src/auth/services/token.service.spec.ts @@ -1,7 +1,10 @@ -import { mock } from "jest-mock-extended"; +import { MockProxy, mock } from "jest-mock-extended"; 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 +15,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, @@ -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,8 @@ describe("TokenService", () => { }); describe("Access Token methods", () => { - const accessTokenPartialSecureStorageKey = `_accessToken`; - const accessTokenSecureStorageKey = `${userIdFromAccessToken}${accessTokenPartialSecureStorageKey}`; + const accessTokenKeyPartialSecureStorageKey = `_accessTokenKey`; + const accessTokenKeySecureStorageKey = `${userIdFromAccessToken}${accessTokenKeyPartialSecureStorageKey}`; describe("setAccessToken", () => { it("should throw an error if the access token is null", async () => { @@ -150,18 +162,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 +186,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 +227,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 +245,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 +277,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 +296,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 +313,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 +352,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 +384,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 +403,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 +422,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, ); }); @@ -2232,6 +2174,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..a1dc7ecf21 100644 --- a/libs/common/src/auth/services/token.service.ts +++ b/libs/common/src/auth/services/token.service.ts @@ -1,11 +1,17 @@ import { firstValueFrom } 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, @@ -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, @@ -101,8 +106,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 +128,17 @@ 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(); } + // 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, @@ -155,6 +173,84 @@ export class TokenService implements TokenServiceAbstraction { } } + 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 +269,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 +329,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 +351,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 @@ -417,7 +531,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 +598,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 +681,7 @@ export class TokenService implements TokenServiceAbstraction { }); } + // TODO: stop accepting optional userIds async clearTokens(userId?: UserId): Promise { userId ??= await firstValueFrom(this.activeUserIdGlobalState.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..24eddc73f5 100644 --- a/libs/common/src/auth/services/token.state.spec.ts +++ b/libs/common/src/auth/services/token.state.spec.ts @@ -3,7 +3,6 @@ import { KeyDefinition } 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, @@ -17,7 +16,6 @@ import { 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], diff --git a/libs/common/src/auth/services/token.state.ts b/libs/common/src/auth/services/token.state.ts index 022f56f7aa..55471e1627 100644 --- a/libs/common/src/auth/services/token.state.ts +++ b/libs/common/src/auth/services/token.state.ts @@ -8,14 +8,6 @@ export const ACCESS_TOKEN_MEMORY = new KeyDefinition(TOKEN_MEMORY, "acce deserializer: (accessToken) => accessToken, }); -export const ACCESS_TOKEN_MIGRATED_TO_SECURE_STORAGE = new KeyDefinition( - TOKEN_DISK, - "accessTokenMigratedToSecureStorage", - { - deserializer: (accessTokenMigratedToSecureStorage) => accessTokenMigratedToSecureStorage, - }, -); - export const REFRESH_TOKEN_DISK = new KeyDefinition(TOKEN_DISK, "refreshToken", { deserializer: (refreshToken) => refreshToken, }); From 98556ce8bd01b9303f245cd0058ab9e526cac726 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 27 Mar 2024 09:43:52 +1000 Subject: [PATCH 035/351] [deps] AC: Update css-loader to v6.10.0 (#8473) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 83 ++++++++++++++++++++++++++++++++++++++++++----- package.json | 2 +- 2 files changed, 76 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index b9e050a22a..cf932a1363 100644 --- a/package-lock.json +++ b/package-lock.json @@ -126,7 +126,7 @@ "concurrently": "8.2.2", "copy-webpack-plugin": "12.0.2", "cross-env": "7.0.3", - "css-loader": "6.8.1", + "css-loader": "6.10.0", "electron": "28.2.8", "electron-builder": "24.13.3", "electron-log": "5.0.1", @@ -729,6 +729,32 @@ } } }, + "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": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -17144,19 +17170,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" @@ -17166,7 +17192,48 @@ "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-loader/node_modules/postcss-modules-local-by-default": { + "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", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/css-loader/node_modules/postcss-modules-scope": { + "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" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" } }, "node_modules/css-select": { diff --git a/package.json b/package.json index e04d7139dc..59d84a7427 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "concurrently": "8.2.2", "copy-webpack-plugin": "12.0.2", "cross-env": "7.0.3", - "css-loader": "6.8.1", + "css-loader": "6.10.0", "electron": "28.2.8", "electron-builder": "24.13.3", "electron-log": "5.0.1", From 96d274b332f147e274053ed12c175de4cf57b9d6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 27 Mar 2024 09:53:34 +1000 Subject: [PATCH 036/351] [deps] AC: Update postcss-loader to v8 (#8480) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 31 ++++++++++++++++++++----------- package.json | 2 +- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index cf932a1363..5368749853 100644 --- a/package-lock.json +++ b/package-lock.json @@ -159,7 +159,7 @@ "node-ipc": "9.2.1", "pkg": "5.8.1", "postcss": "8.4.35", - "postcss-loader": "7.3.4", + "postcss-loader": "8.1.1", "prettier": "3.2.2", "prettier-plugin-tailwindcss": "0.5.12", "process": "0.11.10", @@ -31501,25 +31501,34 @@ } }, "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": { @@ -31529,15 +31538,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" diff --git a/package.json b/package.json index 59d84a7427..86026c6e93 100644 --- a/package.json +++ b/package.json @@ -120,7 +120,7 @@ "node-ipc": "9.2.1", "pkg": "5.8.1", "postcss": "8.4.35", - "postcss-loader": "7.3.4", + "postcss-loader": "8.1.1", "prettier": "3.2.2", "prettier-plugin-tailwindcss": "0.5.12", "process": "0.11.10", From 3f6a5671227d0a9e85c92c85bead575c57c2dbdb Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Wed, 27 Mar 2024 08:47:23 -0700 Subject: [PATCH 037/351] [AC-2351] Call filterCollections within the organizations$ subscription to avoid race condition (#8498) --- libs/angular/src/components/share.component.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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() { From e98d29d2c83ed5b9a77e51ccecad470a81eb73f1 Mon Sep 17 00:00:00 2001 From: Tom <144813356+ttalty@users.noreply.github.com> Date: Wed, 27 Mar 2024 12:34:15 -0400 Subject: [PATCH 038/351] [PM-5593] Removing BrowserSendService from services (#8512) * Removing send service from services, removed browser send, and pointed to send services * Make linter happy --------- Co-authored-by: Daniel James Smith --- .../browser/src/background/main.background.ts | 4 +- .../service-factories/send-service.factory.ts | 4 +- .../src/popup/services/services.module.ts | 42 ------------------- .../src/services/browser-send.service.ts | 15 ------- 4 files changed, 4 insertions(+), 61 deletions(-) delete mode 100644 apps/browser/src/services/browser-send.service.ts diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 14ded13c3e..73c4356f69 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -143,6 +143,7 @@ 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 { SendService } from "@bitwarden/common/tools/send/services/send.service"; import { InternalSendService as InternalSendServiceAbstraction } from "@bitwarden/common/tools/send/services/send.service.abstraction"; import { UserId } from "@bitwarden/common/types/guid"; import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -213,7 +214,6 @@ import { BackgroundPlatformUtilsService } from "../platform/services/platform-ut import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service"; import { BackgroundDerivedStateProvider } from "../platform/state/background-derived-state.provider"; import { BackgroundMemoryStorageService } from "../platform/storage/background-memory-storage.service"; -import { BrowserSendService } from "../services/browser-send.service"; import VaultTimeoutService from "../services/vault-timeout/vault-timeout.service"; import FilelessImporterBackground from "../tools/background/fileless-importer.background"; import { BrowserFido2UserInterfaceService } from "../vault/fido2/browser-fido2-user-interface.service"; @@ -698,7 +698,7 @@ export default class MainBackground { logoutCallback, ); this.containerService = new ContainerService(this.cryptoService, this.encryptService); - this.sendService = new BrowserSendService( + this.sendService = new SendService( this.cryptoService, this.i18nService, this.keyGenerationService, diff --git a/apps/browser/src/background/service-factories/send-service.factory.ts b/apps/browser/src/background/service-factories/send-service.factory.ts index bca46b4703..7c64bc076a 100644 --- a/apps/browser/src/background/service-factories/send-service.factory.ts +++ b/apps/browser/src/background/service-factories/send-service.factory.ts @@ -1,3 +1,4 @@ +import { SendService } from "@bitwarden/common/tools/send/services/send.service"; import { InternalSendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; import { @@ -21,7 +22,6 @@ import { stateServiceFactory, StateServiceInitOptions, } from "../../platform/background/service-factories/state-service.factory"; -import { BrowserSendService } from "../../services/browser-send.service"; type SendServiceFactoryOptions = FactoryOptions; @@ -40,7 +40,7 @@ export function sendServiceFactory( "sendService", opts, async () => - new BrowserSendService( + new SendService( await cryptoServiceFactory(cache, opts), await i18nServiceFactory(cache, opts), await keyGenerationServiceFactory(cache, opts), diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 33fe6a52af..25db6f78ca 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -16,7 +16,6 @@ import { AuthRequestServiceAbstraction, LoginStrategyServiceAbstraction, } from "@bitwarden/auth/common"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service"; import { SearchService as SearchServiceAbstraction } from "@bitwarden/common/abstractions/search.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; @@ -53,9 +52,7 @@ import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.se import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; -import { FileUploadService } from "@bitwarden/common/platform/abstractions/file-upload/file-upload.service"; import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { LogService, LogService as LogServiceAbstraction, @@ -82,12 +79,6 @@ import { import { SearchService } from "@bitwarden/common/services/search.service"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username"; -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 { - InternalSendService as InternalSendServiceAbstraction, - SendService, -} from "@bitwarden/common/tools/send/services/send.service.abstraction"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; import { CipherFileUploadService } from "@bitwarden/common/vault/abstractions/file-upload/cipher-file-upload.service"; @@ -115,7 +106,6 @@ import I18nService from "../../platform/services/i18n.service"; import { ForegroundPlatformUtilsService } from "../../platform/services/platform-utils/foreground-platform-utils.service"; import { ForegroundDerivedStateProvider } from "../../platform/state/foreground-derived-state.provider"; import { ForegroundMemoryStorageService } from "../../platform/storage/foreground-memory-storage.service"; -import { BrowserSendService } from "../../services/browser-send.service"; import { FilePopoutUtilsService } from "../../tools/popup/services/file-popout-utils.service"; import { VaultFilterService } from "../../vault/services/vault-filter.service"; @@ -295,38 +285,6 @@ function getBgService(service: keyof MainBackground) { useFactory: getBgService("passwordGenerationService"), deps: [], }, - { - provide: SendService, - useFactory: ( - cryptoService: CryptoService, - i18nService: I18nServiceAbstraction, - keyGenerationService: KeyGenerationService, - stateServiceAbstraction: StateServiceAbstraction, - ) => { - return new BrowserSendService( - cryptoService, - i18nService, - keyGenerationService, - stateServiceAbstraction, - ); - }, - deps: [CryptoService, I18nServiceAbstraction, KeyGenerationService, StateServiceAbstraction], - }, - { - provide: InternalSendServiceAbstraction, - useExisting: SendService, - }, - { - provide: SendApiServiceAbstraction, - useFactory: ( - apiService: ApiService, - fileUploadService: FileUploadService, - sendService: InternalSendServiceAbstraction, - ) => { - return new SendApiService(apiService, fileUploadService, sendService); - }, - deps: [ApiService, FileUploadService, InternalSendServiceAbstraction], - }, { provide: SyncService, useFactory: getBgService("syncService"), deps: [] }, { provide: DomainSettingsService, diff --git a/apps/browser/src/services/browser-send.service.ts b/apps/browser/src/services/browser-send.service.ts deleted file mode 100644 index 8a197444a9..0000000000 --- a/apps/browser/src/services/browser-send.service.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { BehaviorSubject } from "rxjs"; - -import { Send } from "@bitwarden/common/tools/send/models/domain/send"; -import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; -import { SendService } from "@bitwarden/common/tools/send/services/send.service"; - -import { browserSession, sessionSync } from "../platform/decorators/session-sync-observable"; - -@browserSession -export class BrowserSendService extends SendService { - @sessionSync({ initializer: Send.fromJSON, initializeAs: "array" }) - protected _sends: BehaviorSubject; - @sessionSync({ initializer: SendView.fromJSON, initializeAs: "array" }) - protected _sendViews: BehaviorSubject; -} From 14e8e34b2dc496c9617df6ec5de36d990e9e34a2 Mon Sep 17 00:00:00 2001 From: Matt Bishop Date: Wed, 27 Mar 2024 12:35:13 -0400 Subject: [PATCH 039/351] Adjust scan permissions (#8513) --- .github/workflows/scan.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml index ea9e69226a..878171cd17 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: @@ -43,7 +43,7 @@ jobs: additional_params: --report-format sarif --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 +51,9 @@ jobs: name: Quality scan runs-on: ubuntu-22.04 needs: check-run + permissions: + contents: read + pull-requests: write steps: - name: Check out repo From 64d6f6fef3a5cf4e1d505e9ac1f67862bf1b16d7 Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Wed, 27 Mar 2024 18:02:56 +0100 Subject: [PATCH 040/351] Move export.component from @bitwarden/angular to @bitwarden/vault-export-ui (#8514) Move export.component Export from @bitwarden/vault-export-ui Fix imports on browser, desktop and web Co-authored-by: Daniel James Smith --- apps/browser/src/tools/popup/settings/export.component.ts | 2 +- apps/desktop/src/app/tools/export/export.component.ts | 2 +- apps/web/src/app/tools/vault-export/export.component.ts | 2 +- .../vault-export-ui/src}/components/export.component.ts | 3 +-- libs/tools/export/vault-export/vault-export-ui/src/index.ts | 1 + 5 files changed, 5 insertions(+), 5 deletions(-) rename libs/{angular/src/tools/export => tools/export/vault-export/vault-export-ui/src}/components/export.component.ts (98%) 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/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/web/src/app/tools/vault-export/export.component.ts b/apps/web/src/app/tools/vault-export/export.component.ts index 8b14092f20..3f57f9aa71 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", 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"; From 62ad39e697a617b07a7beb21081ea4db301703a7 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Wed, 27 Mar 2024 12:03:09 -0500 Subject: [PATCH 041/351] Ps/pm 5965/better config polling (#8325) * Create tracker that can await until expected observables are received. * Test dates are almost equal * Remove unused class method * Allow for updating active account in accout service fake * Correct observable tracker behavior Clarify documentation * Transition config service to state provider Updates the config fetching behavior to be lazy and ensure that any emitted value has been updated if older than a configurable value (statically compiled). If desired, config fetching can be ensured fresh through an async. * Update calls to config service in DI and bootstrapping * Migrate account server configs * Fix global config fetching * Test migration rollback * Adhere to implementation naming convention * Adhere to abstract class naming convention * Complete config abstraction rename * Remove unnecessary cli config service * Fix builds * Validate observable does not complete * Use token service to determine authed or unauthed config pull * Remove superfluous factory config * Name describe blocks after the thing they test * Remove implementation documentation Unfortunately the experience when linking to external documentation is quite poor. Instead of following the link and retrieving docs, you get a link that can be clicked to take you out of context to the docs. No link _does_ retrieve docs, but lacks indication in the implementation that documentation exists at all. On the balance, removing the link is the better experience. * Fix storybook --- apps/browser/src/auth/popup/sso.component.ts | 4 +- .../src/auth/popup/two-factor.component.ts | 4 +- .../browser/src/background/main.background.ts | 13 +- .../src/background/runtime.background.ts | 6 +- .../config-api.service.factory.ts | 10 +- .../config-service.factory.ts | 32 +- .../services/browser-config.service.ts | 38 -- .../src/popup/services/init.service.ts | 3 - .../src/popup/services/services.module.ts | 28 +- .../src/popup/settings/about.component.ts | 4 +- .../fileless-importer.background.spec.ts | 2 +- .../fileless-importer.background.ts | 4 +- .../components/vault/add-edit.component.ts | 4 +- apps/cli/src/bw.ts | 13 +- .../platform/services/cli-config.service.ts | 9 - apps/desktop/src/app/app.component.ts | 6 +- apps/desktop/src/app/services/init.service.ts | 4 - apps/desktop/src/auth/sso.component.ts | 4 +- apps/desktop/src/auth/two-factor.component.ts | 4 +- .../src/vault/app/vault/add-edit.component.ts | 4 +- .../core/services/group/group.service.ts | 6 +- .../core/services/user-admin.service.ts | 4 +- .../layouts/organization-layout.component.ts | 2 +- .../member-dialog/member-dialog.component.ts | 4 +- .../settings/account.component.ts | 4 +- apps/web/src/app/app.component.ts | 6 +- .../user-key-rotation.service.spec.ts | 6 +- .../key-rotation/user-key-rotation.service.ts | 4 +- .../emergency-add-edit-cipher.component.ts | 4 +- apps/web/src/app/auth/sso.component.ts | 4 +- apps/web/src/app/auth/two-factor.component.ts | 4 +- .../billing/shared/add-credit.component.ts | 4 +- apps/web/src/app/core/init.service.ts | 4 - .../src/app/layouts/user-layout.component.ts | 2 +- .../collection-dialog.component.ts | 4 +- .../vault-items/vault-items.stories.ts | 4 +- .../individual-vault/add-edit.component.ts | 4 +- .../vault-onboarding.component.spec.ts | 8 +- .../vault-onboarding.component.ts | 4 +- .../vault/individual-vault/vault.component.ts | 4 +- .../app/vault/org-vault/add-edit.component.ts | 4 +- ...-collection-assignment-dialog.component.ts | 4 +- .../app/vault/org-vault/vault.component.ts | 4 +- .../providers/providers-layout.component.ts | 2 +- .../providers/setup/setup.component.ts | 2 +- .../bit-web/src/app/auth/sso/sso.component.ts | 4 +- .../src/auth/components/sso.component.spec.ts | 26 +- .../src/auth/components/sso.component.ts | 4 +- .../components/two-factor.component.spec.ts | 8 +- .../auth/components/two-factor.component.ts | 4 +- .../directives/if-feature.directive.spec.ts | 8 +- .../src/directives/if-feature.directive.ts | 4 +- .../platform/guard/feature-flag.guard.spec.ts | 8 +- .../src/platform/guard/feature-flag.guard.ts | 4 +- .../src/services/jslib-services.module.ts | 27 +- .../vault/components/add-edit.component.ts | 4 +- .../spec/matchers/to-almost-equal.spec.ts | 54 +++ libs/common/spec/matchers/to-almost-equal.ts | 20 + libs/common/spec/observable-tracker.ts | 86 +++++ .../config/config-api.service.abstraction.ts | 6 +- .../config/config.service.abstraction.ts | 30 -- .../abstractions/config/config.service.ts | 47 +++ .../abstractions/config/server-config.ts | 5 - .../platform/abstractions/state.service.ts | 9 - .../src/platform/models/domain/account.ts | 3 - .../services/config/config-api.service.ts | 12 +- .../services/config/config.service.spec.ts | 360 +++++++++++------- .../services/config/config.service.ts | 130 ------- .../services/config/default-config.service.ts | 177 +++++++++ .../src/platform/services/state.service.ts | 18 - .../src/platform/state/state-definitions.ts | 3 + libs/common/src/state-migrations/migrate.ts | 7 +- .../49-move-account-server-configs.spec.ts | 112 ++++++ .../49-move-account-server-configs.ts | 51 +++ .../src/vault/services/cipher.service.spec.ts | 4 +- .../src/vault/services/cipher.service.ts | 4 +- .../fido2/fido2-client.service.spec.ts | 6 +- .../services/fido2/fido2-client.service.ts | 4 +- libs/common/test.setup.ts | 8 + 79 files changed, 946 insertions(+), 609 deletions(-) delete mode 100644 apps/browser/src/platform/services/browser-config.service.ts delete mode 100644 apps/cli/src/platform/services/cli-config.service.ts create mode 100644 libs/common/spec/matchers/to-almost-equal.spec.ts create mode 100644 libs/common/spec/matchers/to-almost-equal.ts create mode 100644 libs/common/spec/observable-tracker.ts delete mode 100644 libs/common/src/platform/abstractions/config/config.service.abstraction.ts create mode 100644 libs/common/src/platform/abstractions/config/config.service.ts delete mode 100644 libs/common/src/platform/services/config/config.service.ts create mode 100644 libs/common/src/platform/services/config/default-config.service.ts create mode 100644 libs/common/src/state-migrations/migrations/49-move-account-server-configs.spec.ts create mode 100644 libs/common/src/state-migrations/migrations/49-move-account-server-configs.ts diff --git a/apps/browser/src/auth/popup/sso.component.ts b/apps/browser/src/auth/popup/sso.component.ts index 430bd855f1..228c7401fd 100644 --- a/apps/browser/src/auth/popup/sso.component.ts +++ b/apps/browser/src/auth/popup/sso.component.ts @@ -12,7 +12,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; 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,7 +44,7 @@ export class SsoComponent extends BaseSsoComponent { environmentService: EnvironmentService, logService: LogService, userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, - configService: ConfigServiceAbstraction, + configService: ConfigService, protected authService: AuthService, @Inject(WINDOW) private win: Window, ) { diff --git a/apps/browser/src/auth/popup/two-factor.component.ts b/apps/browser/src/auth/popup/two-factor.component.ts index da2c3482fd..94dfb5155b 100644 --- a/apps/browser/src/auth/popup/two-factor.component.ts +++ b/apps/browser/src/auth/popup/two-factor.component.ts @@ -16,7 +16,7 @@ import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor 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"; @@ -59,7 +59,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { appIdService: AppIdService, loginService: LoginService, userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, - configService: ConfigServiceAbstraction, + configService: ConfigService, ssoLoginService: SsoLoginServiceAbstraction, private dialogService: DialogService, @Inject(WINDOW) protected win: Window, diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 73c4356f69..c2c8c5be72 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -70,6 +70,7 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs import { DefaultBillingAccountProfileStateService } from "@bitwarden/common/billing/services/account/billing-account-profile-state.service"; import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service"; import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.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"; @@ -93,6 +94,7 @@ import { StateFactory } from "@bitwarden/common/platform/factories/state-factory 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 { DefaultConfigService } from "@bitwarden/common/platform/services/config/default-config.service"; import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation"; @@ -201,7 +203,6 @@ import { BrowserApi } from "../platform/browser/browser-api"; import { flagEnabled } from "../platform/flags"; import { UpdateBadge } from "../platform/listeners/update-badge"; import { BrowserStateService as StateServiceAbstraction } from "../platform/services/abstractions/browser-state.service"; -import { BrowserConfigService } from "../platform/services/browser-config.service"; import { BrowserCryptoService } from "../platform/services/browser-crypto.service"; import { BrowserEnvironmentService } from "../platform/services/browser-environment.service"; import BrowserLocalStorageService from "../platform/services/browser-local-storage.service"; @@ -293,7 +294,7 @@ export default class MainBackground { avatarService: AvatarServiceAbstraction; mainContextMenuHandler: MainContextMenuHandler; cipherContextMenuHandler: CipherContextMenuHandler; - configService: BrowserConfigService; + configService: ConfigService; configApiService: ConfigApiServiceAbstraction; devicesApiService: DevicesApiServiceAbstraction; devicesService: DevicesServiceAbstraction; @@ -609,16 +610,13 @@ export default class MainBackground { this.userVerificationApiService = new UserVerificationApiService(this.apiService); - this.configApiService = new ConfigApiService(this.apiService, this.authService); + this.configApiService = new ConfigApiService(this.apiService, this.tokenService); - this.configService = new BrowserConfigService( - this.stateService, + this.configService = new DefaultConfigService( this.configApiService, - this.authService, this.environmentService, this.logService, this.stateProvider, - true, ); this.cipherService = new CipherService( @@ -1005,7 +1003,6 @@ export default class MainBackground { this.filelessImporterBackground.init(); await this.commandsBackground.init(); - this.configService.init(); this.twoFactorService.init(); await this.overlayBackground.init(); diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts index 0a94e0a79a..dd55c14fb2 100644 --- a/apps/browser/src/background/runtime.background.ts +++ b/apps/browser/src/background/runtime.background.ts @@ -3,7 +3,7 @@ import { firstValueFrom } from "rxjs"; import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service"; import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants"; 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"; @@ -46,7 +46,7 @@ export default class RuntimeBackground { private environmentService: BrowserEnvironmentService, private messagingService: MessagingService, private logService: LogService, - private configService: ConfigServiceAbstraction, + private configService: ConfigService, private fido2Service: Fido2Service, ) { // onInstalled listener must be wired up before anything else, so we do it in the ctor @@ -136,7 +136,7 @@ export default class RuntimeBackground { await this.main.refreshBadge(); await this.main.refreshMenu(); }, 2000); - this.configService.triggerServerConfigFetch(); + await this.configService.ensureConfigFetched(); } break; case "openPopup": diff --git a/apps/browser/src/platform/background/service-factories/config-api.service.factory.ts b/apps/browser/src/platform/background/service-factories/config-api.service.factory.ts index c0dbf1f475..3d7d508832 100644 --- a/apps/browser/src/platform/background/service-factories/config-api.service.factory.ts +++ b/apps/browser/src/platform/background/service-factories/config-api.service.factory.ts @@ -2,9 +2,9 @@ import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstract import { ConfigApiService } from "@bitwarden/common/platform/services/config/config-api.service"; import { - authServiceFactory, - AuthServiceInitOptions, -} from "../../../auth/background/service-factories/auth-service.factory"; + tokenServiceFactory, + TokenServiceInitOptions, +} from "../../../auth/background/service-factories/token-service.factory"; import { apiServiceFactory, ApiServiceInitOptions } from "./api-service.factory"; import { FactoryOptions, CachedServices, factory } from "./factory-options"; @@ -13,7 +13,7 @@ type ConfigApiServiceFactoyOptions = FactoryOptions; export type ConfigApiServiceInitOptions = ConfigApiServiceFactoyOptions & ApiServiceInitOptions & - AuthServiceInitOptions; + TokenServiceInitOptions; export function configApiServiceFactory( cache: { configApiService?: ConfigApiServiceAbstraction } & CachedServices, @@ -26,7 +26,7 @@ export function configApiServiceFactory( async () => new ConfigApiService( await apiServiceFactory(cache, opts), - await authServiceFactory(cache, opts), + await tokenServiceFactory(cache, opts), ), ); } diff --git a/apps/browser/src/platform/background/service-factories/config-service.factory.ts b/apps/browser/src/platform/background/service-factories/config-service.factory.ts index 4e31fb3141..a899f8fd9a 100644 --- a/apps/browser/src/platform/background/service-factories/config-service.factory.ts +++ b/apps/browser/src/platform/background/service-factories/config-service.factory.ts @@ -1,10 +1,5 @@ -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; -import { ConfigService } from "@bitwarden/common/platform/services/config/config.service"; - -import { - authServiceFactory, - AuthServiceInitOptions, -} from "../../../auth/background/service-factories/auth-service.factory"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { DefaultConfigService } from "@bitwarden/common/platform/services/config/default-config.service"; import { configApiServiceFactory, ConfigApiServiceInitOptions } from "./config-api.service.factory"; import { @@ -13,39 +8,30 @@ import { } from "./environment-service.factory"; import { FactoryOptions, CachedServices, factory } from "./factory-options"; import { logServiceFactory, LogServiceInitOptions } from "./log-service.factory"; -import { stateProviderFactory } from "./state-provider.factory"; -import { stateServiceFactory, StateServiceInitOptions } from "./state-service.factory"; +import { stateProviderFactory, StateProviderInitOptions } from "./state-provider.factory"; -type ConfigServiceFactoryOptions = FactoryOptions & { - configServiceOptions?: { - subscribe?: boolean; - }; -}; +type ConfigServiceFactoryOptions = FactoryOptions; export type ConfigServiceInitOptions = ConfigServiceFactoryOptions & - StateServiceInitOptions & ConfigApiServiceInitOptions & - AuthServiceInitOptions & EnvironmentServiceInitOptions & - LogServiceInitOptions; + LogServiceInitOptions & + StateProviderInitOptions; export function configServiceFactory( - cache: { configService?: ConfigServiceAbstraction } & CachedServices, + cache: { configService?: ConfigService } & CachedServices, opts: ConfigServiceInitOptions, -): Promise { +): Promise { return factory( cache, "configService", opts, async () => - new ConfigService( - await stateServiceFactory(cache, opts), + new DefaultConfigService( await configApiServiceFactory(cache, opts), - await authServiceFactory(cache, opts), await environmentServiceFactory(cache, opts), await logServiceFactory(cache, opts), await stateProviderFactory(cache, opts), - opts.configServiceOptions?.subscribe ?? true, ), ); } diff --git a/apps/browser/src/platform/services/browser-config.service.ts b/apps/browser/src/platform/services/browser-config.service.ts deleted file mode 100644 index be8d087f3b..0000000000 --- a/apps/browser/src/platform/services/browser-config.service.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { ReplaySubject } from "rxjs"; - -import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; -import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction"; -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 { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { ConfigService } from "@bitwarden/common/platform/services/config/config.service"; -import { StateProvider } from "@bitwarden/common/platform/state"; - -import { browserSession, sessionSync } from "../decorators/session-sync-observable"; - -@browserSession -export class BrowserConfigService extends ConfigService { - @sessionSync({ initializer: ServerConfig.fromJSON }) - protected _serverConfig: ReplaySubject; - - constructor( - stateService: StateService, - configApiService: ConfigApiServiceAbstraction, - authService: AuthService, - environmentService: EnvironmentService, - logService: LogService, - stateProvider: StateProvider, - subscribe = false, - ) { - super( - stateService, - configApiService, - authService, - environmentService, - logService, - stateProvider, - subscribe, - ); - } -} diff --git a/apps/browser/src/popup/services/init.service.ts b/apps/browser/src/popup/services/init.service.ts index b0e80ab960..4036ace31f 100644 --- a/apps/browser/src/popup/services/init.service.ts +++ b/apps/browser/src/popup/services/init.service.ts @@ -5,7 +5,6 @@ import { AbstractThemingService } from "@bitwarden/angular/platform/services/the import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService as LogServiceAbstraction } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { ConfigService } from "@bitwarden/common/platform/services/config/config.service"; import { BrowserApi } from "../../platform/browser/browser-api"; import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; @@ -19,7 +18,6 @@ export class InitService { private stateService: StateServiceAbstraction, private logService: LogServiceAbstraction, private themingService: AbstractThemingService, - private configService: ConfigService, @Inject(DOCUMENT) private document: Document, ) {} @@ -55,7 +53,6 @@ export class InitService { this.logService.info("Force redraw is on"); } - this.configService.init(); this.setupVaultPopupHeartbeat(); }; } diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 25db6f78ca..7ab04603e4 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -46,17 +46,13 @@ import { UserNotificationSettingsService, UserNotificationSettingsServiceAbstraction, } from "@bitwarden/common/autofill/services/user-notification-settings.service"; -import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.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"; -import { - LogService, - LogService as LogServiceAbstraction, -} from "@bitwarden/common/platform/abstractions/log.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 as BaseStateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; @@ -66,7 +62,6 @@ import { } from "@bitwarden/common/platform/abstractions/storage.service"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; -import { ConfigService } from "@bitwarden/common/platform/services/config/config.service"; import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; @@ -95,7 +90,6 @@ import { Account } from "../../models/account"; import { BrowserApi } from "../../platform/browser/browser-api"; import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; import { BrowserStateService as StateServiceAbstraction } from "../../platform/services/abstractions/browser-state.service"; -import { BrowserConfigService } from "../../platform/services/browser-config.service"; import { BrowserEnvironmentService } from "../../platform/services/browser-environment.service"; import { BrowserFileDownloadService } from "../../platform/services/browser-file-download.service"; import BrowserLocalStorageService from "../../platform/services/browser-local-storage.service"; @@ -186,7 +180,7 @@ function getBgService(service: keyof MainBackground) { i18nService, ); }, - deps: [LogServiceAbstraction, I18nServiceAbstraction], + deps: [LogService, I18nServiceAbstraction], }, { provide: CipherFileUploadService, @@ -205,7 +199,7 @@ function getBgService(service: keyof MainBackground) { deps: [], }, { - provide: LogServiceAbstraction, + provide: LogService, useFactory: (platformUtilsService: PlatformUtilsService) => new ConsoleLogService(platformUtilsService.isDev()), deps: [PlatformUtilsService], @@ -367,7 +361,7 @@ function getBgService(service: keyof MainBackground) { storageService: AbstractStorageService, secureStorageService: AbstractStorageService, memoryStorageService: AbstractMemoryStorageService, - logService: LogServiceAbstraction, + logService: LogService, accountService: AccountServiceAbstraction, environmentService: EnvironmentService, tokenService: TokenService, @@ -389,7 +383,7 @@ function getBgService(service: keyof MainBackground) { AbstractStorageService, SECURE_STORAGE, MEMORY_STORAGE, - LogServiceAbstraction, + LogService, AccountServiceAbstraction, EnvironmentService, TokenService, @@ -430,18 +424,6 @@ function getBgService(service: keyof MainBackground) { }, deps: [PlatformUtilsService], }, - { - provide: ConfigService, - useClass: BrowserConfigService, - deps: [ - StateServiceAbstraction, - ConfigApiServiceAbstraction, - AuthServiceAbstraction, - EnvironmentService, - StateProvider, - LogService, - ], - }, { provide: FilePopoutUtilsService, useFactory: (platformUtilsService: PlatformUtilsService) => { diff --git a/apps/browser/src/popup/settings/about.component.ts b/apps/browser/src/popup/settings/about.component.ts index 4cabb183ae..61b5749b51 100644 --- a/apps/browser/src/popup/settings/about.component.ts +++ b/apps/browser/src/popup/settings/about.component.ts @@ -3,7 +3,7 @@ import { Component } from "@angular/core"; import { combineLatest, map } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -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 { ButtonModule, DialogModule } from "@bitwarden/components"; @@ -24,7 +24,7 @@ export class AboutComponent { ]).pipe(map(([serverConfig, isCloud]) => ({ serverConfig, isCloud }))); constructor( - private configService: ConfigServiceAbstraction, + private configService: ConfigService, private environmentService: EnvironmentService, ) {} } diff --git a/apps/browser/src/tools/background/fileless-importer.background.spec.ts b/apps/browser/src/tools/background/fileless-importer.background.spec.ts index d3436099ef..858889b887 100644 --- a/apps/browser/src/tools/background/fileless-importer.background.spec.ts +++ b/apps/browser/src/tools/background/fileless-importer.background.spec.ts @@ -4,7 +4,7 @@ import { firstValueFrom } from "rxjs"; import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AuthService } from "@bitwarden/common/auth/services/auth.service"; -import { ConfigService } from "@bitwarden/common/platform/services/config/config.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { Importer, ImportResult, ImportServiceAbstraction } from "@bitwarden/importer/core"; diff --git a/apps/browser/src/tools/background/fileless-importer.background.ts b/apps/browser/src/tools/background/fileless-importer.background.ts index 3ddc7bd1b7..57c2faa930 100644 --- a/apps/browser/src/tools/background/fileless-importer.background.ts +++ b/apps/browser/src/tools/background/fileless-importer.background.ts @@ -5,7 +5,7 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; 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 { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { ImportServiceAbstraction } from "@bitwarden/importer/core"; @@ -55,7 +55,7 @@ class FilelessImporterBackground implements FilelessImporterBackgroundInterface * @param syncService - Used to trigger a full sync after the import is completed. */ constructor( - private configService: ConfigServiceAbstraction, + private configService: ConfigService, private authService: AuthService, private policyService: PolicyService, private notificationBackground: NotificationBackground, 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..b27a986231 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, diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index e610f39954..ce2152ffbf 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -47,6 +47,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 +61,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"; @@ -131,7 +133,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"; @@ -214,7 +215,7 @@ export class Main { deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction; authRequestService: AuthRequestService; configApiService: ConfigApiServiceAbstraction; - configService: CliConfigService; + configService: ConfigService; accountService: AccountService; globalStateProvider: GlobalStateProvider; singleUserStateProvider: SingleUserStateProvider; @@ -504,16 +505,13 @@ export class Main { this.stateService, ); - 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( @@ -714,7 +712,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(); 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/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index fa396ab313..196bebfcf7 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -31,7 +31,7 @@ import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authenticatio 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"; @@ -147,7 +147,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 +265,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": diff --git a/apps/desktop/src/app/services/init.service.ts b/apps/desktop/src/app/services/init.service.ts index f45d530edd..bb7d4e7b52 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, ) {} @@ -70,8 +68,6 @@ export class InitService { const containerService = new ContainerService(this.cryptoService, this.encryptService); containerService.attachToGlobal(this.win); - - this.configService.init(); }; } } diff --git a/apps/desktop/src/auth/sso.component.ts b/apps/desktop/src/auth/sso.component.ts index 0268133192..210319b9ed 100644 --- a/apps/desktop/src/auth/sso.component.ts +++ b/apps/desktop/src/auth/sso.component.ts @@ -8,7 +8,7 @@ import { } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; 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 +38,7 @@ export class SsoComponent extends BaseSsoComponent { passwordGenerationService: PasswordGenerationServiceAbstraction, logService: LogService, userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, - configService: ConfigServiceAbstraction, + configService: ConfigService, ) { super( ssoLoginService, diff --git a/apps/desktop/src/auth/two-factor.component.ts b/apps/desktop/src/auth/two-factor.component.ts index 9b862e7c9f..8b46f3d1b9 100644 --- a/apps/desktop/src/auth/two-factor.component.ts +++ b/apps/desktop/src/auth/two-factor.component.ts @@ -16,7 +16,7 @@ import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor 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"; @@ -59,7 +59,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { loginService: LoginService, userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction, - configService: ConfigServiceAbstraction, + configService: ConfigService, @Inject(WINDOW) protected win: Window, ) { super( 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..b89beebaa6 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, 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/layouts/organization-layout.component.ts b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts index 90010160aa..1924476327 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 @@ -17,7 +17,7 @@ import { } 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"; -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"; 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..752122de00 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,7 +24,7 @@ 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"; @@ -148,7 +148,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, ) { 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..b218e680e3 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"; @@ -95,7 +95,7 @@ export class AccountComponent { private organizationApiService: OrganizationApiServiceAbstraction, private dialogService: DialogService, private formBuilder: FormBuilder, - private configService: ConfigServiceAbstraction, + private configService: ConfigService, ) {} async ngOnInit() { diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts index a1b7456627..23b45618c6 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": { 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..7eabbbb5c1 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,7 +2,7 @@ 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 { 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"; @@ -39,7 +39,7 @@ describe("KeyRotationService", () => { let mockCryptoService: MockProxy; let mockEncryptService: MockProxy; let mockStateService: MockProxy; - let mockConfigService: MockProxy; + let mockConfigService: MockProxy; beforeAll(() => { mockApiService = mock(); @@ -52,7 +52,7 @@ describe("KeyRotationService", () => { mockCryptoService = mock(); mockEncryptService = mock(); mockStateService = mock(); - mockConfigService = mock(); + mockConfigService = mock(); keyRotationService = new UserKeyRotationService( mockApiService, 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..b53c71cb2e 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 @@ -3,7 +3,7 @@ import { firstValueFrom } from "rxjs"; import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.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"; @@ -34,7 +34,7 @@ export class UserKeyRotationService { private cryptoService: CryptoService, private encryptService: EncryptService, private stateService: StateService, - private configService: ConfigServiceAbstraction, + private configService: ConfigService, ) {} /** 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/sso.component.ts b/apps/web/src/app/auth/sso.component.ts index 2ef4f3eb15..cdd979aa89 100644 --- a/apps/web/src/app/auth/sso.component.ts +++ b/apps/web/src/app/auth/sso.component.ts @@ -13,7 +13,7 @@ import { OrganizationDomainSsoDetailsResponse } from "@bitwarden/common/admin-co 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 +45,7 @@ export class SsoComponent extends BaseSsoComponent { private orgDomainApiService: OrgDomainApiServiceAbstraction, private validationService: ValidationService, userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, - configService: ConfigServiceAbstraction, + configService: ConfigService, ) { super( ssoLoginService, diff --git a/apps/web/src/app/auth/two-factor.component.ts b/apps/web/src/app/auth/two-factor.component.ts index a47a7a2848..6760ab449f 100644 --- a/apps/web/src/app/auth/two-factor.component.ts +++ b/apps/web/src/app/auth/two-factor.component.ts @@ -15,7 +15,7 @@ import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor 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"; @@ -49,7 +49,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnDest loginService: LoginService, userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction, - configService: ConfigServiceAbstraction, + configService: ConfigService, @Inject(WINDOW) protected win: Window, ) { super( 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/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/layouts/user-layout.component.ts b/apps/web/src/app/layouts/user-layout.component.ts index 2e1813697e..ee30bed0d6 100644 --- a/apps/web/src/app/layouts/user-layout.component.ts +++ b/apps/web/src/app/layouts/user-layout.component.ts @@ -9,7 +9,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; -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 { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { IconModule, LayoutComponent, NavigationModule } from "@bitwarden/components"; diff --git a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts index 357d2217e4..722ab972fc 100644 --- a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts +++ b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts @@ -18,7 +18,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; 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 { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -107,7 +107,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, private organizationUserService: OrganizationUserService, - private configService: ConfigServiceAbstraction, + private configService: ConfigService, private dialogService: DialogService, private changeDetectorRef: ChangeDetectorRef, ) { diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts b/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts index 05659de073..ad80c9f4e5 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts @@ -8,7 +8,7 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.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 { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; @@ -92,7 +92,7 @@ export default { } as Partial, }, { - provide: ConfigServiceAbstraction, + provide: ConfigService, useValue: { getFeatureFlag() { // does not currently affect any display logic, default all to OFF diff --git a/apps/web/src/app/vault/individual-vault/add-edit.component.ts b/apps/web/src/app/vault/individual-vault/add-edit.component.ts index 8332b7e95f..56f18c4a3b 100644 --- a/apps/web/src/app/vault/individual-vault/add-edit.component.ts +++ b/apps/web/src/app/vault/individual-vault/add-edit.component.ts @@ -9,7 +9,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/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 { EventType, ProductType } from "@bitwarden/common/enums"; -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"; @@ -65,7 +65,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On sendApiService: SendApiService, dialogService: DialogService, datePipe: DatePipe, - configService: ConfigServiceAbstraction, + configService: ConfigService, private billingAccountProfileStateService: BillingAccountProfileStateService, ) { super( diff --git a/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.spec.ts b/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.spec.ts index 3424717630..8967336f75 100644 --- a/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.spec.ts +++ b/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.spec.ts @@ -5,7 +5,7 @@ import { Subject, of } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -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 { StateProvider } from "@bitwarden/common/platform/state"; @@ -21,7 +21,7 @@ describe("VaultOnboardingComponent", () => { let mockApiService: Partial; let mockPolicyService: MockProxy; let mockI18nService: MockProxy; - let mockConfigService: MockProxy; + let mockConfigService: MockProxy; let mockVaultOnboardingService: MockProxy; let mockStateProvider: Partial; let setInstallExtLinkSpy: any; @@ -34,7 +34,7 @@ describe("VaultOnboardingComponent", () => { mockApiService = { getProfile: jest.fn(), }; - mockConfigService = mock(); + mockConfigService = mock(); mockVaultOnboardingService = mock(); mockStateProvider = { getActive: jest.fn().mockReturnValue( @@ -56,7 +56,7 @@ describe("VaultOnboardingComponent", () => { { provide: VaultOnboardingServiceAbstraction, useValue: mockVaultOnboardingService }, { provide: I18nService, useValue: mockI18nService }, { provide: ApiService, useValue: mockApiService }, - { provide: ConfigServiceAbstraction, useValue: mockConfigService }, + { provide: ConfigService, useValue: mockConfigService }, { provide: StateProvider, useValue: mockStateProvider }, ], }).compileComponents(); diff --git a/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts b/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts index 16f68d6111..dc3a41cf15 100644 --- a/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts @@ -17,7 +17,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli 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 } 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 { VaultOnboardingMessages } from "@bitwarden/common/vault/enums/vault-onboarding.enum"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -55,7 +55,7 @@ export class VaultOnboardingComponent implements OnInit, OnChanges, OnDestroy { protected platformUtilsService: PlatformUtilsService, protected policyService: PolicyService, private apiService: ApiService, - private configService: ConfigServiceAbstraction, + private configService: ConfigService, private vaultOnboardingService: VaultOnboardingServiceAbstraction, ) {} diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index 1dc6fdaf1c..92d6e13020 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -41,7 +41,7 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs import { EventType } from "@bitwarden/common/enums"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.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 { 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"; @@ -180,7 +180,7 @@ export class VaultComponent implements OnInit, OnDestroy { private eventCollectionService: EventCollectionService, private searchService: SearchService, private searchPipe: SearchPipe, - private configService: ConfigServiceAbstraction, + private configService: ConfigService, private apiService: ApiService, private userVerificationService: UserVerificationService, private billingAccountProfileStateService: BillingAccountProfileStateService, diff --git a/apps/web/src/app/vault/org-vault/add-edit.component.ts b/apps/web/src/app/vault/org-vault/add-edit.component.ts index ba0c65b107..c4213989c6 100644 --- a/apps/web/src/app/vault/org-vault/add-edit.component.ts +++ b/apps/web/src/app/vault/org-vault/add-edit.component.ts @@ -7,7 +7,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"; @@ -54,7 +54,7 @@ export class AddEditComponent extends BaseAddEditComponent { sendApiService: SendApiService, dialogService: DialogService, datePipe: DatePipe, - configService: ConfigServiceAbstraction, + configService: ConfigService, billingAccountProfileStateService: BillingAccountProfileStateService, ) { super( diff --git a/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/bulk-collection-assignment-dialog.component.ts b/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/bulk-collection-assignment-dialog.component.ts index 04edce8543..091c646178 100644 --- a/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/bulk-collection-assignment-dialog.component.ts +++ b/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/bulk-collection-assignment-dialog.component.ts @@ -4,7 +4,7 @@ import { Subject } from "rxjs"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherId, CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; @@ -65,7 +65,7 @@ export class BulkCollectionAssignmentDialogComponent implements OnDestroy, OnIni private cipherService: CipherService, private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, - private configService: ConfigServiceAbstraction, + private configService: ConfigService, private organizationService: OrganizationService, ) {} diff --git a/apps/web/src/app/vault/org-vault/vault.component.ts b/apps/web/src/app/vault/org-vault/vault.component.ts index 6691404b3d..028198723b 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.ts +++ b/apps/web/src/app/vault/org-vault/vault.component.ts @@ -40,7 +40,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 { 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"; @@ -184,7 +184,7 @@ export class VaultComponent implements OnInit, OnDestroy { private totpService: TotpService, private apiService: ApiService, private collectionService: CollectionService, - protected configService: ConfigServiceAbstraction, + protected configService: ConfigService, ) {} async ngOnInit() { diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.ts index 5f45379442..b8afe1c235 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.ts @@ -6,7 +6,7 @@ import { JslibModule } from "@bitwarden/angular/jslib.module"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; 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 { IconModule, LayoutComponent, NavigationModule } from "@bitwarden/components"; import { ProviderPortalLogo } from "@bitwarden/web-vault/app/admin-console/icons/provider-portal-logo"; import { PaymentMethodWarningsModule } from "@bitwarden/web-vault/app/billing/shared"; 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..b3d3112bf5 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 @@ -5,7 +5,7 @@ import { first } from "rxjs/operators"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ProviderSetupRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-setup.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"; 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/libs/angular/src/auth/components/sso.component.spec.ts b/libs/angular/src/auth/components/sso.component.spec.ts index 82650cb7f1..c5c062d9a7 100644 --- a/libs/angular/src/auth/components/sso.component.spec.ts +++ b/libs/angular/src/auth/components/sso.component.spec.ts @@ -16,7 +16,7 @@ import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/ 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 { 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"; @@ -66,7 +66,7 @@ describe("SsoComponent", () => { let mockPasswordGenerationService: MockProxy; let mockLogService: MockProxy; let mockUserDecryptionOptionsService: MockProxy; - let mockConfigService: MockProxy; + let mockConfigService: MockProxy; // Mock authService.logIn params let code: string; @@ -107,16 +107,16 @@ 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(); // Mock loginStrategyService.logIn params code = "code"; @@ -198,7 +198,7 @@ describe("SsoComponent", () => { useValue: mockUserDecryptionOptionsService, }, { provide: LogService, useValue: mockLogService }, - { provide: ConfigServiceAbstraction, useValue: mockConfigService }, + { provide: ConfigService, useValue: mockConfigService }, ], }); diff --git a/libs/angular/src/auth/components/sso.component.ts b/libs/angular/src/auth/components/sso.component.ts index f7d5504e08..68d6e72e8d 100644 --- a/libs/angular/src/auth/components/sso.component.ts +++ b/libs/angular/src/auth/components/sso.component.ts @@ -15,7 +15,7 @@ import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/ 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 +65,7 @@ export class SsoComponent { protected passwordGenerationService: PasswordGenerationServiceAbstraction, protected logService: LogService, protected userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, - protected configService: ConfigServiceAbstraction, + protected configService: ConfigService, ) {} async ngOnInit() { 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..9703c7e703 100644 --- a/libs/angular/src/auth/components/two-factor.component.spec.ts +++ b/libs/angular/src/auth/components/two-factor.component.spec.ts @@ -21,7 +21,7 @@ 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 { 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"; @@ -62,7 +62,7 @@ describe("TwoFactorComponent", () => { let mockLoginService: MockProxy; let mockUserDecryptionOptionsService: MockProxy; let mockSsoLoginService: MockProxy; - let mockConfigService: MockProxy; + let mockConfigService: MockProxy; let mockUserDecryptionOpts: { noMasterPassword: UserDecryptionOptions; @@ -92,7 +92,7 @@ describe("TwoFactorComponent", () => { mockLoginService = mock(); mockUserDecryptionOptionsService = mock(); mockSsoLoginService = mock(); - mockConfigService = mock(); + mockConfigService = mock(); mockUserDecryptionOpts = { noMasterPassword: new UserDecryptionOptions({ @@ -169,7 +169,7 @@ describe("TwoFactorComponent", () => { useValue: mockUserDecryptionOptionsService, }, { provide: SsoLoginServiceAbstraction, useValue: mockSsoLoginService }, - { provide: ConfigServiceAbstraction, useValue: mockConfigService }, + { provide: ConfigService, useValue: mockConfigService }, ], }); diff --git a/libs/angular/src/auth/components/two-factor.component.ts b/libs/angular/src/auth/components/two-factor.component.ts index 78d1c020b8..f64e591fa2 100644 --- a/libs/angular/src/auth/components/two-factor.component.ts +++ b/libs/angular/src/auth/components/two-factor.component.ts @@ -25,7 +25,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"; @@ -91,7 +91,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI protected loginService: LoginService, protected userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, protected ssoLoginService: SsoLoginServiceAbstraction, - protected configService: ConfigServiceAbstraction, + protected configService: ConfigService, ) { super(environmentService, i18nService, platformUtilsService); this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win); 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/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/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index b2aebe20f4..67d38d33de 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -111,7 +111,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 +135,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"; @@ -400,7 +400,7 @@ const typesafeProviders: Array = [ autofillSettingsService: AutofillSettingsServiceAbstraction, encryptService: EncryptService, fileUploadService: CipherFileUploadServiceAbstraction, - configService: ConfigServiceAbstraction, + configService: ConfigService, ) => new CipherService( cryptoService, @@ -424,7 +424,7 @@ const typesafeProviders: Array = [ AutofillSettingsServiceAbstraction, EncryptService, CipherFileUploadServiceAbstraction, - ConfigServiceAbstraction, + ConfigService, ], }), safeProvider({ @@ -851,25 +851,18 @@ 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, diff --git a/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts index 83131f8fc5..4f5334d176 100644 --- a/libs/angular/src/vault/components/add-edit.component.ts +++ b/libs/angular/src/vault/components/add-edit.component.ts @@ -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 }, 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/platform/abstractions/config/config-api.service.abstraction.ts b/libs/common/src/platform/abstractions/config/config-api.service.abstraction.ts index 2b25164e7c..63534becf3 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. + */ + 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/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index 514689313f..b4847279c3 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -16,7 +16,6 @@ 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"; @@ -278,14 +277,6 @@ export abstract class StateService { 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. diff --git a/libs/common/src/platform/models/domain/account.ts b/libs/common/src/platform/models/domain/account.ts index 2657467ae6..d01e9d5b8d 100644 --- a/libs/common/src/platform/models/domain/account.ts +++ b/libs/common/src/platform/models/domain/account.ts @@ -18,7 +18,6 @@ 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"; @@ -196,7 +195,6 @@ export class AccountSettings { protectedPin?: string; vaultTimeout?: number; vaultTimeoutAction?: string = "lock"; - serverConfig?: ServerConfigData; approveLoginRequests?: boolean; avatarColor?: string; trustDeviceChoiceForDecryption?: boolean; @@ -214,7 +212,6 @@ export class AccountSettings { obj?.pinProtected, EncString.fromJSON, ), - serverConfig: ServerConfigData.fromJSON(obj?.serverConfig), }); } } 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..9532b903d3 --- /dev/null +++ b/libs/common/src/platform/services/config/default-config.service.ts @@ -0,0 +1,177 @@ +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"], +}); + +// TODO MDG: When to clean these up? +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/state.service.ts b/libs/common/src/platform/services/state.service.ts index 56fb91dd52..bbcc00e562 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -32,7 +32,6 @@ 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"; @@ -1377,23 +1376,6 @@ export class StateService< ); } - 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())) diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index bf0d162eee..b44c449c21 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -73,6 +73,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"); diff --git a/libs/common/src/state-migrations/migrate.ts b/libs/common/src/state-migrations/migrate.ts index 1b057fda4d..b932a7186e 100644 --- a/libs/common/src/state-migrations/migrate.ts +++ b/libs/common/src/state-migrations/migrate.ts @@ -44,6 +44,7 @@ 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 { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key"; import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account"; @@ -52,8 +53,7 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting import { MinVersionMigrator } from "./migrations/min-version"; export const MIN_VERSION = 3; -export const CURRENT_VERSION = 48; - +export const CURRENT_VERSION = 49; export type MinVersion = typeof MIN_VERSION; export function createMigrationBuilder() { @@ -103,7 +103,8 @@ export function createMigrationBuilder() { .with(MergeEnvironmentState, 44, 45) .with(DeleteBiometricPromptCancelledData, 45, 46) .with(MoveDesktopSettingsMigrator, 46, 47) - .with(MoveDdgToStateProviderMigrator, 47, CURRENT_VERSION); + .with(MoveDdgToStateProviderMigrator, 47, 48) + .with(AccountServerConfigMigrator, 48, CURRENT_VERSION); } export async function currentVersion( 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/vault/services/cipher.service.spec.ts b/libs/common/src/vault/services/cipher.service.spec.ts index bcd4bb9836..c374724781 100644 --- a/libs/common/src/vault/services/cipher.service.spec.ts +++ b/libs/common/src/vault/services/cipher.service.spec.ts @@ -7,7 +7,7 @@ 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"; @@ -108,7 +108,7 @@ describe("Cipher Service", () => { const i18nService = mock(); const searchService = mock(); const encryptService = mock(); - const configService = mock(); + const configService = mock(); let cipherService: CipherService; let cipherObj: Cipher; diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 829ee5ed4e..4a6e96ead7 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -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"; @@ -72,7 +72,7 @@ export class CipherService implements CipherServiceAbstraction { private autofillSettingsService: AutofillSettingsServiceAbstraction, private encryptService: EncryptService, private cipherFileUploadService: CipherFileUploadService, - private configService: ConfigServiceAbstraction, + private configService: ConfigService, ) {} async getDecryptedCipherCache(): Promise { 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; - let configService!: MockProxy; + let configService!: MockProxy; let authService!: MockProxy; let vaultSettingsService: MockProxy; let domainSettingsService: MockProxy; @@ -39,7 +39,7 @@ describe("FidoAuthenticatorService", () => { beforeEach(async () => { authenticator = mock(); - configService = mock(); + configService = mock(); authService = mock(); vaultSettingsService = mock(); domainSettingsService = mock(); 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/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 { 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; } From 5de217717534494487bc748a85e67743dfddc6f2 Mon Sep 17 00:00:00 2001 From: Jake Fink Date: Wed, 27 Mar 2024 13:27:44 -0400 Subject: [PATCH 042/351] only initialize user decryption options if present on response obj (#8508) --- .../models/domain/user-decryption-options.ts | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) 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); } } From aaa745ec3681393e0dd93c5c52c1d6bab1ae30a5 Mon Sep 17 00:00:00 2001 From: Jake Fink Date: Wed, 27 Mar 2024 17:17:17 -0400 Subject: [PATCH 043/351] Return correct master password hash from login strategies (#8518) --- .../password-login.strategy.ts | 24 +++++++++++-------- .../login-strategy.service.ts | 4 ++-- 2 files changed, 16 insertions(+), 12 deletions(-) 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..427f8178e4 100644 --- a/libs/auth/src/common/login-strategies/password-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/password-login.strategy.ts @@ -63,14 +63,12 @@ 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; @@ -107,7 +105,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 +124,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(), 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..428258308a 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 @@ -137,8 +137,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; } From d9bec7f9846c8b2be8ac02bef8f8521733a46ba6 Mon Sep 17 00:00:00 2001 From: Jake Fink Date: Wed, 27 Mar 2024 17:22:56 -0400 Subject: [PATCH 044/351] send captcha bypass token on 2fa token request (#8511) --- .../password-login.strategy.ts | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) 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 427f8178e4..d3de3ea6ba 100644 --- a/libs/auth/src/common/login-strategies/password-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/password-login.strategy.ts @@ -37,15 +37,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 @@ -175,10 +171,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 From 8cdc94076e9cccda9199bf48a4b7e2fb4eabc1b7 Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Wed, 27 Mar 2024 17:46:56 -0400 Subject: [PATCH 045/351] Auth/PM-7092 - Fix CLI login via API key not working due to TokenService changes (#8499) * PM-7092 - Fix CLI login via API key not working (it apparently receives an undefined refresh token which was rejected by setTokens) * PM-7092 - Fix base login strategy tests * PM-7092 - per discucssion with jake, refactor setTokens to accept optional refresh token instead of exposing setRefreshToken as public. --- .../login-strategies/login.strategy.spec.ts | 2 +- .../common/login-strategies/login.strategy.ts | 2 +- .../src/auth/abstractions/token.service.ts | 7 +++--- .../src/auth/services/token.service.spec.ts | 24 ++++++++----------- .../common/src/auth/services/token.service.ts | 12 ++++++---- libs/common/src/services/api.service.ts | 2 +- .../vault-timeout-settings.service.ts | 2 +- 7 files changed, 26 insertions(+), 25 deletions(-) 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..42541808c8 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.spec.ts @@ -186,9 +186,9 @@ describe("LoginStrategy", () => { expect(tokenService.setTokens).toHaveBeenCalledWith( accessToken, - refreshToken, mockVaultTimeoutAction, mockVaultTimeout, + refreshToken, ); expect(stateService.addAccount).toHaveBeenCalledWith( diff --git a/libs/auth/src/common/login-strategies/login.strategy.ts b/libs/auth/src/common/login-strategies/login.strategy.ts index eef5626493..8e927c2cc4 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.ts @@ -182,9 +182,9 @@ export abstract class LoginStrategy { // 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( diff --git a/libs/common/src/auth/abstractions/token.service.ts b/libs/common/src/auth/abstractions/token.service.ts index d2358314d7..18366c5f1b 100644 --- a/libs/common/src/auth/abstractions/token.service.ts +++ b/libs/common/src/auth/abstractions/token.service.ts @@ -10,17 +10,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/services/token.service.spec.ts b/libs/common/src/auth/services/token.service.spec.ts index 63c581910a..8e8ed08853 100644 --- a/libs/common/src/auth/services/token.service.spec.ts +++ b/libs/common/src/auth/services/token.service.spec.ts @@ -991,6 +991,7 @@ describe("TokenService", () => { refreshToken, VaultTimeoutAction.Lock, null, + null, ); // Assert await expect(result).rejects.toThrow("User id not found. Cannot save refresh token."); @@ -1854,7 +1855,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, ]); @@ -1901,7 +1902,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( @@ -1933,9 +1934,9 @@ describe("TokenService", () => { // Act const result = tokenService.setTokens( accessToken, - refreshToken, vaultTimeoutAction, vaultTimeout, + refreshToken, ); // Assert @@ -1952,32 +1953,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(); }); }); diff --git a/libs/common/src/auth/services/token.service.ts b/libs/common/src/auth/services/token.service.ts index a1dc7ecf21..dd011eb40b 100644 --- a/libs/common/src/auth/services/token.service.ts +++ b/libs/common/src/auth/services/token.service.ts @@ -149,13 +149,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 @@ -166,7 +166,11 @@ 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); diff --git a/libs/common/src/services/api.service.ts b/libs/common/src/services/api.service.ts index b6c2ab5c22..6306eb1e28 100644 --- a/libs/common/src/services/api.service.ts +++ b/libs/common/src/services/api.service.ts @@ -1780,9 +1780,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/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, ]); From 5cb2e99b2fd3899a3a940fb985ed08d2d9f88178 Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Thu, 28 Mar 2024 08:08:28 +1000 Subject: [PATCH 046/351] [AC-1724] Remove BulkCollectionAccess feature flag (#8502) --- .../vault/components/vault-items/vault-items.component.ts | 2 +- apps/web/src/app/vault/individual-vault/vault.component.html | 1 - apps/web/src/app/vault/individual-vault/vault.component.ts | 5 ----- apps/web/src/app/vault/org-vault/vault.component.html | 4 +--- apps/web/src/app/vault/org-vault/vault.component.ts | 4 ---- libs/common/src/enums/feature-flag.enum.ts | 1 - 6 files changed, 2 insertions(+), 15 deletions(-) diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts index b17eed8ca1..7a8e858ba5 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts @@ -37,7 +37,7 @@ export class VaultItemsComponent { @Input() showBulkMove: boolean; @Input() showBulkTrashOptions: boolean; // Encompasses functionality only available from the organization vault context - @Input() showAdminActions: boolean; + @Input() showAdminActions = false; @Input() allOrganizations: Organization[] = []; @Input() allCollections: CollectionView[] = []; @Input() allGroups: GroupView[] = []; diff --git a/apps/web/src/app/vault/individual-vault/vault.component.html b/apps/web/src/app/vault/individual-vault/vault.component.html index b59e554f5a..5f90f8d440 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.html +++ b/apps/web/src/app/vault/individual-vault/vault.component.html @@ -50,7 +50,6 @@ [cloneableOrganizationCiphers]="false" [showAdminActions]="false" (onEvent)="onVaultItemsEvent($event)" - [showBulkEditCollectionAccess]="showBulkCollectionAccess$ | async" >

| undefined; protected canCreateCollections = false; protected currentSearchText$: Observable; - protected showBulkCollectionAccess$ = this.configService.getFeatureFlag$( - FeatureFlag.BulkCollectionAccess, - false, - ); private searchText$ = new Subject(); private refresh$ = new BehaviorSubject(null); diff --git a/apps/web/src/app/vault/org-vault/vault.component.html b/apps/web/src/app/vault/org-vault/vault.component.html index 242a03b995..4bec92b5db 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.html +++ b/apps/web/src/app/vault/org-vault/vault.component.html @@ -51,9 +51,7 @@ [cloneableOrganizationCiphers]="true" [showAdminActions]="true" (onEvent)="onVaultItemsEvent($event)" - [showBulkEditCollectionAccess]=" - (showBulkEditCollectionAccess$ | async) && organization?.flexibleCollections - " + [showBulkEditCollectionAccess]="organization?.flexibleCollections" [showBulkAddToCollections]="organization?.flexibleCollections" [viewingOrgVault]="true" > diff --git a/apps/web/src/app/vault/org-vault/vault.component.ts b/apps/web/src/app/vault/org-vault/vault.component.ts index 028198723b..a267612bd6 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.ts +++ b/apps/web/src/app/vault/org-vault/vault.component.ts @@ -143,10 +143,6 @@ export class VaultComponent implements OnInit, OnDestroy { protected currentSearchText$: Observable; protected editableCollections$: Observable; protected allCollectionsWithoutUnassigned$: Observable; - protected showBulkEditCollectionAccess$ = this.configService.getFeatureFlag$( - FeatureFlag.BulkCollectionAccess, - false, - ); private _flexibleCollectionsV1FlagEnabled: boolean; protected get flexibleCollectionsV1Enabled(): boolean { diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 8a5075e96f..ca5ccc17b5 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -2,7 +2,6 @@ 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", From b3b344866e8276a50a0ba5b2be07939f3b60716e Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Thu, 28 Mar 2024 08:28:51 +1000 Subject: [PATCH 047/351] [AC-2278] [AC-2296] Use SafeProvider in browser services module (#8418) --- .../popup/services/unauth-guard.service.ts | 3 - .../popup/services/popup-search.service.ts | 6 +- .../src/popup/services/services.module.ts | 661 +++++++++--------- .../src/platform/utils/safe-provider.ts | 67 +- .../src/services/jslib-services.module.ts | 5 +- 5 files changed, 381 insertions(+), 361 deletions(-) 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/popup/services/popup-search.service.ts b/apps/browser/src/popup/services/popup-search.service.ts index 7eea1265a2..bc5e565e6c 100644 --- a/apps/browser/src/popup/services/popup-search.service.ts +++ b/apps/browser/src/popup/services/popup-search.service.ts @@ -1,14 +1,14 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { SearchService } from "@bitwarden/common/services/search.service"; export class PopupSearchService extends SearchService { constructor( private mainSearchService: SearchService, - consoleLogService: ConsoleLogService, + logService: LogService, i18nService: I18nService, ) { - super(consoleLogService, i18nService); + super(logService, i18nService); } clearIndex() { diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 7ab04603e4..33593b56dd 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -1,15 +1,18 @@ import { APP_INITIALIZER, NgModule, NgZone } from "@angular/core"; import { DomSanitizer } from "@angular/platform-browser"; +import { Router } from "@angular/router"; import { ToastrService } from "ngx-toastr"; import { UnauthGuard as BaseUnauthGuardService } from "@bitwarden/angular/auth/guards"; import { AngularThemingService } from "@bitwarden/angular/platform/services/theming/angular-theming.service"; +import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider"; import { MEMORY_STORAGE, SECURE_STORAGE, OBSERVABLE_DISK_STORAGE, OBSERVABLE_MEMORY_STORAGE, SYSTEM_THEME_OBSERVABLE, + SafeInjectionToken, } from "@bitwarden/angular/services/injection-tokens"; import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module"; import { @@ -129,323 +132,351 @@ function getBgService(service: keyof MainBackground) { }; } +/** + * 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(DebounceNavigationService), + safeProvider(DialogService), + safeProvider(PopupCloseWarningService), + safeProvider({ + provide: APP_INITIALIZER as SafeInjectionToken<() => Promise>, + useFactory: (initService: InitService) => initService.init(), + deps: [InitService], + multi: true, + }), + safeProvider({ + provide: BaseUnauthGuardService, + useClass: UnauthGuardService, + deps: [AuthServiceAbstraction, Router], + }), + safeProvider({ + provide: MessagingService, + useFactory: () => { + return needsBackgroundInit + ? new BrowserMessagingPrivateModePopupService() + : new BrowserMessagingService(); + }, + deps: [], + }), + safeProvider({ + provide: TwoFactorService, + useFactory: getBgService("twoFactorService"), + deps: [], + }), + safeProvider({ + provide: AuthServiceAbstraction, + useFactory: getBgService("authService"), + deps: [], + }), + safeProvider({ + provide: LoginStrategyServiceAbstraction, + useFactory: getBgService("loginStrategyService"), + deps: [], + }), + safeProvider({ + provide: SsoLoginServiceAbstraction, + useFactory: getBgService("ssoLoginService"), + deps: [], + }), + safeProvider({ + provide: SearchServiceAbstraction, + useFactory: (logService: LogService, i18nService: I18nServiceAbstraction) => { + return new PopupSearchService( + getBgService("searchService")(), + logService, + i18nService, + ); + }, + deps: [LogService, I18nServiceAbstraction], + }), + safeProvider({ + provide: CipherFileUploadService, + useFactory: getBgService("cipherFileUploadService"), + deps: [], + }), + safeProvider({ + provide: CipherService, + useFactory: getBgService("cipherService"), + deps: [], + }), + safeProvider({ + provide: CryptoFunctionService, + useFactory: () => new WebCryptoFunctionService(window), + deps: [], + }), + safeProvider({ + provide: CollectionService, + useFactory: getBgService("collectionService"), + deps: [], + }), + safeProvider({ + provide: LogService, + useFactory: (platformUtilsService: PlatformUtilsService) => + new ConsoleLogService(platformUtilsService.isDev()), + deps: [PlatformUtilsService], + }), + safeProvider({ + provide: EnvironmentService, + useExisting: BrowserEnvironmentService, + }), + safeProvider({ + provide: BrowserEnvironmentService, + useClass: BrowserEnvironmentService, + deps: [LogService, StateProvider, AccountServiceAbstraction], + }), + safeProvider({ + provide: TotpService, + useFactory: getBgService("totpService"), + deps: [], + }), + safeProvider({ + provide: I18nServiceAbstraction, + useFactory: (globalStateProvider: GlobalStateProvider) => { + return new I18nService(BrowserApi.getUILanguage(), globalStateProvider); + }, + deps: [GlobalStateProvider], + }), + safeProvider({ + provide: CryptoService, + useFactory: (encryptService: EncryptService) => { + const cryptoService = getBgService("cryptoService")(); + new ContainerService(cryptoService, encryptService).attachToGlobal(self); + return cryptoService; + }, + deps: [EncryptService], + }), + safeProvider({ + provide: AuthRequestServiceAbstraction, + useFactory: getBgService("authRequestService"), + deps: [], + }), + safeProvider({ + provide: DeviceTrustCryptoServiceAbstraction, + useFactory: getBgService("deviceTrustCryptoService"), + deps: [], + }), + safeProvider({ + provide: DevicesServiceAbstraction, + useFactory: getBgService("devicesService"), + deps: [], + }), + safeProvider({ + provide: PlatformUtilsService, + useExisting: ForegroundPlatformUtilsService, + }), + safeProvider({ + provide: ForegroundPlatformUtilsService, + useClass: ForegroundPlatformUtilsService, + useFactory: (sanitizer: DomSanitizer, toastrService: ToastrService) => { + return new ForegroundPlatformUtilsService( + sanitizer, + toastrService, + (clipboardValue: string, clearMs: number) => { + void BrowserApi.sendMessage("clearClipboard", { clipboardValue, clearMs }); + }, + async () => { + const response = await BrowserApi.sendMessageWithResponse<{ + result: boolean; + error: string; + }>("biometricUnlock"); + if (!response.result) { + throw response.error; + } + return response.result; + }, + window, + ); + }, + deps: [DomSanitizer, ToastrService], + }), + safeProvider({ + provide: PasswordGenerationServiceAbstraction, + useFactory: getBgService("passwordGenerationService"), + deps: [], + }), + safeProvider({ + provide: SyncService, + useFactory: getBgService("syncService"), + deps: [], + }), + safeProvider({ + provide: DomainSettingsService, + useClass: DefaultDomainSettingsService, + deps: [StateProvider], + }), + safeProvider({ + provide: AbstractStorageService, + useClass: BrowserLocalStorageService, + deps: [], + }), + safeProvider({ + provide: AutofillService, + useFactory: getBgService("autofillService"), + deps: [], + }), + safeProvider({ + provide: VaultExportServiceAbstraction, + useFactory: getBgService("exportService"), + deps: [], + }), + safeProvider({ + provide: KeyConnectorService, + useFactory: getBgService("keyConnectorService"), + deps: [], + }), + safeProvider({ + provide: UserVerificationService, + useFactory: getBgService("userVerificationService"), + deps: [], + }), + safeProvider({ + provide: VaultTimeoutSettingsService, + useFactory: getBgService("vaultTimeoutSettingsService"), + deps: [], + }), + safeProvider({ + provide: VaultTimeoutService, + useFactory: getBgService("vaultTimeoutService"), + deps: [], + }), + safeProvider({ + provide: NotificationsService, + useFactory: getBgService("notificationsService"), + deps: [], + }), + safeProvider({ + provide: VaultFilterService, + useClass: VaultFilterService, + deps: [ + OrganizationService, + FolderServiceAbstraction, + CipherService, + CollectionService, + PolicyService, + StateProvider, + AccountServiceAbstraction, + ], + }), + safeProvider({ + provide: SECURE_STORAGE, + useExisting: AbstractStorageService, // Secure storage is not available in the browser, so we use normal storage instead and warn users when it is used. + }), + safeProvider({ + provide: MEMORY_STORAGE, + useFactory: getBgService("memoryStorageService"), + deps: [], + }), + safeProvider({ + provide: OBSERVABLE_MEMORY_STORAGE, + useClass: ForegroundMemoryStorageService, + deps: [], + }), + safeProvider({ + provide: OBSERVABLE_DISK_STORAGE, + useExisting: AbstractStorageService, + }), + safeProvider({ + provide: StateServiceAbstraction, + useFactory: ( + storageService: AbstractStorageService, + secureStorageService: AbstractStorageService, + memoryStorageService: AbstractMemoryStorageService, + logService: LogService, + accountService: AccountServiceAbstraction, + environmentService: EnvironmentService, + tokenService: TokenService, + migrationRunner: MigrationRunner, + ) => { + return new BrowserStateService( + storageService, + secureStorageService, + memoryStorageService, + logService, + new StateFactory(GlobalState, Account), + accountService, + environmentService, + tokenService, + migrationRunner, + ); + }, + deps: [ + AbstractStorageService, + SECURE_STORAGE, + MEMORY_STORAGE, + LogService, + AccountServiceAbstraction, + EnvironmentService, + TokenService, + MigrationRunner, + ], + }), + safeProvider({ + provide: UsernameGenerationServiceAbstraction, + useFactory: getBgService("usernameGenerationService"), + deps: [], + }), + safeProvider({ + provide: BaseStateServiceAbstraction, + useExisting: StateServiceAbstraction, + deps: [], + }), + safeProvider({ + provide: FileDownloadService, + useClass: BrowserFileDownloadService, + deps: [], + }), + safeProvider({ + provide: LoginServiceAbstraction, + useClass: LoginService, + deps: [StateServiceAbstraction], + }), + safeProvider({ + provide: SYSTEM_THEME_OBSERVABLE, + useFactory: (platformUtilsService: PlatformUtilsService) => { + // Safari doesn't properly handle the (prefers-color-scheme) media query in the popup window, it always returns light. + // In Safari, we have to use the background page instead, which comes with limitations like not dynamically changing the extension theme when the system theme is changed. + let windowContext = window; + const backgroundWindow = BrowserApi.getBackgroundPage(); + if (platformUtilsService.isSafari() && backgroundWindow) { + windowContext = backgroundWindow; + } + + return AngularThemingService.createSystemThemeFromWindow(windowContext); + }, + deps: [PlatformUtilsService], + }), + safeProvider({ + provide: FilePopoutUtilsService, + useFactory: (platformUtilsService: PlatformUtilsService) => { + return new FilePopoutUtilsService(platformUtilsService); + }, + deps: [PlatformUtilsService], + }), + safeProvider({ + provide: DerivedStateProvider, + useClass: ForegroundDerivedStateProvider, + deps: [OBSERVABLE_MEMORY_STORAGE, NgZone], + }), + safeProvider({ + provide: AutofillSettingsServiceAbstraction, + useClass: AutofillSettingsService, + deps: [StateProvider, PolicyService], + }), + safeProvider({ + provide: UserNotificationSettingsServiceAbstraction, + useClass: UserNotificationSettingsService, + deps: [StateProvider], + }), +]; + @NgModule({ imports: [JslibServicesModule], declarations: [], - providers: [ - InitService, - DebounceNavigationService, - DialogService, - PopupCloseWarningService, - { - provide: APP_INITIALIZER, - useFactory: (initService: InitService) => initService.init(), - deps: [InitService], - multi: true, - }, - { provide: BaseUnauthGuardService, useClass: UnauthGuardService }, - { - provide: MessagingService, - useFactory: () => { - return needsBackgroundInit - ? new BrowserMessagingPrivateModePopupService() - : new BrowserMessagingService(); - }, - }, - { - provide: TwoFactorService, - useFactory: getBgService("twoFactorService"), - deps: [], - }, - { - provide: AuthServiceAbstraction, - useFactory: getBgService("authService"), - deps: [], - }, - { - provide: LoginStrategyServiceAbstraction, - useFactory: getBgService("loginStrategyService"), - }, - { - provide: SsoLoginServiceAbstraction, - useFactory: getBgService("ssoLoginService"), - deps: [], - }, - { - provide: SearchServiceAbstraction, - useFactory: (logService: ConsoleLogService, i18nService: I18nServiceAbstraction) => { - return new PopupSearchService( - getBgService("searchService")(), - logService, - i18nService, - ); - }, - deps: [LogService, I18nServiceAbstraction], - }, - { - provide: CipherFileUploadService, - useFactory: getBgService("cipherFileUploadService"), - deps: [], - }, - { provide: CipherService, useFactory: getBgService("cipherService"), deps: [] }, - { - provide: CryptoFunctionService, - useFactory: () => new WebCryptoFunctionService(window), - deps: [], - }, - { - provide: CollectionService, - useFactory: getBgService("collectionService"), - deps: [], - }, - { - provide: LogService, - useFactory: (platformUtilsService: PlatformUtilsService) => - new ConsoleLogService(platformUtilsService.isDev()), - deps: [PlatformUtilsService], - }, - { - provide: BrowserEnvironmentService, - useClass: BrowserEnvironmentService, - deps: [LogService, StateProvider, AccountServiceAbstraction], - }, - { - provide: EnvironmentService, - useExisting: BrowserEnvironmentService, - }, - { provide: TotpService, useFactory: getBgService("totpService"), deps: [] }, - { - provide: I18nServiceAbstraction, - useFactory: (globalStateProvider: GlobalStateProvider) => { - return new I18nService(BrowserApi.getUILanguage(), globalStateProvider); - }, - deps: [GlobalStateProvider], - }, - { - provide: CryptoService, - useFactory: (encryptService: EncryptService) => { - const cryptoService = getBgService("cryptoService")(); - new ContainerService(cryptoService, encryptService).attachToGlobal(self); - return cryptoService; - }, - deps: [EncryptService], - }, - { - provide: AuthRequestServiceAbstraction, - useFactory: getBgService("authRequestService"), - deps: [], - }, - { - provide: DeviceTrustCryptoServiceAbstraction, - useFactory: getBgService("deviceTrustCryptoService"), - deps: [], - }, - { - provide: DevicesServiceAbstraction, - useFactory: getBgService("devicesService"), - deps: [], - }, - { - provide: PlatformUtilsService, - useExisting: ForegroundPlatformUtilsService, - }, - { - provide: ForegroundPlatformUtilsService, - useClass: ForegroundPlatformUtilsService, - useFactory: (sanitizer: DomSanitizer, toastrService: ToastrService) => { - return new ForegroundPlatformUtilsService( - sanitizer, - toastrService, - (clipboardValue: string, clearMs: number) => { - void BrowserApi.sendMessage("clearClipboard", { clipboardValue, clearMs }); - }, - async () => { - const response = await BrowserApi.sendMessageWithResponse<{ - result: boolean; - error: string; - }>("biometricUnlock"); - if (!response.result) { - throw response.error; - } - return response.result; - }, - window, - ); - }, - deps: [DomSanitizer, ToastrService], - }, - { - provide: PasswordGenerationServiceAbstraction, - useFactory: getBgService("passwordGenerationService"), - deps: [], - }, - { provide: SyncService, useFactory: getBgService("syncService"), deps: [] }, - { - provide: DomainSettingsService, - useClass: DefaultDomainSettingsService, - deps: [StateProvider], - }, - { - provide: AbstractStorageService, - useClass: BrowserLocalStorageService, - deps: [], - }, - { - provide: AutofillService, - useFactory: getBgService("autofillService"), - deps: [], - }, - { - provide: VaultExportServiceAbstraction, - useFactory: getBgService("exportService"), - deps: [], - }, - { - provide: KeyConnectorService, - useFactory: getBgService("keyConnectorService"), - deps: [], - }, - { - provide: UserVerificationService, - useFactory: getBgService("userVerificationService"), - deps: [], - }, - { - provide: VaultTimeoutSettingsService, - useFactory: getBgService("vaultTimeoutSettingsService"), - deps: [], - }, - { - provide: VaultTimeoutService, - useFactory: getBgService("vaultTimeoutService"), - deps: [], - }, - { - provide: NotificationsService, - useFactory: getBgService("notificationsService"), - deps: [], - }, - { - provide: VaultFilterService, - useClass: VaultFilterService, - deps: [ - OrganizationService, - FolderServiceAbstraction, - CipherService, - CollectionService, - PolicyService, - StateProvider, - AccountServiceAbstraction, - ], - }, - { - provide: SECURE_STORAGE, - useExisting: AbstractStorageService, // Secure storage is not available in the browser, so we use normal storage instead and warn users when it is used. - }, - { - provide: MEMORY_STORAGE, - useFactory: getBgService("memoryStorageService"), - }, - { - provide: OBSERVABLE_MEMORY_STORAGE, - useClass: ForegroundMemoryStorageService, - deps: [], - }, - { - provide: OBSERVABLE_DISK_STORAGE, - useExisting: AbstractStorageService, - }, - { - provide: StateServiceAbstraction, - useFactory: ( - storageService: AbstractStorageService, - secureStorageService: AbstractStorageService, - memoryStorageService: AbstractMemoryStorageService, - logService: LogService, - accountService: AccountServiceAbstraction, - environmentService: EnvironmentService, - tokenService: TokenService, - migrationRunner: MigrationRunner, - ) => { - return new BrowserStateService( - storageService, - secureStorageService, - memoryStorageService, - logService, - new StateFactory(GlobalState, Account), - accountService, - environmentService, - tokenService, - migrationRunner, - ); - }, - deps: [ - AbstractStorageService, - SECURE_STORAGE, - MEMORY_STORAGE, - LogService, - AccountServiceAbstraction, - EnvironmentService, - TokenService, - MigrationRunner, - ], - }, - { - provide: UsernameGenerationServiceAbstraction, - useFactory: getBgService("usernameGenerationService"), - deps: [], - }, - { - provide: BaseStateServiceAbstraction, - useExisting: StateServiceAbstraction, - deps: [], - }, - { - provide: FileDownloadService, - useClass: BrowserFileDownloadService, - }, - { - provide: LoginServiceAbstraction, - useClass: LoginService, - deps: [StateServiceAbstraction], - }, - { - provide: SYSTEM_THEME_OBSERVABLE, - useFactory: (platformUtilsService: PlatformUtilsService) => { - // Safari doesn't properly handle the (prefers-color-scheme) media query in the popup window, it always returns light. - // In Safari, we have to use the background page instead, which comes with limitations like not dynamically changing the extension theme when the system theme is changed. - let windowContext = window; - const backgroundWindow = BrowserApi.getBackgroundPage(); - if (platformUtilsService.isSafari() && backgroundWindow) { - windowContext = backgroundWindow; - } - - return AngularThemingService.createSystemThemeFromWindow(windowContext); - }, - deps: [PlatformUtilsService], - }, - { - provide: FilePopoutUtilsService, - useFactory: (platformUtilsService: PlatformUtilsService) => { - return new FilePopoutUtilsService(platformUtilsService); - }, - deps: [PlatformUtilsService], - }, - { - provide: DerivedStateProvider, - useClass: ForegroundDerivedStateProvider, - deps: [OBSERVABLE_MEMORY_STORAGE, NgZone], - }, - { - provide: AutofillSettingsServiceAbstraction, - useClass: AutofillSettingsService, - deps: [StateProvider, PolicyService], - }, - { - provide: UserNotificationSettingsServiceAbstraction, - useClass: UserNotificationSettingsService, - 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/libs/angular/src/platform/utils/safe-provider.ts b/libs/angular/src/platform/utils/safe-provider.ts index 65ce49cda9..4ab1d2ae2a 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,37 +50,25 @@ 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; @@ -84,31 +82,26 @@ type SafeExistingProvider< */ 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>, >( provider: | SafeClassProvider | SafeValueProvider - | SafeFactoryProviderWithToken - | SafeFactoryProviderWithClass + | SafeFactoryProvider | SafeExistingProvider | Constructor, ): SafeProvider => provider as SafeProvider; diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 67d38d33de..521387181b 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 { AuthRequestServiceAbstraction, @@ -267,7 +266,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), @@ -1085,6 +1084,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 {} From d10c14791d8905251e28fb47ae3986c36193e1f6 Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Thu, 28 Mar 2024 08:44:08 +1000 Subject: [PATCH 048/351] [AC-2329] [BEEEP] Use safeProvider in desktop services module (#8457) --- .../src/app/services/services.module.ts | 328 ++++++++++-------- .../native-message-handler.service.ts | 2 +- .../src/platform/utils/safe-provider.ts | 15 + 3 files changed, 198 insertions(+), 147 deletions(-) diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index 495d6abcf1..1d75ff4ca9 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,6 +12,8 @@ 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"; @@ -77,153 +79,187 @@ 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: LoginServiceAbstraction, + useClass: LoginService, + deps: [StateServiceAbstraction], + }), + safeProvider({ + provide: CryptoFunctionServiceAbstraction, + useClass: RendererCryptoFunctionService, + deps: [WINDOW], + }), + safeProvider({ + provide: CryptoServiceAbstraction, + useClass: ElectronCryptoService, + deps: [ + 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], - }, - { - provide: DesktopAutofillSettingsService, - useClass: DesktopAutofillSettingsService, - 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/services/native-message-handler.service.ts b/apps/desktop/src/services/native-message-handler.service.ts index 785b65195a..ebe1ee6248 100644 --- a/apps/desktop/src/services/native-message-handler.service.ts +++ b/apps/desktop/src/services/native-message-handler.service.ts @@ -5,10 +5,10 @@ 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"; diff --git a/libs/angular/src/platform/utils/safe-provider.ts b/libs/angular/src/platform/utils/safe-provider.ts index 4ab1d2ae2a..7c19a280d6 100644 --- a/libs/angular/src/platform/utils/safe-provider.ts +++ b/libs/angular/src/platform/utils/safe-provider.ts @@ -74,6 +74,17 @@ type SafeExistingProvider< 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; +}; + /** * 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. @@ -97,11 +108,15 @@ export const safeProvider = < IExisting extends | Constructor> | AbstractConstructor>, + // types for no token + IConcrete extends Constructor, + DConcrete extends MapParametersToDeps>, >( provider: | SafeClassProvider | SafeValueProvider | SafeFactoryProvider | SafeExistingProvider + | SafeConcreteProvider | Constructor, ): SafeProvider => provider as SafeProvider; From 2edc156dd64544fb13b420d22e1a0151e491aaf8 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Thu, 28 Mar 2024 12:01:09 +0100 Subject: [PATCH 049/351] [STRICT TS] Migrate platform abstract services functions (#8527) We currently use a callback syntax for abstract services. This syntax isn't completely strict compliant and will fail the strictPropertyInitialization check. We also currently don't get any compile time errors if we forget to implement a function. To that end this PR updates all platform owned services to use the appropriate abstract keyword for non implemented functions. I also updated the fields to be actual functions and not properties. --- .../biometrics.service.abstraction.ts | 32 ++-- .../form-validation-errors.service.ts | 2 +- .../theming/theming.service.abstraction.ts | 4 +- .../platform/abstractions/app-id.service.ts | 8 +- .../abstractions/broadcaster.service.ts | 6 +- .../config/config-api.service.abstraction.ts | 2 +- .../abstractions/crypto-function.service.ts | 62 +++---- .../platform/abstractions/crypto.service.ts | 161 ++++++++++-------- .../platform/abstractions/encrypt.service.ts | 23 +-- .../file-download/file-download.service.ts | 2 +- .../file-upload/file-upload.service.ts | 4 +- .../src/platform/abstractions/i18n.service.ts | 4 +- .../abstractions/key-generation.service.ts | 14 +- .../src/platform/abstractions/log.service.ts | 10 +- .../abstractions/messaging.service.ts | 2 +- .../abstractions/platform-utils.service.ts | 52 +++--- .../platform/abstractions/system.service.ts | 8 +- .../abstractions/translation.service.ts | 12 +- .../abstractions/validation.service.ts | 2 +- .../biometrics/biometric-state.service.ts | 14 +- .../platform/state/derived-state.provider.ts | 4 +- .../platform/state/global-state.provider.ts | 2 +- .../src/platform/state/state.provider.ts | 8 +- .../src/platform/state/user-state.provider.ts | 2 +- .../platform/theming/theme-state.service.ts | 4 +- 25 files changed, 232 insertions(+), 212 deletions(-) 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/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/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/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 63534becf3..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 @@ -5,5 +5,5 @@ export abstract class ConfigApiServiceAbstraction { /** * Fetches the server configuration for the given user. If no user is provided, the configuration will not contain user-specific context. */ - get: (userId: UserId | undefined) => Promise; + abstract get(userId: UserId | undefined): Promise; } 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..44ff521680 100644 --- a/libs/common/src/platform/abstractions/crypto.service.ts +++ b/libs/common/src/platform/abstractions/crypto.service.ts @@ -12,7 +12,7 @@ import { EncString } from "../models/domain/enc-string"; import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; export abstract class CryptoService { - activeUserKey$: Observable; + abstract activeUserKey$: Observable; /** * Sets the provided user key and stores * any other necessary versions (such as auto, biometrics, @@ -22,105 +22,105 @@ export abstract class CryptoService { * @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]>; + abstract 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 clearUserKey(clearSecretStorage?: boolean, userId?: string): Promise; /** * 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; + abstract 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; + abstract setMasterKey(key: MasterKey, userId?: string): Promise; /** * @param userId The desired user * @returns The user's master key */ - getMasterKey: (userId?: string) => Promise; + abstract getMasterKey(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 +129,17 @@ 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; + ): Promise; /** * Clears the user's master key * @param userId The desired user */ - clearMasterKey: (userId?: string) => Promise; + abstract clearMasterKey(userId?: string): Promise; /** * Encrypts the existing (or provided) user key with the * provided master key @@ -147,10 +147,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 +158,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 +172,25 @@ 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; + abstract 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; + abstract setMasterKeyHash(keyHash: string): Promise; /** * @returns The user's master password hash */ - getMasterKeyHash: () => Promise; + abstract getMasterKeyHash(): Promise; /** * Clears the user's stored master password hash * @param userId The desired user */ - clearMasterKeyHash: (userId?: string) => Promise; + abstract clearMasterKeyHash(userId?: string): Promise; /** * Compares the provided master password to the stored password hash and server password hash. * Updates the stored hash if outdated. @@ -195,107 +199,109 @@ 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]>; + abstract 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 clearOrgKeys(memoryOnly?: boolean, userId?: string): Promise; /** * 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>; + abstract getProviderKeys(): Promise>; /** * @param memoryOnly Clear only the in-memory keys * @param userId The desired user */ - clearProviderKeys: (memoryOnly?: boolean, userId?: string) => Promise; + abstract clearProviderKeys(memoryOnly?: boolean, userId?: string): 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]>; + abstract 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 clearKeyPair(memoryOnly?: boolean, userId?: string): Promise; /** * @param pin The user's pin * @param salt The user's salt @@ -303,14 +309,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 +332,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 +351,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 +411,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 +430,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/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/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/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-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/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", { From 0fbe64e5b95ebdc334be82ad430d1f6bdad96328 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 28 Mar 2024 11:06:01 +0000 Subject: [PATCH 050/351] Autosync the updated translations (#8526) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/browser/src/_locales/id/messages.json | 2 +- apps/browser/src/_locales/pt_PT/messages.json | 2 +- apps/browser/src/_locales/zh_CN/messages.json | 16 ++++++++-------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index 1b3e7d4e36..44f6be8cef 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" diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index a532ac5029..117c5be6b4 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" diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index fec2dcc2d9..1c269640c8 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": { From f30116b34dec0f2bd3dd3a5ce2d079f5eb888441 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 28 Mar 2024 11:06:17 +0000 Subject: [PATCH 051/351] Autosync the updated translations (#8525) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/desktop/src/locales/el/messages.json | 202 +++++++++---------- apps/desktop/src/locales/zh_CN/messages.json | 2 +- 2 files changed, 102 insertions(+), 102 deletions(-) diff --git a/apps/desktop/src/locales/el/messages.json b/apps/desktop/src/locales/el/messages.json index 6d6fcaae45..f5e18bdb85 100644 --- a/apps/desktop/src/locales/el/messages.json +++ b/apps/desktop/src/locales/el/messages.json @@ -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" @@ -1554,7 +1554,7 @@ "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,22 +2532,22 @@ } }, "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." @@ -2633,68 +2633,68 @@ "message": "Multifactor authentication failed" }, "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": "Troubleshooting" + "message": "Αντιμετώπιση Προβλημάτων" }, "disableHardwareAccelerationRestart": { - "message": "Disable hardware acceleration and restart" + "message": "Απενεργοποίηση επιτάχυνσης υλικού και επανεκκίνηση" }, "enableHardwareAccelerationRestart": { - "message": "Enable hardware acceleration and restart" + "message": "Ενεργοποίηση επιτάχυνσης υλικού και επανεκκίνηση" } } diff --git a/apps/desktop/src/locales/zh_CN/messages.json b/apps/desktop/src/locales/zh_CN/messages.json index 84ffcecbb8..617e8dd934 100644 --- a/apps/desktop/src/locales/zh_CN/messages.json +++ b/apps/desktop/src/locales/zh_CN/messages.json @@ -2685,7 +2685,7 @@ "message": "将与您的 LastPass 账户关联的 YubiKey 插入计算机的 USB 端口,然后触摸其按钮。" }, "commonImportFormats": { - "message": "通用格式", + "message": "常规格式", "description": "Label indicating the most common import formats" }, "troubleshooting": { From ddae908d86fe3126bf5b33cb4023a81551092de4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 28 Mar 2024 11:06:25 +0000 Subject: [PATCH 052/351] Autosync the updated translations (#8524) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/web/src/locales/nl/messages.json | 64 ++++++++++++------------ apps/web/src/locales/zh_CN/messages.json | 26 +++++----- 2 files changed, 45 insertions(+), 45 deletions(-) diff --git a/apps/web/src/locales/nl/messages.json b/apps/web/src/locales/nl/messages.json index 0b99351080..ecdde73f68 100644 --- a/apps/web/src/locales/nl/messages.json +++ b/apps/web/src/locales/nl/messages.json @@ -579,7 +579,7 @@ "message": "Toegang" }, "accessLevel": { - "message": "Access level" + "message": "Toegangsniveau" }, "loggedOut": { "message": "Uitgelogd" @@ -660,13 +660,13 @@ "message": "Geef je passkey een naam om deze later te herkennen." }, "useForVaultEncryption": { - "message": "Use for vault encryption" + "message": "Gebruik voor kluis versleuteling" }, "useForVaultEncryptionInfo": { - "message": "Log in and unlock on supported devices without your master password. Follow the prompts from your browser to finalize setup." + "message": "Meld aan en ontgrendel op ondersteunde apparaten zonder uw hoofdwachtwoord. Volg de aanwijzingen in uw browser om dit te activeren." }, "useForVaultEncryptionErrorReadingPasskey": { - "message": "Error reading passkey. Try again or uncheck this option." + "message": "Fout bij lezen van passkey. Probeer het opnieuw of selecteer een andere optie." }, "encryptionNotSupported": { "message": "Encryptie niet ondersteund" @@ -2036,7 +2036,7 @@ "message": "1 GB versleutelde opslag voor bijlagen." }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "Gepatenteerde 2 stap login opties zoals YubiKey en Duo." }, "premiumSignUpEmergency": { "message": "Noodtoegang" @@ -3621,7 +3621,7 @@ "message": "Encryptiesleutel bijwerken" }, "updateEncryptionSchemeDesc": { - "message": "We've changed the encryption scheme to provide better security. Update your encryption key now by entering your master password below." + "message": "Wij hebben de versleutelings- methode aangepast om betere beveiliging te kunnen leveren. Voer uw hoofdwachtwoord in om dit door te voeren." }, "updateEncryptionKeyWarning": { "message": "Na het bijwerken van je encryptiesleutel moet je je afmelden en weer aanmelden bij alle Bitwarden-applicaties die je gebruikt (zoals de mobiele app of browserextensies). Als je niet opnieuw inlogt (wat je nieuwe encryptiesleutel downloadt), kan dit gegevensbeschadiging tot gevolg hebben. We proberen je automatisch uit te loggen, maar het kan zijn dat dit met enige vertraging gebeurt." @@ -3781,7 +3781,7 @@ "message": "Dit item heeft oude bestandsbijlagen die aangepast moeten worden." }, "attachmentFixDescription": { - "message": "This attachment uses outdated encryption. Select 'Fix' to download, re-encrypt, and re-upload the attachment." + "message": "Deze bijlage maakt gebruik van verouderde versleuteling. Selecteer \"oplossen\" om het bestand te downloaden, opnieuw te versleutelen en vervolgens opnieuw te uploaden." }, "fix": { "message": "Oplossen", @@ -4044,10 +4044,10 @@ "message": "Je kunt dit tabblad nu sluiten en doorgaan in de extensie." }, "youSuccessfullyLoggedIn": { - "message": "You successfully logged in" + "message": "U bent succesvol ingelogd" }, "thisWindowWillCloseIn5Seconds": { - "message": "This window will automatically close in 5 seconds" + "message": "Dit scherm sluit automatisch over 5 seconden" }, "includeAllTeamsFeatures": { "message": "Alle functionaliteit van Teams plus:" @@ -4776,13 +4776,13 @@ "message": "Accountherstel-administratie" }, "accountRecoveryPolicyDesc": { - "message": "Based on the encryption method, recover accounts when master passwords or trusted devices are forgotten or lost." + "message": "Gebaseerd op de huidige versleutelings- methode, herstel accounts wanneer hoofdwachtwoorden of vertrouwde apparaten vergeten of vermist zijn." }, "accountRecoveryPolicyWarning": { - "message": "Existing accounts with master passwords will require members to self-enroll before administrators can recover their accounts. Automatic enrollment will turn on account recovery for new members." + "message": "Bestaande accounts met hoofdwachtwoorden vereisen gebruikers om zelf in te schrijven voordat administratoren hun accounts kunnen herstellen. Automatische inschrijving zal automatisch account herstel inschakelen voor nieuwe gebruikers." }, "accountRecoverySingleOrgRequirementDesc": { - "message": "The single organization Enterprise policy must be turned on before activating this policy." + "message": "Het enkele organisatie beleid moet aangezet zijn voordat dit beleid geactiveerd kan worden." }, "resetPasswordPolicyAutoEnroll": { "message": "Automatische inschrijving" @@ -5114,7 +5114,7 @@ "message": "Automatisch invullen activeren" }, "activateAutofillPolicyDesc": { - "message": "Activate the auto-fill on page load setting on the browser extension for all existing and new members." + "message": "Activeer de automatisch invullen wanneer de pagina geladen is instelling in de browser extensie voor bestaande en nieuwe gebruikers." }, "experimentalFeature": { "message": "Gehackte of onbetrouwbare websites kunnen automatisch invullen bij laden van pagina misbruiken." @@ -5412,7 +5412,7 @@ "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Use the require single-sign-on authentication policy to require all members to log in with SSO.'" }, "ssoPolicyHelpAnchor": { - "message": "require single sign-on authentication policy", + "message": "vereis single sign-on authenticatie beleid", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Use the require single-sign-on authentication policy to require all members to log in with SSO.'" }, "ssoPolicyHelpEnd": { @@ -5429,15 +5429,15 @@ "message": "Key Connector" }, "memberDecryptionKeyConnectorDescStart": { - "message": "Connect login with SSO to your self-hosted decryption key server. Using this option, members won’t need to use their master passwords to decrypt vault data. The", + "message": "Verbind inloggen met SSO naar je zelf beheerde Decoderingsserver. Door deze optie te gebruiken hoeven gebruikers niet hun hoofdwachtwoord te gebruiken om kluis data te decoderen. Het", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Connect login with SSO to your self-hosted decryption key server. Using this option, members won’t need to use their master passwords to decrypt vault data. The require SSO authentication and single organization policies are required to set up Key Connector decryption. Contact Bitwarden Support for set up assistance.'" }, "memberDecryptionKeyConnectorDescLink": { - "message": "require SSO authentication and single organization policies", + "message": "vereis het SSO authenticatie beleid en het enkele organisatie beleid", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Connect login with SSO to your self-hosted decryption key server. Using this option, members won’t need to use their master passwords to decrypt vault data. The require SSO authentication and single organization policies are required to set up Key Connector decryption. Contact Bitwarden Support for set up assistance.'" }, "memberDecryptionKeyConnectorDescEnd": { - "message": "are required to set up Key Connector decryption. Contact Bitwarden Support for set up assistance.", + "message": "is vereist om Key Connector decryptie in te stellen. Contacteer Bitwarden support voor assistentie.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Connect login with SSO to your self-hosted decryption key server. Using this option, members won’t need to use their master passwords to decrypt vault data. The require SSO authentication and single organization policies are required to set up Key Connector decryption. Contact Bitwarden Support for set up assistance.'" }, "keyConnectorPolicyRestriction": { @@ -6197,7 +6197,7 @@ } }, "deleteServiceAccountToast": { - "message": "Service account deleted" + "message": "Service account verwijderd" }, "deleteServiceAccountsToast": { "message": "Serviceaccounts verwijderd" @@ -6734,7 +6734,7 @@ } }, "teamsStarterPlanInvLimitReachedManageBilling": { - "message": "Teams Starter plans may have up to $SEATCOUNT$ members. Upgrade to your plan to invite more members.", + "message": "Gratis organisaties beschikken maximaal over $SEATCOUNT$ leden. Upgrade je abonnement om meer leden uit te kunnen nodigen.", "placeholders": { "seatcount": { "content": "$1", @@ -6881,7 +6881,7 @@ "message": "Werk je versleutelingsinstellingen bij om aan de nieuwe beveiligingsaanbevelingen te voldoen en de bescherming van je account te verbeteren." }, "changeKdfLoggedOutWarning": { - "message": "Proceeding will log you out of all active sessions. You will need to log back in and complete two-step login setup. We recommend exporting your vault before changing your encryption settings to prevent data loss." + "message": "Als u doorgaat zullen al uw actieve sessies word uitgelogd. U zult zich opnieuw aan moeten melden en 2 stappen verificatie moeten instellen. Wij raden u aan om uw gegevens eerst te exporteren voordat u uw encryptie instellingen aanpast om data verlies te voorkomen." }, "secretsManager": { "message": "Secrets Manager" @@ -7020,10 +7020,10 @@ "message": "Gelekt hoofdwachtwoord" }, "exposedMasterPasswordDesc": { - "message": "Password found in a data breach. Use a unique password to protect your account. Are you sure you want to use an exposed password?" + "message": "Dit wachtwoord is gevonden in een datalek. Gebruik een uniek wachtwoord om je account te beveiligen. Weet je zeker dat je een gelekt wachtwoord wil gebruiken?" }, "weakAndExposedMasterPassword": { - "message": "Weak and Exposed Master Password" + "message": "Zwak en gelekt hoofdwachtwoord" }, "weakAndBreachedMasterPasswordDesc": { "message": "Zwak wachtwoord geïdentificeerd en gevonden in een datalek. Gebruik een sterk en uniek wachtwoord om je account te beschermen. Weet je zeker dat je dit wachtwoord wilt gebruiken?" @@ -7271,7 +7271,7 @@ "message": "Volgende" }, "ssoLoginIsRequired": { - "message": "SSO login is required" + "message": "SSO login is vereist" }, "selectedRegionFlag": { "message": "Geselecteerde regionale vlag" @@ -7423,7 +7423,7 @@ "message": "Service account limiet (optioneel)" }, "maxServiceAccountCost": { - "message": "Max potential service account cost" + "message": "Maximale potentiële service account kosten" }, "loggedInExclamation": { "message": "Ingelogd!" @@ -7444,7 +7444,7 @@ "message": "Aliasdomein" }, "alreadyHaveAccount": { - "message": "Already have an account?" + "message": "Heb je al een account?" }, "skipToContent": { "message": "Ga naar de inhoud" @@ -7471,7 +7471,7 @@ } }, "seeDetailedInstructions": { - "message": "See detailed instructions on our help site at", + "message": "Zie gedetailleerde instructies op onze hulp pagina hier", "description": "This is followed a by a hyperlink to the help website." }, "installBrowserExtension": { @@ -7487,13 +7487,13 @@ "message": "Er is een onverwachte fout opgetreden tijdens het laden van deze Send. Probeer het later opnieuw." }, "seatLimitReached": { - "message": "Seat limit has been reached" + "message": "Gebruikers limiet is bereikt" }, "contactYourProvider": { - "message": "Contact your provider to purchase additional seats." + "message": "Contacteer uw provider om aanvullende licenties aan te schaffen." }, "seatLimitReachedContactYourProvider": { - "message": "Seat limit has been reached. Contact your provider to purchase additional seats." + "message": "De limiet voor het aantal gebruikers is bereikt. Contacteer je provider om aanvullende licenties aan te schaffen." }, "collectionAccessRestricted": { "message": "Collectietoegang is beperkt" @@ -7511,7 +7511,7 @@ "message": "Toegang tot serviceaccount bijgewerkt" }, "commonImportFormats": { - "message": "Common formats", + "message": "Gangbare formaten", "description": "Label indicating the most common import formats" }, "maintainYourSubscription": { @@ -7601,10 +7601,10 @@ "message": "Providerportaal" }, "restrictedGroupAccess": { - "message": "You cannot add yourself to groups." + "message": "Het is niet mogelijk om jezelf toe te voegen aan groepen." }, "restrictedCollectionAccess": { - "message": "You cannot add yourself to collections." + "message": "Het is niet mogelijk om jezelf toe te voegen aan collecties." }, "assign": { "message": "Toewijzen" diff --git a/apps/web/src/locales/zh_CN/messages.json b/apps/web/src/locales/zh_CN/messages.json index f2d8619b32..e7668f85d5 100644 --- a/apps/web/src/locales/zh_CN/messages.json +++ b/apps/web/src/locales/zh_CN/messages.json @@ -7478,7 +7478,7 @@ "message": "安装浏览器扩展" }, "installBrowserExtensionDetails": { - "message": "使用扩展快速保存登录信息和自动填充表单,而无需打开网页应用程序。" + "message": "使用扩展快速保存登录信息和自动填充表单,而无需打开网页 App。" }, "projectAccessUpdated": { "message": "工程访问权限已更新" @@ -7511,7 +7511,7 @@ "message": "服务账户访问权限已更新" }, "commonImportFormats": { - "message": "通用格式", + "message": "常规格式", "description": "Label indicating the most common import formats" }, "maintainYourSubscription": { @@ -7533,7 +7533,7 @@ "description": "This describes new features and improvements for user roles and collections" }, "collectionEnhancementsLearnMore": { - "message": "了解更多关于集合管理" + "message": "了解更多关于集合管理的信息" }, "organizationInformation": { "message": "组织信息" @@ -7604,31 +7604,31 @@ "message": "您不能将自己添加到群组。" }, "restrictedCollectionAccess": { - "message": "您不能将自己添加到群组。" + "message": "您不能将自己添加到集合。" }, "assign": { - "message": "Assign" + "message": "分配" }, "assignToCollections": { - "message": "Assign to collections" + "message": "分配到集合" }, "assignToTheseCollections": { - "message": "Assign to these collections" + "message": "分配到这些集合" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "选择要共享项目的集合。当一个项目在某个集合中更新后,它将反映在所有集合中。只有能够访问这些集合的组织成员才能看到此项目。" }, "selectCollectionsToAssign": { - "message": "Select collections to assign" + "message": "选择要分配的集合" }, "noCollectionsAssigned": { - "message": "No collections have been assigned" + "message": "没有分配任何集合" }, "successfullyAssignedCollections": { - "message": "Successfully assigned collections" + "message": "成功分配了集合" }, "bulkCollectionAssignmentWarning": { - "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "message": "您选择了 $TOTAL_COUNT$ 个项目。其中的 $READONLY_COUNT$ 个项目由于您没有编辑权限,您将无法更新它们。", "placeholders": { "total_count": { "content": "$1", @@ -7641,6 +7641,6 @@ } }, "items": { - "message": "Items" + "message": "项目" } } From 37735436d1826f88e340e50d2fc048c211f0a846 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Thu, 28 Mar 2024 06:53:20 -0500 Subject: [PATCH 053/351] Move biometric texts all to getters (#8520) We cannot load biometric text on init because they are not valid everywhere. This was causing issues with settings storage on linux. --- .../src/app/accounts/settings.component.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src/app/accounts/settings.component.ts b/apps/desktop/src/app/accounts/settings.component.ts index 60aa2ebae8..a613328878 100644 --- a/apps/desktop/src/app/accounts/settings.component.ts +++ b/apps/desktop/src/app/accounts/settings.component.ts @@ -42,7 +42,6 @@ export class SettingsComponent implements OnInit { themeOptions: any[]; clearClipboardOptions: any[]; supportsBiometric: boolean; - additionalBiometricSettingsText: string; showAlwaysShowDock = false; requireEnableTray = false; showDuckDuckGoIntegrationOption = false; @@ -283,10 +282,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$ @@ -700,4 +695,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"); + } + } } From bd6b3266d43327183cb33c4df18005fc0436e9a1 Mon Sep 17 00:00:00 2001 From: Jake Fink Date: Thu, 28 Mar 2024 09:34:21 -0400 Subject: [PATCH 054/351] move auth request notification to service (#8451) - cleanup hanging promises --- .../popup/login-via-auth-request.component.ts | 7 +- .../login/login-via-auth-request.component.ts | 11 +-- .../login/login-via-auth-request.component.ts | 70 +------------------ .../login-via-auth-request.component.ts | 21 +++--- .../src/services/jslib-services.module.ts | 2 +- .../auth-request.service.abstraction.ts | 12 ++++ .../abstractions/login-strategy.service.ts | 9 --- .../auth-request/auth-request.service.ts | 12 ++++ .../login-strategy.service.ts | 14 ---- .../abstractions/anonymous-hub.service.ts | 4 +- .../auth/services/anonymous-hub.service.ts | 28 ++++---- 11 files changed, 57 insertions(+), 133 deletions(-) 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..4ef1c78cb4 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,5 +1,5 @@ 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"; @@ -28,10 +28,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, 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..9a6fa8e388 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"; @@ -31,10 +31,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; @@ -109,10 +106,6 @@ export class LoginViaAuthRequestComponent }); } - ngOnDestroy(): void { - super.ngOnDestroy(); - } - back() { this.location.back(); } 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/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..b1d0b81922 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 @@ -68,7 +68,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, @@ -98,14 +97,16 @@ export class LoginViaAuthRequestComponent this.email = this.loginService.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); + }); }); } @@ -164,10 +165,10 @@ 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) { @@ -213,7 +214,7 @@ export class LoginViaAuthRequestComponent // 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() { @@ -273,7 +274,7 @@ export class LoginViaAuthRequestComponent } if (reqResponse.id) { - this.anonymousHubService.createHubConnection(reqResponse.id); + await this.anonymousHubService.createHubConnection(reqResponse.id); } } catch (e) { this.logService.error(e); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 521387181b..b2f5ee1f89 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -866,7 +866,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: AnonymousHubServiceAbstraction, useClass: AnonymousHubService, - deps: [EnvironmentService, LoginStrategyServiceAbstraction, LogService], + deps: [EnvironmentService, AuthRequestServiceAbstraction], }), safeProvider({ provide: ValidationServiceAbstraction, 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..7af92fc8f8 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,12 @@ +import { Observable } from "rxjs"; + import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; +import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response"; 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; /** * Approve or deny an auth request. * @param approve True to approve, false to deny. @@ -54,4 +59,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/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/services/auth-request/auth-request.service.ts b/libs/auth/src/common/services/auth-request/auth-request.service.ts index ab33780fe6..ff33eadfba 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,6 +1,9 @@ +import { Observable, Subject } from "rxjs"; + import { ApiService } from "@bitwarden/common/abstractions/api.service"; 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"; @@ -11,6 +14,9 @@ import { MasterKey, UserKey } from "@bitwarden/common/types/key"; import { AuthRequestServiceAbstraction } from "../../abstractions/auth-request.service.abstraction"; export class AuthRequestService implements AuthRequestServiceAbstraction { + private authRequestPushNotificationSubject = new Subject(); + authRequestPushNotification$: Observable; + constructor( private appIdService: AppIdService, private cryptoService: CryptoService, @@ -126,4 +132,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/login-strategies/login-strategy.service.ts b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts index 428258308a..b55f38af7f 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, @@ -23,7 +22,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,8 +79,6 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { >; currentAuthType$: Observable; - // TODO: move to auth request service - authRequestPushNotification$: Observable; constructor( protected cryptoService: CryptoService, @@ -114,9 +110,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$), @@ -256,13 +249,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, 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/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, + ); + } } } From 65353ae71d0a5d1051d3a5da7bcccefbb8c431af Mon Sep 17 00:00:00 2001 From: Victoria League Date: Thu, 28 Mar 2024 10:26:26 -0400 Subject: [PATCH 055/351] [CL-215] Fix broken icon stories and clarify usage (#8484) --- libs/components/src/icon/icon.stories.ts | 27 +++++++++--------------- 1 file changed, 10 insertions(+), 17 deletions(-) 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; -export const ReportExposedPasswords: Story = { - render: (args) => ({ - props: args, - template: ` -
- -
- `, - }), -}; - -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" }, + }, }, }; From df058ba399a5f9f12dcbcb3a8e41eba9b1e72bd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=A8=20Audrey=20=E2=9C=A8?= Date: Thu, 28 Mar 2024 12:19:12 -0400 Subject: [PATCH 056/351] [PM-6146] generator history (#8497) * introduce `GeneratorHistoryService` abstraction * implement generator history service with `LocalGeneratorHistoryService` * cache decrypted data using `ReplaySubject` instead of `DerivedState` * move Jsonification from `DataPacker` to `SecretClassifier` because the classifier is the only component that has full type information. The data packer still handles stringification. --- .../generator-history.abstraction.ts | 47 ++++ .../history/generated-credential.spec.ts | 58 ++++ .../generator/history/generated-credential.ts | 47 ++++ .../src/tools/generator/history/index.ts | 2 + .../local-generator-history.service.spec.ts | 198 ++++++++++++++ .../local-generator-history.service.ts | 116 ++++++++ .../src/tools/generator/history/options.ts | 10 + .../tools/generator/key-definition.spec.ts | 9 - .../src/tools/generator/key-definitions.ts | 11 +- .../generator/state/classified-format.ts | 19 ++ .../state/data-packer.abstraction.ts | 2 +- .../state/padded-data-packer.spec.ts | 10 - .../generator/state/padded-data-packer.ts | 2 +- .../generator/state/secret-classifier.spec.ts | 18 +- .../generator/state/secret-classifier.ts | 22 +- .../state/secret-key-definition.spec.ts | 22 ++ .../generator/state/secret-key-definition.ts | 17 +- .../generator/state/secret-state.spec.ts | 20 +- .../src/tools/generator/state/secret-state.ts | 253 ++++++++---------- .../state/user-encryptor.abstraction.ts | 6 +- .../state/user-key-encryptor.spec.ts | 8 +- .../generator/state/user-key-encryptor.ts | 6 +- 22 files changed, 691 insertions(+), 212 deletions(-) create mode 100644 libs/common/src/tools/generator/abstractions/generator-history.abstraction.ts create mode 100644 libs/common/src/tools/generator/history/generated-credential.spec.ts create mode 100644 libs/common/src/tools/generator/history/generated-credential.ts create mode 100644 libs/common/src/tools/generator/history/index.ts create mode 100644 libs/common/src/tools/generator/history/local-generator-history.service.spec.ts create mode 100644 libs/common/src/tools/generator/history/local-generator-history.service.ts create mode 100644 libs/common/src/tools/generator/history/options.ts create mode 100644 libs/common/src/tools/generator/state/classified-format.ts 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/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..f21767e77e 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, @@ -101,12 +100,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..d51af70f2e 100644 --- a/libs/common/src/tools/generator/key-definitions.ts +++ b/libs/common/src/tools/generator/key-definitions.ts @@ -1,8 +1,10 @@ import { GENERATOR_DISK, KeyDefinition } from "../../platform/state"; +import { GeneratedCredential } from "./history/generated-credential"; 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 { @@ -107,10 +109,11 @@ export const SIMPLE_LOGIN_FORWARDER = new KeyDefinition( ); /** 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, }, ); 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..e2f5058b21 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); 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..7352631ff6 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 @@ -7,6 +7,28 @@ describe("SecretKeyDefinition", () => { const classifier = SecretClassifier.allSecret<{ foo: boolean }>(); const options = { deserializer: (v: any) => v }; + it("toEncryptedStateKey returns a key", () => { + const expectedOptions = { + deserializer: (v: any) => v, + cleanupDelayMs: 100, + }; + 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(deserializerResult).toBe(expectedDeserializerResult); + }); + describe("value", () => { it("returns an initialized SecretKeyDefinition", () => { const definition = SecretKeyDefinition.value(GENERATOR_DISK, "key", classifier, options); 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..0de59be624 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 { KeyDefinition, KeyDefinitionOptions } 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`. @@ -18,6 +19,20 @@ 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 KeyDefinition<ClassifiedFormat<Id, Disclosed>[]>( + this.stateDefinition, + this.key, + { + cleanupDelayMs: this.options.cleanupDelayMs, + deserializer: (jsonValue) => jsonValue as ClassifiedFormat<Id, Disclosed>[], + }, + ); + + return secretKey; + } + /** * Define a secret state for a single value * @param stateDefinition The domain of the secret's durable state. 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); From 3d19e3489c0cee2ae68aac1ea7653a9580c55860 Mon Sep 17 00:00:00 2001 From: Ike <137194738+ike-kottlowski@users.noreply.github.com> Date: Thu, 28 Mar 2024 09:50:24 -0700 Subject: [PATCH 057/351] [PM-5269] Key Connector state migration (#8327) * key connector migration initial * migrator complete * fix dependencies * finalized tests * fix deps and sync main * clean up definition file * fixing tests * fixed tests * fixing CLI, Browser, Desktop builds * fixed factory options * reverting exports * implemented UserKeyDefinition clearOn * Update KeyConnector MIgration * updated migrator and tests to match profile object * removed unused service and updated clear * dep fix * dep fixes * clear usesKeyConnector on logout --- .../key-connector-service.factory.ts | 12 +- .../browser/context-menu-clicked-handler.ts | 5 +- .../browser/src/background/main.background.ts | 3 +- apps/cli/src/bw.ts | 2 +- apps/desktop/src/app/app.component.ts | 1 - apps/web/src/app/app.component.ts | 1 - .../src/services/jslib-services.module.ts | 2 +- .../abstractions/key-connector.service.ts | 1 - .../services/key-connector.service.spec.ts | 376 ++++++++++++++++++ .../auth/services/key-connector.service.ts | 60 ++- .../platform/abstractions/state.service.ts | 4 - .../src/platform/models/domain/account.ts | 2 - .../src/platform/services/state.service.ts | 34 -- .../src/platform/state/state-definitions.ts | 1 + libs/common/src/state-migrations/migrate.ts | 7 +- ...ve-key-connector-to-state-provider.spec.ts | 174 ++++++++ ...50-move-key-connector-to-state-provider.ts | 78 ++++ 17 files changed, 691 insertions(+), 72 deletions(-) create mode 100644 libs/common/src/auth/services/key-connector.service.spec.ts create mode 100644 libs/common/src/state-migrations/migrations/50-move-key-connector-to-state-provider.spec.ts create mode 100644 libs/common/src/state-migrations/migrations/50-move-key-connector-to-state-provider.ts 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..4a0dd07b32 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,9 +27,9 @@ 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 { TokenServiceInitOptions, tokenServiceFactory } from "./token-service.factory"; @@ -40,13 +40,13 @@ type KeyConnectorServiceFactoryOptions = FactoryOptions & { }; export type KeyConnectorServiceInitOptions = KeyConnectorServiceFactoryOptions & - StateServiceInitOptions & CryptoServiceInitOptions & ApiServiceInitOptions & TokenServiceInitOptions & LogServiceInitOptions & OrganizationServiceInitOptions & - KeyGenerationServiceInitOptions; + KeyGenerationServiceInitOptions & + StateProviderInitOptions; export function keyConnectorServiceFactory( cache: { keyConnectorService?: AbstractKeyConnectorService } & CachedServices, @@ -58,7 +58,6 @@ export function keyConnectorServiceFactory( opts, async () => new KeyConnectorService( - await stateServiceFactory(cache, opts), await cryptoServiceFactory(cache, opts), await apiServiceFactory(cache, opts), await tokenServiceFactory(cache, opts), @@ -66,6 +65,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/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/background/main.background.ts b/apps/browser/src/background/main.background.ts index c2c8c5be72..5bb47ab68a 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -514,7 +514,6 @@ export default class MainBackground { this.badgeSettingsService = new BadgeSettingsService(this.stateProvider); this.policyApiService = new PolicyApiService(this.policyService, this.apiService); this.keyConnectorService = new KeyConnectorService( - this.stateService, this.cryptoService, this.apiService, this.tokenService, @@ -522,6 +521,7 @@ export default class MainBackground { this.organizationService, this.keyGenerationService, logoutCallback, + this.stateProvider, ); this.passwordStrengthService = new PasswordStrengthService(); @@ -1125,7 +1125,6 @@ export default class MainBackground { this.policyService.clear(userId), this.passwordGenerationService.clear(userId), this.vaultTimeoutSettingsService.clear(userId), - this.keyConnectorService.clear(), this.vaultFilterService.clear(), this.biometricStateService.logout(userId), this.providerService.save(null, userId), diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index ce2152ffbf..7f23e6f2d0 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -427,7 +427,6 @@ export class Main { this.policyApiService = new PolicyApiService(this.policyService, this.apiService); this.keyConnectorService = new KeyConnectorService( - this.stateService, this.cryptoService, this.apiService, this.tokenService, @@ -435,6 +434,7 @@ export class Main { this.organizationService, this.keyGenerationService, async (expired: boolean) => await this.logout(), + this.stateProvider, ); this.twoFactorService = new TwoFactorService(this.i18nService, this.platformUtilsService); diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index 196bebfcf7..4e74135c49 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -584,7 +584,6 @@ export class AppComponent implements OnInit, OnDestroy { 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); diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts index 23b45618c6..32f4ee67e2 100644 --- a/apps/web/src/app/app.component.ts +++ b/apps/web/src/app/app.component.ts @@ -276,7 +276,6 @@ export class AppComponent implements OnDestroy, OnInit { this.collectionService.clear(userId), this.policyService.clear(userId), this.passwordGenerationService.clear(), - this.keyConnectorService.clear(), this.biometricStateService.logout(userId as UserId), this.paymentMethodWarningService.clear(), ]); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index b2f5ee1f89..841edb4289 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -765,7 +765,6 @@ const safeProviders: SafeProvider[] = [ provide: KeyConnectorServiceAbstraction, useClass: KeyConnectorService, deps: [ - StateServiceAbstraction, CryptoServiceAbstraction, ApiServiceAbstraction, TokenServiceAbstraction, @@ -773,6 +772,7 @@ const safeProviders: SafeProvider[] = [ OrganizationServiceAbstraction, KeyGenerationServiceAbstraction, LOGOUT_CALLBACK, + StateProvider, ], }), safeProvider({ 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<void>; getConvertAccountRequired: () => Promise<boolean>; removeConvertAccountRequired: () => Promise<void>; - clear: () => Promise<void>; } 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..50fed856f9 --- /dev/null +++ b/libs/common/src/auth/services/key-connector.service.spec.ts @@ -0,0 +1,376 @@ +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 { TokenService } from "./token.service"; + +describe("KeyConnectorService", () => { + let keyConnectorService: KeyConnectorService; + + const cryptoService = mock<CryptoService>(); + const apiService = mock<ApiService>(); + const tokenService = mock<TokenService>(); + const logService = mock<LogService>(); + const organizationService = mock<OrganizationService>(); + const keyGenerationService = mock<KeyGenerationService>(); + + let stateProvider: FakeStateProvider; + + let accountService: FakeAccountService; + + 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(); + + accountService = mockAccountServiceWith(mockUserId); + stateProvider = new FakeStateProvider(accountService); + + keyConnectorService = new KeyConnectorService( + 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(cryptoService.setMasterKey).toHaveBeenCalledWith(masterKey); + }); + + 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(); + const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64); + + jest.spyOn(keyConnectorService, "getManagingOrganization").mockResolvedValue(organization); + jest.spyOn(cryptoService, "getMasterKey").mockResolvedValue(masterKey); + jest.spyOn(apiService, "postUserKeyToKeyConnector").mockResolvedValue(); + + // Act + await keyConnectorService.migrateUser(); + + // Assert + expect(keyConnectorService.getManagingOrganization).toHaveBeenCalled(); + expect(cryptoService.getMasterKey).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]); + + jest.spyOn(keyConnectorService, "getManagingOrganization").mockResolvedValue(organization); + jest.spyOn(cryptoService, "getMasterKey").mockResolvedValue(masterKey); + 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(cryptoService.getMasterKey).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..d1502ce06c 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,9 +7,14 @@ 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 { KeyConnectorService as KeyConnectorServiceAbstraction } from "../abstractions/key-connector.service"; import { TokenService } from "../abstractions/token.service"; @@ -16,9 +23,28 @@ import { KeyConnectorUserKeyRequest } from "../models/request/key-connector-user 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<boolean>( + KEY_CONNECTOR_DISK, + "usesKeyConnector", + { + deserializer: (usesKeyConnector) => usesKeyConnector, + clearOn: ["logout"], + }, +); + +export const CONVERT_ACCOUNT_TO_KEY_CONNECTOR = new UserKeyDefinition<boolean>( + KEY_CONNECTOR_DISK, + "convertAccountToKeyConnector", + { + deserializer: (convertAccountToKeyConnector) => convertAccountToKeyConnector, + clearOn: ["logout"], + }, +); + export class KeyConnectorService implements KeyConnectorServiceAbstraction { + private usesKeyConnectorState: ActiveUserState<boolean>; + private convertAccountToKeyConnectorState: ActiveUserState<boolean>; constructor( - private stateService: StateService, private cryptoService: CryptoService, private apiService: ApiService, private tokenService: TokenService, @@ -26,14 +52,20 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { private organizationService: OrganizationService, private keyGenerationService: KeyGenerationService, private logoutCallback: (expired: boolean, userId?: string) => Promise<void>, - ) {} - - 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<boolean> { - return await this.stateService.getUsesKeyConnector(); + async setUsesKeyConnector(usesKeyConnector: boolean) { + await this.usesKeyConnectorState.update(() => usesKeyConnector); + } + + getUsesKeyConnector(): Promise<boolean> { + return firstValueFrom(this.usesKeyConnectorState.state$); } async userNeedsMigration() { @@ -132,19 +164,15 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { } async setConvertAccountRequired(status: boolean) { - await this.stateService.setConvertAccountToKeyConnector(status); + await this.convertAccountToKeyConnectorState.update(() => status); } - async getConvertAccountRequired(): Promise<boolean> { - return await this.stateService.getConvertAccountToKeyConnector(); + getConvertAccountRequired(): Promise<boolean> { + 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/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index b4847279c3..0ca0615380 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -52,8 +52,6 @@ export abstract class StateService<T extends Account = Account> { setAddEditCipherInfo: (value: AddEditCipherInfo, options?: StorageOptions) => Promise<void>; getBiometricFingerprintValidated: (options?: StorageOptions) => Promise<boolean>; setBiometricFingerprintValidated: (value: boolean, options?: StorageOptions) => Promise<void>; - getConvertAccountToKeyConnector: (options?: StorageOptions) => Promise<boolean>; - setConvertAccountToKeyConnector: (value: boolean, options?: StorageOptions) => Promise<void>; /** * Gets the user's master key */ @@ -269,8 +267,6 @@ export abstract class StateService<T extends Account = Account> { getSecurityStamp: (options?: StorageOptions) => Promise<string>; setSecurityStamp: (value: string, options?: StorageOptions) => Promise<void>; getUserId: (options?: StorageOptions) => Promise<string>; - getUsesKeyConnector: (options?: StorageOptions) => Promise<boolean>; - setUsesKeyConnector: (value: boolean, options?: StorageOptions) => Promise<void>; getVaultTimeout: (options?: StorageOptions) => Promise<number>; setVaultTimeout: (value: number, options?: StorageOptions) => Promise<void>; getVaultTimeoutAction: (options?: StorageOptions) => Promise<string>; diff --git a/libs/common/src/platform/models/domain/account.ts b/libs/common/src/platform/models/domain/account.ts index d01e9d5b8d..01660006c0 100644 --- a/libs/common/src/platform/models/domain/account.ts +++ b/libs/common/src/platform/models/domain/account.ts @@ -158,7 +158,6 @@ export class AccountKeys { } export class AccountProfile { - convertAccountToKeyConnector?: boolean; name?: string; email?: string; emailVerified?: boolean; @@ -166,7 +165,6 @@ export class AccountProfile { forceSetPasswordReason?: ForceSetPasswordReason; lastSync?: string; userId?: string; - usesKeyConnector?: boolean; keyHash?: string; kdfIterations?: number; kdfMemory?: number; diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index bbcc00e562..8c98cc346f 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -293,23 +293,6 @@ export class StateService< ); } - async getConvertAccountToKeyConnector(options?: StorageOptions): Promise<boolean> { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.profile?.convertAccountToKeyConnector; - } - - async setConvertAccountToKeyConnector(value: boolean, options?: StorageOptions): Promise<void> { - 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 */ @@ -1298,23 +1281,6 @@ export class StateService< )?.profile?.userId; } - async getUsesKeyConnector(options?: StorageOptions): Promise<boolean> { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.profile?.usesKeyConnector; - } - - async setUsesKeyConnector(value: boolean, options?: StorageOptions): Promise<void> { - 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<number> { const accountVaultTimeout = ( await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())) diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index b44c449c21..35714ee7c4 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -35,6 +35,7 @@ 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 AVATAR_DISK = new StateDefinition("avatar", "disk", { web: "disk-local" }); export const SSO_DISK = new StateDefinition("ssoLogin", "disk"); diff --git a/libs/common/src/state-migrations/migrate.ts b/libs/common/src/state-migrations/migrate.ts index b932a7186e..60bd31d049 100644 --- a/libs/common/src/state-migrations/migrate.ts +++ b/libs/common/src/state-migrations/migrate.ts @@ -46,6 +46,7 @@ import { MoveDesktopSettingsMigrator } from "./migrations/47-move-desktop-settin 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 { 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"; @@ -53,7 +54,8 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting import { MinVersionMigrator } from "./migrations/min-version"; export const MIN_VERSION = 3; -export const CURRENT_VERSION = 49; +export const CURRENT_VERSION = 50; + export type MinVersion = typeof MIN_VERSION; export function createMigrationBuilder() { @@ -104,7 +106,8 @@ export function createMigrationBuilder() { .with(DeleteBiometricPromptCancelledData, 45, 46) .with(MoveDesktopSettingsMigrator, 46, 47) .with(MoveDdgToStateProviderMigrator, 47, 48) - .with(AccountServerConfigMigrator, 48, CURRENT_VERSION); + .with(AccountServerConfigMigrator, 48, 49) + .with(KeyConnectorMigrator, 49, CURRENT_VERSION); } export async function currentVersion( 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<MigrationHelper>; + 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<void> { + const accounts = await helper.getAccounts<ExpectedAccountType>(); + async function migrateAccount(userId: string, account: ExpectedAccountType): Promise<void> { + 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<void> { + const accounts = await helper.getAccounts<ExpectedAccountType>(); + async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise<void> { + 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))]); + } +} From b70897a441211c336084fd9a650fec78d353e345 Mon Sep 17 00:00:00 2001 From: Addison Beck <github@addisonbeck.com> Date: Thu, 28 Mar 2024 14:12:52 -0500 Subject: [PATCH 058/351] Await `this.getScimEndpointUrl()` (#8532) --- .../app/admin-console/organizations/manage/scim.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/scim.component.ts b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/scim.component.ts index a22de64c39..8e8db457e5 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/scim.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/scim.component.ts @@ -107,7 +107,7 @@ export class ScimComponent implements OnInit { try { const response = await this.rotatePromise; this.formData.setValue({ - endpointUrl: this.getScimEndpointUrl(), + endpointUrl: await this.getScimEndpointUrl(), clientSecret: response.apiKey, }); this.platformUtilsService.showToast("success", null, this.i18nService.t("scimApiKeyRotated")); From 7021e944752e8d8bef6945370b64b3fc2f6a1c28 Mon Sep 17 00:00:00 2001 From: watsondm <129207532+watsondm@users.noreply.github.com> Date: Thu, 28 Mar 2024 16:44:40 -0400 Subject: [PATCH 059/351] CLOUDOPS-1369 Remove R2 bucket secrets and step from artifacts (#8534) --- .github/workflows/release-desktop-beta.yml | 20 +------------------- .github/workflows/release-desktop.yml | 21 +-------------------- 2 files changed, 2 insertions(+), 39 deletions(-) diff --git a/.github/workflows/release-desktop-beta.yml b/.github/workflows/release-desktop-beta.yml index b9e2d7a8c8..46f4ffad57 100644 --- a/.github/workflows/release-desktop-beta.yml +++ b/.github/workflows/release-desktop-beta.yml @@ -955,11 +955,7 @@ jobs: keyvault: "bitwarden-ci" secrets: "aws-electron-access-id, aws-electron-access-key, - aws-electron-bucket-name, - r2-electron-access-id, - r2-electron-access-key, - r2-electron-bucket-name, - cf-prod-account" + aws-electron-bucket-name" - name: Download all artifacts uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4 @@ -985,20 +981,6 @@ jobs: --recursive \ --quiet - - name: Publish artifacts to R2 - env: - AWS_ACCESS_KEY_ID: ${{ steps.retrieve-secrets.outputs.r2-electron-access-id }} - AWS_SECRET_ACCESS_KEY: ${{ steps.retrieve-secrets.outputs.r2-electron-access-key }} - AWS_DEFAULT_REGION: 'us-east-1' - AWS_S3_BUCKET_NAME: ${{ steps.retrieve-secrets.outputs.r2-electron-bucket-name }} - CF_ACCOUNT: ${{ steps.retrieve-secrets.outputs.cf-prod-account }} - working-directory: apps/desktop/artifacts - run: | - aws s3 cp ./ $AWS_S3_BUCKET_NAME/desktop/ \ - --recursive \ - --quiet \ - --endpoint-url https://${CF_ACCOUNT}.r2.cloudflarestorage.com - - name: Update deployment status to Success if: ${{ success() }} uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3 diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml index cf857d7177..dc6957d00d 100644 --- a/.github/workflows/release-desktop.yml +++ b/.github/workflows/release-desktop.yml @@ -115,11 +115,7 @@ jobs: keyvault: "bitwarden-ci" secrets: "aws-electron-access-id, aws-electron-access-key, - aws-electron-bucket-name, - r2-electron-access-id, - r2-electron-access-key, - r2-electron-bucket-name, - cf-prod-account" + aws-electron-bucket-name" - name: Download all artifacts if: ${{ github.event.inputs.release_type != 'Dry Run' }} @@ -169,21 +165,6 @@ jobs: --recursive \ --quiet - - name: Publish artifacts to R2 - if: ${{ github.event.inputs.release_type != 'Dry Run' && github.event.inputs.electron_publish == 'true' }} - env: - AWS_ACCESS_KEY_ID: ${{ steps.retrieve-secrets.outputs.r2-electron-access-id }} - AWS_SECRET_ACCESS_KEY: ${{ steps.retrieve-secrets.outputs.r2-electron-access-key }} - AWS_DEFAULT_REGION: 'us-east-1' - AWS_S3_BUCKET_NAME: ${{ steps.retrieve-secrets.outputs.r2-electron-bucket-name }} - CF_ACCOUNT: ${{ steps.retrieve-secrets.outputs.cf-prod-account }} - working-directory: apps/desktop/artifacts - run: | - aws s3 cp ./ $AWS_S3_BUCKET_NAME/desktop/ \ - --recursive \ - --quiet \ - --endpoint-url https://${CF_ACCOUNT}.r2.cloudflarestorage.com - - name: Get checksum files uses: bitwarden/gh-actions/get-checksum@main with: From ebe5a46b57bcfca686fe1f15bc7fb59ed17cbc6e Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Thu, 28 Mar 2024 16:56:02 -0400 Subject: [PATCH 060/351] PM-5263 - Clear all tokens on logout (#8536) --- libs/common/src/auth/services/token.state.ts | 4 ++++ libs/common/src/platform/services/state.service.ts | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/libs/common/src/auth/services/token.state.ts b/libs/common/src/auth/services/token.state.ts index 55471e1627..368f3c4ca2 100644 --- a/libs/common/src/auth/services/token.state.ts +++ b/libs/common/src/auth/services/token.state.ts @@ -1,5 +1,9 @@ import { KeyDefinition, TOKEN_DISK, TOKEN_DISK_LOCAL, TOKEN_MEMORY } from "../../platform/state"; +// 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 KeyDefinition<string>(TOKEN_DISK, "accessToken", { deserializer: (accessToken) => accessToken, }); diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index 8c98cc346f..d4297ecf94 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -1729,7 +1729,9 @@ export class StateService< } protected async deAuthenticateAccount(userId: string): Promise<void> { - 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); From 813dd97fed81ef6df40db6dd5172cc4155780782 Mon Sep 17 00:00:00 2001 From: aj-rosado <109146700+aj-rosado@users.noreply.github.com> Date: Fri, 29 Mar 2024 13:08:41 +0000 Subject: [PATCH 061/351] Removing clientSideOnlyVerification on UserVerificationDialogComponent on web export.component (#8545) --- apps/web/src/app/tools/vault-export/export.component.ts | 1 - 1 file changed, 1 deletion(-) 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 3f57f9aa71..4fdd3ff9e0 100644 --- a/apps/web/src/app/tools/vault-export/export.component.ts +++ b/apps/web/src/app/tools/vault-export/export.component.ts @@ -95,7 +95,6 @@ export class ExportComponent extends BaseExportComponent { } const result = await UserVerificationDialogComponent.open(this.dialogService, { - clientSideOnlyVerification: true, title: "confirmVaultExport", bodyText: confirmDescription, confirmButtonOptions: { From 07c172d3a3431bd3e45c9967796f7548780e8738 Mon Sep 17 00:00:00 2001 From: watsondm <129207532+watsondm@users.noreply.github.com> Date: Fri, 29 Mar 2024 09:43:06 -0400 Subject: [PATCH 062/351] Revert "CLOUDOPS-1369 Remove R2 bucket secrets and step from artifacts (#8534)" (#8546) This reverts commit 7021e944752e8d8bef6945370b64b3fc2f6a1c28. --- .github/workflows/release-desktop-beta.yml | 20 +++++++++++++++++++- .github/workflows/release-desktop.yml | 21 ++++++++++++++++++++- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-desktop-beta.yml b/.github/workflows/release-desktop-beta.yml index 46f4ffad57..b9e2d7a8c8 100644 --- a/.github/workflows/release-desktop-beta.yml +++ b/.github/workflows/release-desktop-beta.yml @@ -955,7 +955,11 @@ jobs: keyvault: "bitwarden-ci" secrets: "aws-electron-access-id, aws-electron-access-key, - aws-electron-bucket-name" + aws-electron-bucket-name, + r2-electron-access-id, + r2-electron-access-key, + r2-electron-bucket-name, + cf-prod-account" - name: Download all artifacts uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4 @@ -981,6 +985,20 @@ jobs: --recursive \ --quiet + - name: Publish artifacts to R2 + env: + AWS_ACCESS_KEY_ID: ${{ steps.retrieve-secrets.outputs.r2-electron-access-id }} + AWS_SECRET_ACCESS_KEY: ${{ steps.retrieve-secrets.outputs.r2-electron-access-key }} + AWS_DEFAULT_REGION: 'us-east-1' + AWS_S3_BUCKET_NAME: ${{ steps.retrieve-secrets.outputs.r2-electron-bucket-name }} + CF_ACCOUNT: ${{ steps.retrieve-secrets.outputs.cf-prod-account }} + working-directory: apps/desktop/artifacts + run: | + aws s3 cp ./ $AWS_S3_BUCKET_NAME/desktop/ \ + --recursive \ + --quiet \ + --endpoint-url https://${CF_ACCOUNT}.r2.cloudflarestorage.com + - name: Update deployment status to Success if: ${{ success() }} uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3 diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml index dc6957d00d..cf857d7177 100644 --- a/.github/workflows/release-desktop.yml +++ b/.github/workflows/release-desktop.yml @@ -115,7 +115,11 @@ jobs: keyvault: "bitwarden-ci" secrets: "aws-electron-access-id, aws-electron-access-key, - aws-electron-bucket-name" + aws-electron-bucket-name, + r2-electron-access-id, + r2-electron-access-key, + r2-electron-bucket-name, + cf-prod-account" - name: Download all artifacts if: ${{ github.event.inputs.release_type != 'Dry Run' }} @@ -165,6 +169,21 @@ jobs: --recursive \ --quiet + - name: Publish artifacts to R2 + if: ${{ github.event.inputs.release_type != 'Dry Run' && github.event.inputs.electron_publish == 'true' }} + env: + AWS_ACCESS_KEY_ID: ${{ steps.retrieve-secrets.outputs.r2-electron-access-id }} + AWS_SECRET_ACCESS_KEY: ${{ steps.retrieve-secrets.outputs.r2-electron-access-key }} + AWS_DEFAULT_REGION: 'us-east-1' + AWS_S3_BUCKET_NAME: ${{ steps.retrieve-secrets.outputs.r2-electron-bucket-name }} + CF_ACCOUNT: ${{ steps.retrieve-secrets.outputs.cf-prod-account }} + working-directory: apps/desktop/artifacts + run: | + aws s3 cp ./ $AWS_S3_BUCKET_NAME/desktop/ \ + --recursive \ + --quiet \ + --endpoint-url https://${CF_ACCOUNT}.r2.cloudflarestorage.com + - name: Get checksum files uses: bitwarden/gh-actions/get-checksum@main with: From 3a830789babbdc8f33ee18bb785e1d70518204fc Mon Sep 17 00:00:00 2001 From: SmithThe4th <gsmith@bitwarden.com> Date: Fri, 29 Mar 2024 10:06:50 -0400 Subject: [PATCH 063/351] [PM-5884] Allow deletion of passkey from edit view - clients (#8500) * add remove button for passkeys during edit * added live region to announce when a passkey is removed * removed announce passkey removed by SR * removed unused variable --- apps/browser/src/_locales/en/messages.json | 6 ++++++ .../components/vault/add-edit.component.html | 18 ++++++++++++++---- apps/desktop/src/locales/en/messages.json | 6 ++++++ .../vault/app/vault/add-edit.component.html | 17 ++++++++++++++--- .../individual-vault/add-edit.component.html | 15 ++++++++++++--- .../src/vault/components/add-edit.component.ts | 8 ++++++++ 6 files changed, 60 insertions(+), 10 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 0defc8aa7c..d802d27700 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -2999,5 +2999,11 @@ "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" } } 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 }}" > <div class="box-content"> - <div class="box-content-row text-muted"> - <span class="row-label">{{ "typePasskey" | i18n }}</span> - {{ "dateCreated" | i18n }} - {{ cipher.login.fido2Credentials[0].creationDate | date: "short" }} + <div class="box-content-row box-content-row-multi text-muted" appBoxRow> + <button + type="button" + appStopClick + (click)="removePasskey()" + appA11yTitle="{{ 'removePasskey' | i18n }}" + > + <i class="bwi bwi-fw bwi-minus-circle bwi-lg" aria-hidden="true"></i> + </button> + <div class="row-main"> + <span class="row-label">{{ "typePasskey" | i18n }}</span> + {{ "dateCreated" | i18n }} + {{ cipher.login.fido2Credentials[0].creationDate | date: "short" }} + </div> </div> </div> </div> diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 3a4835f16a..394a5951e9 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -2696,5 +2696,11 @@ }, "enableHardwareAccelerationRestart": { "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" } } 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 @@ </div> <!--Passkey--> <div - class="box-content-row text-muted" + class="box-content-row box-content-row-multi text-muted" *ngIf="cipher.login.hasFido2Credentials && !cloneMode" appBoxRow tabindex="0" attr.aria-label="{{ 'typePasskey' | i18n }} {{ fido2CredentialCreationDateValue }}" > - <span class="row-label">{{ "typePasskey" | i18n }}</span> - {{ fido2CredentialCreationDateValue }} + <button + type="button" + appStopClick + (click)="removePasskey()" + appA11yTitle="{{ 'removePasskey' | i18n }}" + [disabled]="!cipher.edit && editMode" + > + <i class="bwi bwi-fw bwi-minus-circle bwi-lg" aria-hidden="true"></i> + </button> + <div class="row-main"> + <span class="row-label">{{ "typePasskey" | i18n }}</span> + {{ fido2CredentialCreationDateValue }} + </div> </div> <div class="box-content-row" appBoxRow> diff --git a/apps/web/src/app/vault/individual-vault/add-edit.component.html b/apps/web/src/app/vault/individual-vault/add-edit.component.html index 12b41f181d..85075acfdd 100644 --- a/apps/web/src/app/vault/individual-vault/add-edit.component.html +++ b/apps/web/src/app/vault/individual-vault/add-edit.component.html @@ -192,11 +192,11 @@ </div> </div> <ng-container *ngIf="cipher.login.hasFido2Credentials"> - <div class="row"> - <div class="col-6 form-group"> + <div class="tw-flex tw-flex-row"> + <div class="tw-mb-4 tw-w-1/2"> <label for="loginFido2credential">{{ "typePasskey" | i18n }}</label> <div - class="input-group" + class="tw-flex tw-flex-row" tabindex="0" attr.aria-label="{{ 'typePasskey' | i18n }} {{ fido2CredentialCreationDateValue @@ -212,6 +212,15 @@ disabled readonly /> + <button + type="button" + class="tw-items-center tw-border-none tw-bg-transparent tw-text-danger tw-ml-3" + appA11yTitle="{{ 'removePasskey' | i18n }}" + (click)="removePasskey()" + *ngIf="!cipher.isDeleted && !viewOnly" + > + <i class="bwi bwi-lg bwi-minus-circle"></i> + </button> </div> </div> </div> diff --git a/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts index 4f5334d176..4c177a77f2 100644 --- a/libs/angular/src/vault/components/add-edit.component.ts +++ b/libs/angular/src/vault/components/add-edit.component.ts @@ -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); } From 9d1219bda6b6e9bfe509865f9beb65e6add46c70 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 29 Mar 2024 15:54:20 +0000 Subject: [PATCH 064/351] Autosync the updated translations (#8541) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/browser/store/locales/gl/copy.resx | 48 ++++++++++++------------- 1 file changed, 24 insertions(+), 24 deletions(-) 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 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden – Free Password Manager</value> + <value>Bitwarden – Xestor de contrasinais gratuíto</value> </data> <data name="Summary" xml:space="preserve"> - <value>A secure and free password manager for all of your devices</value> + <value>Un xestor de contrasinais seguro e gratuíto para todos os teus dispositivos</value> </data> <data name="Description" xml:space="preserve"> - <value>Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + <value>Bitwarden, Inc. é a empresa matriz de 8bit Solutions LLC. -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS &amp; WORLD REPORT, CNET, AND MORE. +NOMEADO MELLOR ADMINISTRADOR DE CONTRASINAIS POR THE VERGE, Ou.S. NEWS &amp; 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. </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>A secure and free password manager for all of your devices</value> + <value>Un xestor de contrasinais seguro e gratuíto para todos os teus dispositivos</value> </data> <data name="ScreenshotSync" xml:space="preserve"> - <value>Sync and access your vault from multiple devices</value> + <value>Sincroniza e accede á túa caixa forte desde múltiples dispositivos</value> </data> <data name="ScreenshotVault" xml:space="preserve"> - <value>Manage all your logins and passwords from a secure vault</value> + <value>Xestiona todos os teus usuarios e contrasinais desde unha caixa forte segura</value> </data> <data name="ScreenshotAutofill" xml:space="preserve"> - <value>Quickly auto-fill your login credentials into any website that you visit</value> + <value>Autocompleta rapidamente os teus datos de acceso en calquera páxina web que visites</value> </data> <data name="ScreenshotMenu" xml:space="preserve"> - <value>Your vault is also conveniently accessible from the right-click menu</value> + <value>A túa caixa forte tamén é facilmente accesible desde o menú de clic dereito</value> </data> <data name="ScreenshotPassword" xml:space="preserve"> - <value>Automatically generate strong, random, and secure passwords</value> + <value>Xera automaticamente contrasinais fortes, aleatorias e seguras</value> </data> <data name="ScreenshotEdit" xml:space="preserve"> - <value>Your information is managed securely using AES-256 bit encryption</value> + <value>A túa información é xestionada de forma segura con cifrado AES de 256 bits</value> </data> </root> From 670f33daa8af3416e0356a0ba932d4d38d75cc79 Mon Sep 17 00:00:00 2001 From: Cesar Gonzalez <cesar.a.gonzalezcs@gmail.com> Date: Fri, 29 Mar 2024 10:55:23 -0500 Subject: [PATCH 065/351] [PM-5743] Implement eslint rule for usage of window object in background script (#7849) * [PM-5742] Rework Usage of Extension APIs that Cannot be Called with the Background Service Worker * [PM-5742] Implementing jest tests for the updated BrowserApi methods * [PM-5742] Implementing jest tests to validate logic within added API calls * [PM-5742] Implementing jest tests to validate logic within added API calls * [PM-5742] Fixing broken Jest tests * [PM-5742] Fixing linter error * [PM-5887] Refactor WebCryptoFunction to Remove Usage of the window Object in the Background Script * [PM-5878] Rework `window` call within OverlayBackground to function within AutofillOverlayIframe service * [PM-6122] Rework `window` call within NotificationBackground to function within content script * [PM-5881] Adjust usage of the `chrome.extension.getViews` API to ensure expected behavior in manifest v3 * [PM-5881] Reworking how we handle early returns from `reloadOpenWindows` * [PM-5881] Implementing jest test to validate changes within BrowserApi.reloadOpenWindows * [PM-5743] Implement eslint rule to impeede usage of the `window` object in the background script * [PM-5743] Working through fixing eslint rule errors, and setting up ignore statements for lines that will be refactored at a later date * [PM-5743] Fixing broken jest tests * [PM-5879] Removing `backgroundWindow` reference used for determing system theme preference in Safari * [PM-5879] Removing `backgroundWindow` reference used for determing system theme preference in Safari * [PM-5743] Updating references to NodeJS.Timeout * [PM-5743] Adding notification bar and overaly content scripts to the eslint excluded files key * [PM-5743] Adding other excluded files from the eslint rule * [PM-5743] Reworking implementation to have the .eslintrc.json file present within the browser subdirectory --- apps/browser/.eslintrc.json | 26 +++++++++++++++++ .../background/web-request.background.ts | 2 +- .../src/autofill/content/autofiller.ts | 2 +- apps/browser/src/autofill/notification/bar.ts | 6 ++-- .../autofill-overlay-iframe.service.ts | 2 +- .../pages/list/autofill-overlay-list.ts | 2 +- .../services/abstractions/autofill.service.ts | 2 +- .../autofill-overlay-content.service.ts | 4 +-- .../src/autofill/services/autofill.service.ts | 2 +- .../collect-autofill-content.service.ts | 6 ++-- .../dom-element-visibility.service.ts | 2 +- .../insert-autofill-content.service.spec.ts | 28 +++++++++---------- .../insert-autofill-content.service.ts | 16 +++++------ .../browser/src/background/idle.background.ts | 6 ++-- .../browser/src/background/main.background.ts | 2 +- .../src/background/runtime.background.ts | 2 +- apps/browser/src/platform/background.ts | 2 +- .../src/platform/browser/browser-api.ts | 6 ++-- .../offscreen-document/offscreen-document.ts | 4 +-- .../services/browser-file-download.service.ts | 4 +-- ...ssaging-private-mode-background.service.ts | 2 +- ...er-messaging-private-mode-popup.service.ts | 2 +- apps/browser/src/popup/app.component.ts | 2 +- .../src/popup/services/services.module.ts | 2 +- 24 files changed, 79 insertions(+), 55 deletions(-) create mode 100644 apps/browser/.eslintrc.json rename apps/browser/src/platform/{ => popup}/services/browser-file-download.service.ts (93%) 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/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/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/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<string, string>) { 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/autofill-overlay-list.ts b/apps/browser/src/autofill/overlay/pages/list/autofill-overlay-list.ts index 305a230e5c..8d4fa724af 100644 --- a/apps/browser/src/autofill/overlay/pages/list/autofill-overlay-list.ts +++ b/apps/browser/src/autofill/overlay/pages/list/autofill-overlay-list.ts @@ -19,7 +19,7 @@ class AutofillOverlayList extends AutofillOverlayPageElement { private ciphers: OverlayCipherData[] = []; private ciphersList: HTMLUListElement; private cipherListScrollIsDebounced = false; - private cipherListScrollDebounceTimeout: NodeJS.Timeout; + private cipherListScrollDebounceTimeout: number | NodeJS.Timeout; private currentCipherIndex = 0; private readonly showCiphersPerPage = 6; private readonly overlayListWindowMessageHandlers: OverlayListWindowMessageHandlers = { diff --git a/apps/browser/src/autofill/services/abstractions/autofill.service.ts b/apps/browser/src/autofill/services/abstractions/autofill.service.ts index 77a5f982fd..54a91a5176 100644 --- a/apps/browser/src/autofill/services/abstractions/autofill.service.ts +++ b/apps/browser/src/autofill/services/abstractions/autofill.service.ts @@ -15,7 +15,7 @@ export interface PageDetail { export interface AutoFillOptions { cipher: CipherView; pageDetails: PageDetail[]; - doc?: typeof window.document; + doc?: typeof self.document; tab: chrome.tabs.Tab; skipUsernameOnlyFill?: boolean; onlyEmptyFields?: boolean; diff --git a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts index 79abdc3938..cd373cdfd3 100644 --- a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts +++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts @@ -712,7 +712,7 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte private async getBoundingClientRectFromIntersectionObserver( formFieldElement: ElementWithOpId<FormFieldElement>, ): Promise<DOMRectReadOnly | null> { - if (!("IntersectionObserver" in window) && !("IntersectionObserverEntry" in window)) { + if (!("IntersectionObserver" in globalThis) && !("IntersectionObserverEntry" in globalThis)) { return null; } @@ -901,7 +901,7 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte if ( this.focusedFieldData.focusedFieldRects?.top > 0 && - this.focusedFieldData.focusedFieldRects?.top < window.innerHeight + this.focusedFieldData.focusedFieldRects?.top < globalThis.innerHeight ) { return; } diff --git a/apps/browser/src/autofill/services/autofill.service.ts b/apps/browser/src/autofill/services/autofill.service.ts index e353a34ea0..dae29e61e4 100644 --- a/apps/browser/src/autofill/services/autofill.service.ts +++ b/apps/browser/src/autofill/services/autofill.service.ts @@ -42,7 +42,7 @@ import { export default class AutofillService implements AutofillServiceInterface { private openVaultItemPasswordRepromptPopout = openVaultItemPasswordRepromptPopout; - private openPasswordRepromptPopoutDebounce: NodeJS.Timeout; + private openPasswordRepromptPopoutDebounce: number | NodeJS.Timeout; private currentlyOpeningPasswordRepromptPopout = false; private autofillScriptPortsSet = new Set<chrome.runtime.Port>(); static searchFieldNamesSet = new Set(AutoFillConstants.SearchFieldNames); diff --git a/apps/browser/src/autofill/services/collect-autofill-content.service.ts b/apps/browser/src/autofill/services/collect-autofill-content.service.ts index 1de801a2c2..3144edcda9 100644 --- a/apps/browser/src/autofill/services/collect-autofill-content.service.ts +++ b/apps/browser/src/autofill/services/collect-autofill-content.service.ts @@ -39,7 +39,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte private autofillFieldElements: AutofillFieldElements = new Map(); private currentLocationHref = ""; private mutationObserver: MutationObserver; - private updateAutofillElementsAfterMutationTimeout: NodeJS.Timeout; + private updateAutofillElementsAfterMutationTimeout: number | NodeJS.Timeout; private readonly updateAfterMutationTimeoutDelay = 1000; private readonly ignoredInputTypes = new Set([ "hidden", @@ -180,7 +180,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte ): AutofillPageDetails { return { title: document.title, - url: (document.defaultView || window).location.href, + url: (document.defaultView || globalThis).location.href, documentUrl: document.location.href, forms: autofillFormsData, fields: autofillFieldsData, @@ -240,7 +240,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte * @private */ private getFormActionAttribute(element: ElementWithOpId<HTMLFormElement>): string { - return new URL(this.getPropertyOrAttribute(element, "action"), window.location.href).href; + return new URL(this.getPropertyOrAttribute(element, "action"), globalThis.location.href).href; } /** diff --git a/apps/browser/src/autofill/services/dom-element-visibility.service.ts b/apps/browser/src/autofill/services/dom-element-visibility.service.ts index acc5b12059..127ce84d91 100644 --- a/apps/browser/src/autofill/services/dom-element-visibility.service.ts +++ b/apps/browser/src/autofill/services/dom-element-visibility.service.ts @@ -66,7 +66,7 @@ class DomElementVisibilityService implements domElementVisibilityServiceInterfac */ private getElementStyle(element: HTMLElement, styleProperty: string): string { if (!this.cachedComputedStyle) { - this.cachedComputedStyle = (element.ownerDocument.defaultView || window).getComputedStyle( + this.cachedComputedStyle = (element.ownerDocument.defaultView || globalThis).getComputedStyle( element, ); } diff --git a/apps/browser/src/autofill/services/insert-autofill-content.service.spec.ts b/apps/browser/src/autofill/services/insert-autofill-content.service.spec.ts index 5ea1284d1b..5a123bf835 100644 --- a/apps/browser/src/autofill/services/insert-autofill-content.service.spec.ts +++ b/apps/browser/src/autofill/services/insert-autofill-content.service.spec.ts @@ -47,7 +47,7 @@ const initEventCount = Object.freeze( ); let confirmSpy: jest.SpyInstance<boolean, [message?: string]>; -let windowSpy: jest.SpyInstance<any>; +let windowLocationSpy: jest.SpyInstance<any>; let savedURLs: string[] | null = ["https://bitwarden.com"]; function setMockWindowLocation({ protocol, @@ -56,11 +56,9 @@ function setMockWindowLocation({ protocol: "http:" | "https:"; hostname: string; }) { - windowSpy.mockImplementation(() => ({ - location: { - protocol, - hostname, - }, + windowLocationSpy.mockImplementation(() => ({ + protocol, + hostname, })); } @@ -76,8 +74,8 @@ describe("InsertAutofillContentService", () => { beforeEach(() => { document.body.innerHTML = mockLoginForm; - confirmSpy = jest.spyOn(window, "confirm"); - windowSpy = jest.spyOn(window, "window", "get"); + confirmSpy = jest.spyOn(globalThis, "confirm"); + windowLocationSpy = jest.spyOn(globalThis, "location", "get"); insertAutofillContentService = new InsertAutofillContentService( domElementVisibilityService, collectAutofillContentService, @@ -101,7 +99,7 @@ describe("InsertAutofillContentService", () => { afterEach(() => { jest.resetAllMocks(); - windowSpy.mockRestore(); + windowLocationSpy.mockRestore(); confirmSpy.mockRestore(); document.body.innerHTML = ""; }); @@ -245,8 +243,8 @@ describe("InsertAutofillContentService", () => { }); it("returns true if the frameElement has a sandbox attribute", () => { - Object.defineProperty(globalThis, "window", { - value: { frameElement: { hasAttribute: jest.fn(() => true) } }, + Object.defineProperty(globalThis, "frameElement", { + value: { hasAttribute: jest.fn(() => true) }, writable: true, }); @@ -991,11 +989,11 @@ describe("InsertAutofillContentService", () => { const inputElement = document.querySelector('input[type="text"]') as HTMLInputElement; inputElement.value = "test"; jest.spyOn(inputElement, "focus"); - jest.spyOn(window, "String"); + jest.spyOn(globalThis, "String"); insertAutofillContentService["triggerFocusOnElement"](inputElement, true); - expect(window.String).toHaveBeenCalledWith(value); + expect(globalThis.String).toHaveBeenCalledWith(value); expect(inputElement.focus).toHaveBeenCalled(); expect(inputElement.value).toEqual(value); }); @@ -1005,11 +1003,11 @@ describe("InsertAutofillContentService", () => { const inputElement = document.querySelector('input[type="text"]') as HTMLInputElement; inputElement.value = "test"; jest.spyOn(inputElement, "focus"); - jest.spyOn(window, "String"); + jest.spyOn(globalThis, "String"); insertAutofillContentService["triggerFocusOnElement"](inputElement, false); - expect(window.String).not.toHaveBeenCalledWith(); + expect(globalThis.String).not.toHaveBeenCalledWith(); expect(inputElement.focus).toHaveBeenCalled(); expect(inputElement.value).toEqual(value); }); diff --git a/apps/browser/src/autofill/services/insert-autofill-content.service.ts b/apps/browser/src/autofill/services/insert-autofill-content.service.ts index dd14cadfa7..5cfa8091c4 100644 --- a/apps/browser/src/autofill/services/insert-autofill-content.service.ts +++ b/apps/browser/src/autofill/services/insert-autofill-content.service.ts @@ -65,8 +65,8 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf private fillingWithinSandboxedIframe() { return ( String(self.origin).toLowerCase() === "null" || - window.frameElement?.hasAttribute("sandbox") || - window.location.hostname === "" + globalThis.frameElement?.hasAttribute("sandbox") || + globalThis.location.hostname === "" ); } @@ -79,8 +79,8 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf */ private userCancelledInsecureUrlAutofill(savedUrls?: string[] | null): boolean { if ( - !savedUrls?.some((url) => url.startsWith(`https://${window.location.hostname}`)) || - window.location.protocol !== "http:" || + !savedUrls?.some((url) => url.startsWith(`https://${globalThis.location.hostname}`)) || + globalThis.location.protocol !== "http:" || !this.isPasswordFieldWithinDocument() ) { return false; @@ -88,10 +88,10 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf const confirmationWarning = [ chrome.i18n.getMessage("insecurePageWarning"), - chrome.i18n.getMessage("insecurePageWarningFillPrompt", [window.location.hostname]), + chrome.i18n.getMessage("insecurePageWarningFillPrompt", [globalThis.location.hostname]), ].join("\n\n"); - return !confirm(confirmationWarning); + return !globalThis.confirm(confirmationWarning); } /** @@ -129,10 +129,10 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf const confirmationWarning = [ chrome.i18n.getMessage("autofillIframeWarning"), - chrome.i18n.getMessage("autofillIframeWarningTip", [window.location.hostname]), + chrome.i18n.getMessage("autofillIframeWarningTip", [globalThis.location.hostname]), ].join("\n\n"); - return !confirm(confirmationWarning); + return !globalThis.confirm(confirmationWarning); } /** diff --git a/apps/browser/src/background/idle.background.ts b/apps/browser/src/background/idle.background.ts index 28c056cc0e..7b273459ad 100644 --- a/apps/browser/src/background/idle.background.ts +++ b/apps/browser/src/background/idle.background.ts @@ -11,7 +11,7 @@ const IdleInterval = 60 * 5; // 5 minutes export default class IdleBackground { private idle: typeof chrome.idle | typeof browser.idle | null; - private idleTimer: number = null; + private idleTimer: number | NodeJS.Timeout = null; private idleState = "active"; constructor( @@ -73,7 +73,7 @@ export default class IdleBackground { private pollIdle(handler: (newState: string) => void) { if (this.idleTimer != null) { - window.clearTimeout(this.idleTimer); + globalThis.clearTimeout(this.idleTimer); this.idleTimer = null; } // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. @@ -83,7 +83,7 @@ export default class IdleBackground { this.idleState = state; handler(state); } - this.idleTimer = window.setTimeout(() => this.pollIdle(handler), 5000); + this.idleTimer = globalThis.setTimeout(() => this.pollIdle(handler), 5000); }); } } diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 5bb47ab68a..957ebd998b 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -991,7 +991,7 @@ export default class MainBackground { } async bootstrap() { - this.containerService.attachToGlobal(window); + this.containerService.attachToGlobal(self); await this.stateService.init(); diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts index dd55c14fb2..a88bc051d8 100644 --- a/apps/browser/src/background/runtime.background.ts +++ b/apps/browser/src/background/runtime.background.ts @@ -89,7 +89,7 @@ export default class RuntimeBackground { BrowserApi.messageListener("runtime.background", backgroundMessageListener); if (this.main.popupOnlyContext) { - (window as any).bitwardenBackgroundMessageListener = backgroundMessageListener; + (self as any).bitwardenBackgroundMessageListener = backgroundMessageListener; } } diff --git a/apps/browser/src/platform/background.ts b/apps/browser/src/platform/background.ts index b71b4d96b0..5aa2820e5f 100644 --- a/apps/browser/src/platform/background.ts +++ b/apps/browser/src/platform/background.ts @@ -33,7 +33,7 @@ if (BrowserApi.isManifestVersion(3)) { }, ); } else { - const bitwardenMain = ((window as any).bitwardenMain = new MainBackground()); + const bitwardenMain = ((self as any).bitwardenMain = new MainBackground()); // 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 bitwardenMain.bootstrap().then(() => { diff --git a/apps/browser/src/platform/browser/browser-api.ts b/apps/browser/src/platform/browser/browser-api.ts index 362aac1af9..b2ee66f051 100644 --- a/apps/browser/src/platform/browser/browser-api.ts +++ b/apps/browser/src/platform/browser/browser-api.ts @@ -351,11 +351,11 @@ export class BrowserApi { private static setupUnloadListeners() { // The MDN recommend using 'visibilitychange' but that event is fired any time the popup window is obscured as well // 'pagehide' works just like 'unload' but is compatible with the back/forward cache, so we prefer using that one - window.onpagehide = () => { + self.addEventListener("pagehide", () => { for (const [event, callback] of BrowserApi.trackedChromeEventListeners) { event.removeListener(callback); } - }; + }); } static sendMessage(subscriber: string, arg: any = {}) { @@ -423,7 +423,7 @@ export class BrowserApi { return; } - const currentHref = window.location.href; + const currentHref = self.location.href; views .filter((w) => w.location.href != null && !w.location.href.includes("background.html")) .filter((w) => !exemptCurrentHref || w.location.href !== currentHref) diff --git a/apps/browser/src/platform/offscreen-document/offscreen-document.ts b/apps/browser/src/platform/offscreen-document/offscreen-document.ts index 02ae5cdb23..627036b80b 100644 --- a/apps/browser/src/platform/offscreen-document/offscreen-document.ts +++ b/apps/browser/src/platform/offscreen-document/offscreen-document.ts @@ -29,14 +29,14 @@ class OffscreenDocument implements OffscreenDocumentInterface { * @param message - The extension message containing the text to copy */ private async handleOffscreenCopyToClipboard(message: OffscreenDocumentExtensionMessage) { - await BrowserClipboardService.copy(window, message.text); + await BrowserClipboardService.copy(self, message.text); } /** * Reads the user's clipboard and returns the text. */ private async handleOffscreenReadFromClipboard() { - return await BrowserClipboardService.read(window); + return await BrowserClipboardService.read(self); } /** diff --git a/apps/browser/src/platform/services/browser-file-download.service.ts b/apps/browser/src/platform/popup/services/browser-file-download.service.ts similarity index 93% rename from apps/browser/src/platform/services/browser-file-download.service.ts rename to apps/browser/src/platform/popup/services/browser-file-download.service.ts index 8cb4d498a3..e9aaa639c4 100644 --- a/apps/browser/src/platform/services/browser-file-download.service.ts +++ b/apps/browser/src/platform/popup/services/browser-file-download.service.ts @@ -5,8 +5,8 @@ import { FileDownloadRequest } from "@bitwarden/common/platform/abstractions/fil import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { SafariApp } from "../../browser/safariApp"; -import { BrowserApi } from "../browser/browser-api"; +import { SafariApp } from "../../../browser/safariApp"; +import { BrowserApi } from "../../browser/browser-api"; @Injectable() export class BrowserFileDownloadService implements FileDownloadService { diff --git a/apps/browser/src/platform/services/browser-messaging-private-mode-background.service.ts b/apps/browser/src/platform/services/browser-messaging-private-mode-background.service.ts index c2a6f8c5e1..0c7008473b 100644 --- a/apps/browser/src/platform/services/browser-messaging-private-mode-background.service.ts +++ b/apps/browser/src/platform/services/browser-messaging-private-mode-background.service.ts @@ -3,6 +3,6 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag export default class BrowserMessagingPrivateModeBackgroundService implements MessagingService { send(subscriber: string, arg: any = {}) { const message = Object.assign({}, { command: subscriber }, arg); - (window as any).bitwardenPopupMainMessageListener(message); + (self as any).bitwardenPopupMainMessageListener(message); } } diff --git a/apps/browser/src/platform/services/browser-messaging-private-mode-popup.service.ts b/apps/browser/src/platform/services/browser-messaging-private-mode-popup.service.ts index 5572ba1ba4..5883f61197 100644 --- a/apps/browser/src/platform/services/browser-messaging-private-mode-popup.service.ts +++ b/apps/browser/src/platform/services/browser-messaging-private-mode-popup.service.ts @@ -3,6 +3,6 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag export default class BrowserMessagingPrivateModePopupService implements MessagingService { send(subscriber: string, arg: any = {}) { const message = Object.assign({}, { command: subscriber }, arg); - (window as any).bitwardenBackgroundMessageListener(message); + (self as any).bitwardenBackgroundMessageListener(message); } } diff --git a/apps/browser/src/popup/app.component.ts b/apps/browser/src/popup/app.component.ts index aec8ba7c66..9aa438d3b3 100644 --- a/apps/browser/src/popup/app.component.ts +++ b/apps/browser/src/popup/app.component.ts @@ -140,7 +140,7 @@ export class AppComponent implements OnInit, OnDestroy { } }; - (window as any).bitwardenPopupMainMessageListener = bitwardenPopupMainMessageListener; + (self as any).bitwardenPopupMainMessageListener = bitwardenPopupMainMessageListener; this.browserMessagingApi.messageListener("app.component", bitwardenPopupMainMessageListener); // eslint-disable-next-line rxjs/no-async-subscribe diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 33593b56dd..9bdd317854 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -92,9 +92,9 @@ import MainBackground from "../../background/main.background"; import { Account } from "../../models/account"; import { BrowserApi } from "../../platform/browser/browser-api"; import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; +import { BrowserFileDownloadService } from "../../platform/popup/services/browser-file-download.service"; import { BrowserStateService as StateServiceAbstraction } from "../../platform/services/abstractions/browser-state.service"; import { BrowserEnvironmentService } from "../../platform/services/browser-environment.service"; -import { BrowserFileDownloadService } from "../../platform/services/browser-file-download.service"; import BrowserLocalStorageService from "../../platform/services/browser-local-storage.service"; import BrowserMessagingPrivateModePopupService from "../../platform/services/browser-messaging-private-mode-popup.service"; import BrowserMessagingService from "../../platform/services/browser-messaging.service"; From 77cfa8a5ad9ab355896b3a5a76998d5c15c8a823 Mon Sep 17 00:00:00 2001 From: Cesar Gonzalez <cesar.a.gonzalezcs@gmail.com> Date: Fri, 29 Mar 2024 14:08:46 -0500 Subject: [PATCH 066/351] [PM-7128] Fix cached form fields not showing the inline menu after their visibility is changed using CSS (#8509) --- .../autofill/content/autofill-init.spec.ts | 1 + .../autofill-overlay-content.service.spec.ts | 53 ++++++------- .../autofill-overlay-content.service.ts | 2 +- .../collect-autofill-content.service.spec.ts | 73 +++++++++++++++++- .../collect-autofill-content.service.ts | 76 ++++++++++++++++--- 5 files changed, 161 insertions(+), 44 deletions(-) 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<IntersectionObserver>()); }); afterEach(() => { diff --git a/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts b/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts index 9f3ffea142..96a1b4c851 100644 --- a/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts +++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts @@ -173,12 +173,10 @@ describe("AutofillOverlayContentService", () => { autofillFieldData = mock<AutofillField>(); }); - it("ignores fields that are readonly", () => { + it("ignores fields that are readonly", async () => { autofillFieldData.readonly = true; - // 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 - autofillOverlayContentService.setupAutofillOverlayListenerOnField( + await autofillOverlayContentService.setupAutofillOverlayListenerOnField( autofillFieldElement, autofillFieldData, ); @@ -186,12 +184,10 @@ describe("AutofillOverlayContentService", () => { expect(autofillFieldElement.addEventListener).not.toHaveBeenCalled(); }); - it("ignores fields that contain a disabled attribute", () => { + it("ignores fields that contain a disabled attribute", async () => { autofillFieldData.disabled = true; - // 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 - autofillOverlayContentService.setupAutofillOverlayListenerOnField( + await autofillOverlayContentService.setupAutofillOverlayListenerOnField( autofillFieldElement, autofillFieldData, ); @@ -199,12 +195,10 @@ describe("AutofillOverlayContentService", () => { expect(autofillFieldElement.addEventListener).not.toHaveBeenCalled(); }); - it("ignores fields that are not viewable", () => { + it("ignores fields that are not viewable", async () => { autofillFieldData.viewable = false; - // 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 - autofillOverlayContentService.setupAutofillOverlayListenerOnField( + await autofillOverlayContentService.setupAutofillOverlayListenerOnField( autofillFieldElement, autofillFieldData, ); @@ -213,12 +207,10 @@ describe("AutofillOverlayContentService", () => { }); it("ignores fields that are part of the ExcludedOverlayTypes", () => { - AutoFillConstants.ExcludedOverlayTypes.forEach((excludedType) => { + AutoFillConstants.ExcludedOverlayTypes.forEach(async (excludedType) => { autofillFieldData.type = excludedType; - // 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 - autofillOverlayContentService.setupAutofillOverlayListenerOnField( + await autofillOverlayContentService.setupAutofillOverlayListenerOnField( autofillFieldElement, autofillFieldData, ); @@ -227,12 +219,10 @@ describe("AutofillOverlayContentService", () => { }); }); - it("ignores fields that contain the keyword `search`", () => { + it("ignores fields that contain the keyword `search`", async () => { autofillFieldData.placeholder = "search"; - // 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 - autofillOverlayContentService.setupAutofillOverlayListenerOnField( + await autofillOverlayContentService.setupAutofillOverlayListenerOnField( autofillFieldElement, autofillFieldData, ); @@ -240,12 +230,10 @@ describe("AutofillOverlayContentService", () => { expect(autofillFieldElement.addEventListener).not.toHaveBeenCalled(); }); - it("ignores fields that contain the keyword `captcha` ", () => { + it("ignores fields that contain the keyword `captcha` ", async () => { autofillFieldData.placeholder = "captcha"; - // 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 - autofillOverlayContentService.setupAutofillOverlayListenerOnField( + await autofillOverlayContentService.setupAutofillOverlayListenerOnField( autofillFieldElement, autofillFieldData, ); @@ -253,12 +241,10 @@ describe("AutofillOverlayContentService", () => { expect(autofillFieldElement.addEventListener).not.toHaveBeenCalled(); }); - it("ignores fields that do not appear as a login field", () => { + it("ignores fields that do not appear as a login field", async () => { autofillFieldData.placeholder = "not-a-login-field"; - // 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 - autofillOverlayContentService.setupAutofillOverlayListenerOnField( + await autofillOverlayContentService.setupAutofillOverlayListenerOnField( autofillFieldElement, autofillFieldData, ); @@ -267,6 +253,17 @@ describe("AutofillOverlayContentService", () => { }); }); + it("skips setup on fields that have been previously set up", async () => { + autofillOverlayContentService["formFieldElements"].add(autofillFieldElement); + + await autofillOverlayContentService.setupAutofillOverlayListenerOnField( + autofillFieldElement, + autofillFieldData, + ); + + expect(autofillFieldElement.addEventListener).not.toHaveBeenCalled(); + }); + describe("identifies the overlay visibility setting", () => { it("defaults the overlay visibility setting to `OnFieldFocus` if a value is not set", async () => { sendExtensionMessageSpy.mockResolvedValueOnce(undefined); diff --git a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts index cd373cdfd3..4b786e6ca3 100644 --- a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts +++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts @@ -86,7 +86,7 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte formFieldElement: ElementWithOpId<FormFieldElement>, autofillFieldData: AutofillField, ) { - if (this.isIgnoredField(autofillFieldData)) { + if (this.isIgnoredField(autofillFieldData) || this.formFieldElements.has(formFieldElement)) { return; } diff --git a/apps/browser/src/autofill/services/collect-autofill-content.service.spec.ts b/apps/browser/src/autofill/services/collect-autofill-content.service.spec.ts index d5c461269b..79cb41b9a1 100644 --- a/apps/browser/src/autofill/services/collect-autofill-content.service.spec.ts +++ b/apps/browser/src/autofill/services/collect-autofill-content.service.spec.ts @@ -27,6 +27,7 @@ describe("CollectAutofillContentService", () => { const domElementVisibilityService = new DomElementVisibilityService(); const autofillOverlayContentService = new AutofillOverlayContentService(); let collectAutofillContentService: CollectAutofillContentService; + const mockIntersectionObserver = mock<IntersectionObserver>(); beforeEach(() => { document.body.innerHTML = mockLoginForm; @@ -34,6 +35,7 @@ describe("CollectAutofillContentService", () => { domElementVisibilityService, autofillOverlayContentService, ); + window.IntersectionObserver = jest.fn(() => mockIntersectionObserver); }); afterEach(() => { @@ -2527,10 +2529,10 @@ describe("CollectAutofillContentService", () => { }); updatedAttributes.forEach((attribute) => { - it(`will update the ${attribute} value for the field element`, async () => { + it(`will update the ${attribute} value for the field element`, () => { jest.spyOn(collectAutofillContentService["autofillFieldElements"], "set"); - await collectAutofillContentService["updateAutofillFieldElementData"]( + collectAutofillContentService["updateAutofillFieldElementData"]( attribute, fieldElement, autofillField, @@ -2543,10 +2545,10 @@ describe("CollectAutofillContentService", () => { }); }); - it("will not update an attribute value if it is not present in the updateActions object", async () => { + it("will not update an attribute value if it is not present in the updateActions object", () => { jest.spyOn(collectAutofillContentService["autofillFieldElements"], "set"); - await collectAutofillContentService["updateAutofillFieldElementData"]( + collectAutofillContentService["updateAutofillFieldElementData"]( "random-attribute", fieldElement, autofillField, @@ -2555,4 +2557,67 @@ describe("CollectAutofillContentService", () => { expect(collectAutofillContentService["autofillFieldElements"].set).not.toBeCalled(); }); }); + + describe("handleFormElementIntersection", () => { + let isFormFieldViewableSpy: jest.SpyInstance; + let setupAutofillOverlayListenerOnFieldSpy: jest.SpyInstance; + + beforeEach(() => { + isFormFieldViewableSpy = jest.spyOn( + collectAutofillContentService["domElementVisibilityService"], + "isFormFieldViewable", + ); + setupAutofillOverlayListenerOnFieldSpy = jest.spyOn( + collectAutofillContentService["autofillOverlayContentService"], + "setupAutofillOverlayListenerOnField", + ); + }); + + it("skips the initial intersection event for an observed element", async () => { + const formFieldElement = document.createElement("input") as ElementWithOpId<FormFieldElement>; + collectAutofillContentService["elementInitializingIntersectionObserver"].add( + formFieldElement, + ); + const entries = [ + { target: formFieldElement, isIntersecting: true }, + ] as unknown as IntersectionObserverEntry[]; + + await collectAutofillContentService["handleFormElementIntersection"](entries); + + expect(isFormFieldViewableSpy).not.toHaveBeenCalled(); + expect(setupAutofillOverlayListenerOnFieldSpy).not.toHaveBeenCalled(); + }); + + it("skips setting up the overlay listeners on a field that is not viewable", async () => { + const formFieldElement = document.createElement("input") as ElementWithOpId<FormFieldElement>; + const entries = [ + { target: formFieldElement, isIntersecting: true }, + ] as unknown as IntersectionObserverEntry[]; + isFormFieldViewableSpy.mockReturnValueOnce(false); + + await collectAutofillContentService["handleFormElementIntersection"](entries); + + expect(isFormFieldViewableSpy).toHaveBeenCalledWith(formFieldElement); + expect(setupAutofillOverlayListenerOnFieldSpy).not.toHaveBeenCalled(); + }); + + it("sets up the overlay listeners on a viewable field", async () => { + const formFieldElement = document.createElement("input") as ElementWithOpId<FormFieldElement>; + const autofillField = mock<AutofillField>(); + const entries = [ + { target: formFieldElement, isIntersecting: true }, + ] as unknown as IntersectionObserverEntry[]; + isFormFieldViewableSpy.mockReturnValueOnce(true); + collectAutofillContentService["autofillFieldElements"].set(formFieldElement, autofillField); + collectAutofillContentService["intersectionObserver"] = mockIntersectionObserver; + + await collectAutofillContentService["handleFormElementIntersection"](entries); + + expect(isFormFieldViewableSpy).toHaveBeenCalledWith(formFieldElement); + expect(setupAutofillOverlayListenerOnFieldSpy).toHaveBeenCalledWith( + formFieldElement, + autofillField, + ); + }); + }); }); diff --git a/apps/browser/src/autofill/services/collect-autofill-content.service.ts b/apps/browser/src/autofill/services/collect-autofill-content.service.ts index 3144edcda9..63dee7f3b1 100644 --- a/apps/browser/src/autofill/services/collect-autofill-content.service.ts +++ b/apps/browser/src/autofill/services/collect-autofill-content.service.ts @@ -38,6 +38,8 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte private autofillFormElements: AutofillFormElements = new Map(); private autofillFieldElements: AutofillFieldElements = new Map(); private currentLocationHref = ""; + private intersectionObserver: IntersectionObserver; + private elementInitializingIntersectionObserver: Set<Element> = new Set(); private mutationObserver: MutationObserver; private updateAutofillElementsAfterMutationTimeout: number | NodeJS.Timeout; private readonly updateAfterMutationTimeoutDelay = 1000; @@ -70,6 +72,10 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte this.setupMutationObserver(); } + if (!this.intersectionObserver) { + this.setupIntersectionObserver(); + } + if (!this.domRecentlyMutated && this.noFieldsFound) { return this.getFormattedPageDetails({}, []); } @@ -360,11 +366,14 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte tagName: this.getAttributeLowerCase(element, "tagName"), }; + if (!autofillFieldBase.viewable) { + this.elementInitializingIntersectionObserver.add(element); + this.intersectionObserver.observe(element); + } + if (elementIsSpanElement(element)) { this.cacheAutofillFieldElement(index, element, autofillFieldBase); - // 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.autofillOverlayContentService?.setupAutofillOverlayListenerOnField( + void this.autofillOverlayContentService?.setupAutofillOverlayListenerOnField( element, autofillFieldBase, ); @@ -407,9 +416,10 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte }; this.cacheAutofillFieldElement(index, element, autofillField); - // 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.autofillOverlayContentService?.setupAutofillOverlayListenerOnField(element, autofillField); + void this.autofillOverlayContentService?.setupAutofillOverlayListenerOnField( + element, + autofillField, + ); return autofillField; }; @@ -1189,8 +1199,6 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte 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.updateAutofillFieldElementData( attributeName, targetElement as ElementWithOpId<FormFieldElement>, @@ -1232,13 +1240,12 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte /** * Updates the autofill field element data based on the passed attribute name. + * * @param {string} attributeName * @param {ElementWithOpId<FormFieldElement>} element * @param {AutofillField} dataTarget - * @returns {Promise<void>} - * @private */ - private async updateAutofillFieldElementData( + private updateAutofillFieldElementData( attributeName: string, element: ElementWithOpId<FormFieldElement>, dataTarget: AutofillField, @@ -1304,6 +1311,52 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte return attributeValue; } + /** + * Sets up an IntersectionObserver to observe found form + * field elements that are not viewable in the viewport. + */ + private setupIntersectionObserver() { + this.intersectionObserver = new IntersectionObserver(this.handleFormElementIntersection, { + root: null, + rootMargin: "0px", + threshold: 1.0, + }); + } + + /** + * Handles observed form field elements that are not viewable in the viewport. + * Will re-evaluate the visibility of the element and set up the autofill + * overlay listeners on the field if it is viewable. + * + * @param entries - The entries observed by the IntersectionObserver + */ + private handleFormElementIntersection = async (entries: IntersectionObserverEntry[]) => { + for (let entryIndex = 0; entryIndex < entries.length; entryIndex++) { + const entry = entries[entryIndex]; + const formFieldElement = entry.target as ElementWithOpId<FormFieldElement>; + if (this.elementInitializingIntersectionObserver.has(formFieldElement)) { + this.elementInitializingIntersectionObserver.delete(formFieldElement); + continue; + } + + const isViewable = + await this.domElementVisibilityService.isFormFieldViewable(formFieldElement); + if (!isViewable) { + continue; + } + + const cachedAutofillFieldElement = this.autofillFieldElements.get(formFieldElement); + cachedAutofillFieldElement.viewable = true; + + void this.autofillOverlayContentService?.setupAutofillOverlayListenerOnField( + formFieldElement, + cachedAutofillFieldElement, + ); + + this.intersectionObserver.unobserve(entry.target); + } + }; + /** * Destroys the CollectAutofillContentService. Clears all * timeouts and disconnects the mutation observer. @@ -1313,6 +1366,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte clearTimeout(this.updateAutofillElementsAfterMutationTimeout); } this.mutationObserver?.disconnect(); + this.intersectionObserver?.disconnect(); } } From 2e51d96416bc119b33e6eafa89485f2c9a108327 Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Sat, 30 Mar 2024 11:00:27 -0700 Subject: [PATCH 067/351] [PM-5264] Implement StateProvider in LoginEmailService (#7662) * setup StateProvider in LoginService * replace implementations * replace implementation * remove stateService * change storage location for web to 'disk-local' * implement migrate() method of Migrator * add RememberedEmailMigrator to migrate.ts * add rollback * add tests * replace implementation * replace implementation * add StateProvider to Desktop services * rename LoginService to RememberEmailService * update state definition * rename file * rename to storedEmail * rename service to EmailService to avoid confusion * add jsdocs * refactor login.component.ts * fix typos * fix test * rename to LoginEmailService * update factory * more renaming * remove duplicate logic and rename method * convert storedEmail to observable * refactor to remove setStoredEmail() method * move service to libs/auth/common * address floating promises * remove comment * remove unnecessary deps in service registration --- .../login-email-service.factory.ts | 28 +++++++ apps/browser/src/auth/popup/hint.component.ts | 6 +- .../src/auth/popup/home.component.html | 2 +- apps/browser/src/auth/popup/home.component.ts | 47 +++++------ .../popup/login-via-auth-request.component.ts | 6 +- .../src/auth/popup/login.component.html | 2 +- .../browser/src/auth/popup/login.component.ts | 16 ++-- .../src/auth/popup/two-factor.component.ts | 6 +- .../browser/src/background/main.background.ts | 6 +- .../src/popup/services/services.module.ts | 7 -- .../app/layout/account-switcher.component.ts | 7 +- .../src/app/services/services.module.ts | 7 -- apps/desktop/src/auth/hint.component.ts | 6 +- .../login/login-via-auth-request.component.ts | 6 +- .../src/auth/login/login.component.html | 4 +- .../desktop/src/auth/login/login.component.ts | 10 ++- apps/desktop/src/auth/two-factor.component.ts | 6 +- apps/web/src/app/auth/hint.component.ts | 6 +- .../src/app/auth/login/login.component.html | 4 +- .../web/src/app/auth/login/login.component.ts | 23 ++---- apps/web/src/app/auth/two-factor.component.ts | 8 +- apps/web/src/app/core/core.module.ts | 8 +- ...base-login-decryption-options.component.ts | 20 ++--- .../src/auth/components/hint.component.ts | 6 +- .../login-via-auth-request.component.ts | 17 ++-- .../src/auth/components/login.component.ts | 47 ++++++----- .../components/two-factor.component.spec.ts | 16 ++-- .../auth/components/two-factor.component.ts | 6 +- .../src/services/jslib-services.module.ts | 10 +-- libs/auth/src/common/abstractions/index.ts | 1 + .../abstractions/login-email.service.ts | 38 +++++++++ libs/auth/src/common/services/index.ts | 1 + .../login-email/login-email.service.ts | 52 ++++++++++++ .../src/auth/abstractions/login.service.ts | 8 -- .../common/src/auth/services/login.service.ts | 35 -------- .../platform/abstractions/state.service.ts | 2 - .../platform/models/domain/global-state.ts | 1 - .../src/platform/services/state.service.ts | 17 ---- .../src/platform/state/state-definitions.ts | 5 +- libs/common/src/state-migrations/migrate.ts | 6 +- ...emembered-email-to-state-providers.spec.ts | 81 +++++++++++++++++++ ...ove-remembered-email-to-state-providers.ts | 46 +++++++++++ 42 files changed, 396 insertions(+), 240 deletions(-) create mode 100644 apps/browser/src/auth/background/service-factories/login-email-service.factory.ts create mode 100644 libs/auth/src/common/abstractions/login-email.service.ts create mode 100644 libs/auth/src/common/services/login-email/login-email.service.ts delete mode 100644 libs/common/src/auth/abstractions/login.service.ts delete mode 100644 libs/common/src/auth/services/login.service.ts create mode 100644 libs/common/src/state-migrations/migrations/51-move-remembered-email-to-state-providers.spec.ts create mode 100644 libs/common/src/state-migrations/migrations/51-move-remembered-email-to-state-providers.ts 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<LoginEmailServiceAbstraction> { + return factory( + cache, + "loginEmailService", + opts, + async () => new LoginEmailService(await stateProviderFactory(cache, opts)), + ); +} 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 @@ </form> <p class="createAccountLink"> {{ "newAroundHere" | i18n }} - <a routerLink="/register" (click)="setFormValues()">{{ "createAccount" | i18n }}</a> + <a routerLink="/register" (click)="setLoginEmailValues()">{{ "createAccount" | i18n }}</a> </p> </div> </div> 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<void> { - 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/login-via-auth-request.component.ts b/apps/browser/src/auth/popup/login-via-auth-request.component.ts index 4ef1c78cb4..8d438d5b78 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 @@ -6,12 +6,12 @@ import { LoginViaAuthRequestComponent as BaseLoginWithDeviceComponent } from "@b import { AuthRequestServiceAbstraction, LoginStrategyServiceAbstraction, + LoginEmailServiceAbstraction, } 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"; @@ -44,7 +44,7 @@ export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent { anonymousHubService: AnonymousHubService, validationService: ValidationService, stateService: StateService, - loginService: LoginService, + loginEmailService: LoginEmailServiceAbstraction, syncService: SyncService, deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, authRequestService: AuthRequestServiceAbstraction, @@ -66,7 +66,7 @@ export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent { anonymousHubService, validationService, stateService, - loginService, + loginEmailService, deviceTrustCryptoService, authRequestService, loginStrategyService, 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 @@ </div> </div> <div class="box-footer"> - <button type="button" class="btn link" routerLink="/hint" (click)="setFormValues()"> + <button type="button" class="btn link" routerLink="/hint" (click)="setLoginEmailValues()"> <b>{{ "getMasterPasswordHint" | i18n }}</b> </button> </div> 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/two-factor.component.ts b/apps/browser/src/auth/popup/two-factor.component.ts index 94dfb5155b..dd541f63f8 100644 --- a/apps/browser/src/auth/popup/two-factor.component.ts +++ b/apps/browser/src/auth/popup/two-factor.component.ts @@ -7,10 +7,10 @@ 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 { 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"; @@ -57,7 +57,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { logService: LogService, twoFactorService: TwoFactorService, appIdService: AppIdService, - loginService: LoginService, + loginEmailService: LoginEmailServiceAbstraction, userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, configService: ConfigService, ssoLoginService: SsoLoginServiceAbstraction, @@ -78,7 +78,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { logService, twoFactorService, appIdService, - loginService, + loginEmailService, userDecryptionOptionsService, ssoLoginService, configService, diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 957ebd998b..ee17a7f1f0 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -9,6 +9,7 @@ import { UserDecryptionOptionsService, AuthRequestServiceAbstraction, AuthRequestService, + LoginEmailServiceAbstraction, } from "@bitwarden/auth/common"; import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service"; import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service"; @@ -258,6 +259,7 @@ export default class MainBackground { auditService: AuditServiceAbstraction; authService: AuthServiceAbstraction; loginStrategyService: LoginStrategyServiceAbstraction; + loginEmailService: LoginEmailServiceAbstraction; importApiService: ImportApiServiceAbstraction; importService: ImportServiceAbstraction; exportService: VaultExportServiceAbstraction; @@ -1080,7 +1082,9 @@ export default class MainBackground { await this.stateService.setActiveUser(userId); if (userId == null) { - await this.stateService.setRememberedEmail(null); + this.loginEmailService.setRememberEmail(false); + await this.loginEmailService.saveEmailSettings(); + await this.refreshBadge(); await this.refreshMenu(); await this.overlayBackground.updateOverlayCiphers(); diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 9bdd317854..fbeabca462 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -30,13 +30,11 @@ import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/ab import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; -import { LoginService as LoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/login.service"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { AuthService } from "@bitwarden/common/auth/services/auth.service"; -import { LoginService } from "@bitwarden/common/auth/services/login.service"; import { AutofillSettingsService, AutofillSettingsServiceAbstraction, @@ -429,11 +427,6 @@ const safeProviders: SafeProvider[] = [ useClass: BrowserFileDownloadService, deps: [], }), - safeProvider({ - provide: LoginServiceAbstraction, - useClass: LoginService, - deps: [StateServiceAbstraction], - }), safeProvider({ provide: SYSTEM_THEME_OBSERVABLE, useFactory: (platformUtilsService: PlatformUtilsService) => { 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<void> { @@ -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/services.module.ts b/apps/desktop/src/app/services/services.module.ts index 1d75ff4ca9..84932ce7d9 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -20,9 +20,7 @@ import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaul 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 { 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"; @@ -221,11 +219,6 @@ const safeProviders: SafeProvider[] = [ DesktopAutofillSettingsService, ], }), - safeProvider({ - provide: LoginServiceAbstraction, - useClass: LoginService, - deps: [StateServiceAbstraction], - }), safeProvider({ provide: CryptoFunctionServiceAbstraction, useClass: RendererCryptoFunctionService, diff --git a/apps/desktop/src/auth/hint.component.ts b/apps/desktop/src/auth/hint.component.ts index 5eeeb8106e..cee1f18981 100644 --- a/apps/desktop/src/auth/hint.component.ts +++ b/apps/desktop/src/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 { i18nService: I18nService, apiService: ApiService, logService: LogService, - loginService: LoginService, + loginEmailService: LoginEmailServiceAbstraction, ) { - super(router, i18nService, apiService, platformUtilsService, logService, loginService); + super(router, i18nService, apiService, platformUtilsService, logService, loginEmailService); } } 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 9a6fa8e388..28163d09d0 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 @@ -7,12 +7,12 @@ 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 { 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"; @@ -53,7 +53,7 @@ export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent { private modalService: ModalService, syncService: SyncService, stateService: StateService, - loginService: LoginService, + loginEmailService: LoginEmailServiceAbstraction, deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, authRequestService: AuthRequestServiceAbstraction, loginStrategyService: LoginStrategyServiceAbstraction, @@ -74,7 +74,7 @@ export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent { anonymousHubService, validationService, stateService, - loginService, + loginEmailService, deviceTrustCryptoService, authRequestService, loginStrategyService, 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()" > <i class="bwi bwi-universal-access" aria-hidden="true"></i> {{ "loadAccessibilityCookie" | i18n }} @@ -139,7 +139,7 @@ type="button" class="text text-primary password-hint-btn" routerLink="/hint" - (click)="setFormValues()" + (click)="setLoginEmailValues()" > {{ "getMasterPasswordHint" | i18n }} </button> diff --git a/apps/desktop/src/auth/login/login.component.ts b/apps/desktop/src/auth/login/login.component.ts index dd22a0fa37..eb7b924362 100644 --- a/apps/desktop/src/auth/login/login.component.ts +++ b/apps/desktop/src/auth/login/login.component.ts @@ -7,9 +7,11 @@ import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components 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"; @@ -69,7 +71,7 @@ export class LoginComponent extends BaseLoginComponent implements OnDestroy { formBuilder: FormBuilder, formValidationErrorService: FormValidationErrorsService, route: ActivatedRoute, - loginService: LoginService, + loginEmailService: LoginEmailServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction, webAuthnLoginService: WebAuthnLoginServiceAbstraction, ) { @@ -89,7 +91,7 @@ export class LoginComponent extends BaseLoginComponent implements OnDestroy { formBuilder, formValidationErrorService, route, - loginService, + loginEmailService, ssoLoginService, webAuthnLoginService, ); diff --git a/apps/desktop/src/auth/two-factor.component.ts b/apps/desktop/src/auth/two-factor.component.ts index 8b46f3d1b9..fdbc52b4bf 100644 --- a/apps/desktop/src/auth/two-factor.component.ts +++ b/apps/desktop/src/auth/two-factor.component.ts @@ -7,10 +7,10 @@ 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 { 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"; @@ -56,7 +56,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { logService: LogService, twoFactorService: TwoFactorService, appIdService: AppIdService, - loginService: LoginService, + loginEmailService: LoginEmailServiceAbstraction, userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction, configService: ConfigService, @@ -75,7 +75,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { logService, twoFactorService, appIdService, - loginService, + loginEmailService, userDecryptionOptionsService, ssoLoginService, configService, 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/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 @@ <form #form - (ngSubmit)="submit()" + (ngSubmit)="submit(false)" [appApiAction]="formPromise" class="tw-container tw-mx-auto" [formGroup]="formGroup" @@ -91,7 +91,7 @@ class="-tw-mt-2" routerLink="/hint" (mousedown)="goToHint()" - (click)="setFormValues()" + (click)="setLoginEmailValues()" >{{ "getMasterPasswordHint" | i18n }}</a > </div> 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/two-factor.component.ts b/apps/web/src/app/auth/two-factor.component.ts index 6760ab449f..65bf1dba58 100644 --- a/apps/web/src/app/auth/two-factor.component.ts +++ b/apps/web/src/app/auth/two-factor.component.ts @@ -6,10 +6,10 @@ 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 { 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"; @@ -46,7 +46,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnDest logService: LogService, twoFactorService: TwoFactorService, appIdService: AppIdService, - loginService: LoginService, + loginEmailService: LoginEmailServiceAbstraction, userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction, configService: ConfigService, @@ -65,7 +65,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnDest logService, twoFactorService, appIdService, - loginService, + loginEmailService, userDecryptionOptionsService, ssoLoginService, configService, @@ -103,7 +103,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/core/core.module.ts b/apps/web/src/app/core/core.module.ts index e2d3f64f2d..bd514b1d18 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -16,8 +16,6 @@ import { 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 +27,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"; @@ -117,11 +116,6 @@ import { WebPlatformUtilsService } from "./web-platform-utils.service"; provide: FileDownloadService, useClass: WebFileDownloadService, }, - { - provide: LoginServiceAbstraction, - useClass: LoginService, - deps: [StateService], - }, CollectionAdminService, { provide: OBSERVABLE_DISK_LOCAL_STORAGE, 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..6bb545c4b5 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,6 +15,7 @@ import { } from "rxjs"; import { + LoginEmailServiceAbstraction, UserDecryptionOptions, UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; @@ -23,7 +24,6 @@ import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-conso import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.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"; @@ -82,7 +82,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, @@ -244,23 +244,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() { 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/login-via-auth-request.component.ts b/libs/angular/src/auth/components/login-via-auth-request.component.ts index b1d0b81922..66b7c1918c 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 @@ -6,12 +6,12 @@ import { AuthRequestLoginCredentials, AuthRequestServiceAbstraction, LoginStrategyServiceAbstraction, + LoginEmailServiceAbstraction, } 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 { 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"; @@ -83,7 +83,7 @@ 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, @@ -94,7 +94,7 @@ 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 @@ -151,7 +151,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")); @@ -472,17 +472,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/two-factor.component.spec.ts b/libs/angular/src/auth/components/two-factor.component.spec.ts index 9703c7e703..bff39188ea 100644 --- a/libs/angular/src/auth/components/two-factor.component.spec.ts +++ b/libs/angular/src/auth/components/two-factor.component.spec.ts @@ -7,14 +7,14 @@ 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 { 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"; @@ -59,7 +59,7 @@ describe("TwoFactorComponent", () => { let mockLogService: MockProxy<LogService>; let mockTwoFactorService: MockProxy<TwoFactorService>; let mockAppIdService: MockProxy<AppIdService>; - let mockLoginService: MockProxy<LoginService>; + let mockLoginEmailService: MockProxy<LoginEmailServiceAbstraction>; let mockUserDecryptionOptionsService: MockProxy<UserDecryptionOptionsServiceAbstraction>; let mockSsoLoginService: MockProxy<SsoLoginServiceAbstraction>; let mockConfigService: MockProxy<ConfigService>; @@ -89,7 +89,7 @@ describe("TwoFactorComponent", () => { mockLogService = mock<LogService>(); mockTwoFactorService = mock<TwoFactorService>(); mockAppIdService = mock<AppIdService>(); - mockLoginService = mock<LoginService>(); + mockLoginEmailService = mock<LoginEmailServiceAbstraction>(); mockUserDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>(); mockSsoLoginService = mock<SsoLoginServiceAbstraction>(); mockConfigService = mock<ConfigService>(); @@ -163,7 +163,7 @@ 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, @@ -280,11 +280,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(); diff --git a/libs/angular/src/auth/components/two-factor.component.ts b/libs/angular/src/auth/components/two-factor.component.ts index f64e591fa2..c306e6cc80 100644 --- a/libs/angular/src/auth/components/two-factor.component.ts +++ b/libs/angular/src/auth/components/two-factor.component.ts @@ -8,12 +8,12 @@ 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 { 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"; @@ -88,7 +88,7 @@ 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: ConfigService, @@ -288,7 +288,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)) { diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 841edb4289..a31d5141c4 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -7,6 +7,8 @@ import { PinCryptoService, LoginStrategyServiceAbstraction, LoginStrategyService, + LoginEmailServiceAbstraction, + LoginEmailService, InternalUserDecryptionOptionsServiceAbstraction, UserDecryptionOptionsService, UserDecryptionOptionsServiceAbstraction, @@ -58,7 +60,6 @@ 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 { 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"; @@ -77,7 +78,6 @@ 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 { 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"; @@ -874,9 +874,9 @@ const safeProviders: SafeProvider[] = [ deps: [I18nServiceAbstraction, PlatformUtilsServiceAbstraction], }), safeProvider({ - provide: LoginServiceAbstraction, - useClass: LoginService, - deps: [StateServiceAbstraction], + provide: LoginEmailServiceAbstraction, + useClass: LoginEmailService, + deps: [StateProvider], }), safeProvider({ provide: OrgDomainInternalServiceAbstraction, 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<string>; + /** + * 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<void>; +} 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<string>(LOGIN_EMAIL_DISK, "storedEmail", { + deserializer: (value: string) => value, +}); + +export class LoginEmailService implements LoginEmailServiceAbstraction { + private email: string; + private rememberEmail: boolean; + + private readonly storedEmailState: GlobalState<string>; + storedEmail$: Observable<string>; + + 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/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<void>; -} 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/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index 0ca0615380..79dc83868e 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -262,8 +262,6 @@ export abstract class StateService<T extends Account = Account> { * Sets the user's Pin, encrypted by the user key */ setProtectedPin: (value: string, options?: StorageOptions) => Promise<void>; - getRememberedEmail: (options?: StorageOptions) => Promise<string>; - setRememberedEmail: (value: string, options?: StorageOptions) => Promise<void>; getSecurityStamp: (options?: StorageOptions) => Promise<string>; setSecurityStamp: (value: string, options?: StorageOptions) => Promise<void>; getUserId: (options?: StorageOptions) => Promise<string>; diff --git a/libs/common/src/platform/models/domain/global-state.ts b/libs/common/src/platform/models/domain/global-state.ts index 7e35606e26..b0a59e4617 100644 --- a/libs/common/src/platform/models/domain/global-state.ts +++ b/libs/common/src/platform/models/domain/global-state.ts @@ -3,7 +3,6 @@ import { ThemeType } from "../../enums"; export class GlobalState { installedVersion?: string; organizationInvitation?: any; - rememberedEmail?: string; theme?: ThemeType = ThemeType.System; twoFactorToken?: string; biometricFingerprintValidated?: boolean; diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index d4297ecf94..c0b2a8fa2e 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -1241,23 +1241,6 @@ export class StateService< ); } - async getRememberedEmail(options?: StorageOptions): Promise<string> { - return ( - await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())) - )?.rememberedEmail; - } - - async setRememberedEmail(value: string, options?: StorageOptions): Promise<void> { - 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<string> { return ( await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index 35714ee7c4..814bf0280f 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -38,13 +38,16 @@ export const BILLING_DISK = new StateDefinition("billing", "disk"); export const KEY_CONNECTOR_DISK = new StateDefinition("keyConnector", "disk"); export const ACCOUNT_MEMORY = new StateDefinition("account", "memory"); export const AVATAR_DISK = new StateDefinition("avatar", "disk", { web: "disk-local" }); +export const LOGIN_EMAIL_DISK = new StateDefinition("loginEmail", "disk", { + web: "disk-local", +}); +export const LOGIN_STRATEGY_MEMORY = new StateDefinition("loginStrategy", "memory"); 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 USER_DECRYPTION_OPTIONS_DISK = new StateDefinition("userDecryptionOptions", "disk"); // Autofill diff --git a/libs/common/src/state-migrations/migrate.ts b/libs/common/src/state-migrations/migrate.ts index 60bd31d049..5222ee7ad7 100644 --- a/libs/common/src/state-migrations/migrate.ts +++ b/libs/common/src/state-migrations/migrate.ts @@ -47,6 +47,7 @@ import { MoveDdgToStateProviderMigrator } from "./migrations/48-move-ddg-to-stat 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 { 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"; @@ -54,7 +55,7 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting import { MinVersionMigrator } from "./migrations/min-version"; export const MIN_VERSION = 3; -export const CURRENT_VERSION = 50; +export const CURRENT_VERSION = 51; export type MinVersion = typeof MIN_VERSION; @@ -107,7 +108,8 @@ export function createMigrationBuilder() { .with(MoveDesktopSettingsMigrator, 46, 47) .with(MoveDdgToStateProviderMigrator, 47, 48) .with(AccountServerConfigMigrator, 48, 49) - .with(KeyConnectorMigrator, 49, CURRENT_VERSION); + .with(KeyConnectorMigrator, 49, 50) + .with(RememberedEmailMigrator, 50, CURRENT_VERSION); } export async function currentVersion( 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<MigrationHelper>; + 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<void> { + const legacyGlobal = await helper.get<ExpectedGlobalState>("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<void> { + let legacyGlobal = await helper.get<ExpectedGlobalState>("global"); + let updatedLegacyGlobal = false; + const globalStoredEmail = await helper.getFromGlobal<string>(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); + } + } +} From 8803f87994e2090251b5275422a69d48e912bba3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 1 Apr 2024 11:09:00 -0400 Subject: [PATCH 068/351] [deps] Autofill: Update prettier-plugin-tailwindcss to v0.5.13 (#8558) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 15 ++++++++------- package.json | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5368749853..0fb4d3e36a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -161,7 +161,7 @@ "postcss": "8.4.35", "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", @@ -31723,9 +31723,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" @@ -31735,6 +31735,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": "*", @@ -31760,6 +31761,9 @@ "@trivago/prettier-plugin-sort-imports": { "optional": true }, + "@zackad/prettier-plugin-twig-melody": { + "optional": true + }, "prettier-plugin-astro": { "optional": true }, @@ -31789,9 +31793,6 @@ }, "prettier-plugin-svelte": { "optional": true - }, - "prettier-plugin-twig-melody": { - "optional": true } } }, diff --git a/package.json b/package.json index 86026c6e93..9f85b42251 100644 --- a/package.json +++ b/package.json @@ -122,7 +122,7 @@ "postcss": "8.4.35", "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", From 4db7cf915551b95851f7a7c928f2751544cfbee8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 1 Apr 2024 11:10:11 -0400 Subject: [PATCH 069/351] [deps] Autofill: Update tldts to v6.1.16 (#8559) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- apps/cli/package.json | 2 +- package-lock.json | 18 +++++++++--------- package.json | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/cli/package.json b/apps/cli/package.json index 2de11df4e6..690842d831 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -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/package-lock.json b/package-lock.json index 0fb4d3e36a..96c2d69459 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" @@ -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": { @@ -36529,20 +36529,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", diff --git a/package.json b/package.json index 9f85b42251..5a67feb253 100644 --- a/package.json +++ b/package.json @@ -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" From c3c895230fcae65d46c31eeb2282503789ebdd4d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 1 Apr 2024 11:54:27 -0700 Subject: [PATCH 070/351] [deps] SM: Update typescript-eslint monorepo to v7 (#8116) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 156 +++++++++++++++++++++++----------------------- package.json | 4 +- 2 files changed, 80 insertions(+), 80 deletions(-) diff --git a/package-lock.json b/package-lock.json index 96c2d69459..ef23904967 100644 --- a/package-lock.json +++ b/package-lock.json @@ -117,8 +117,8 @@ "@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", @@ -11982,16 +11982,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", @@ -12000,15 +12000,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": { @@ -12017,16 +12017,16 @@ } }, "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==", + "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": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0" + "@typescript-eslint/types": "7.4.0", + "@typescript-eslint/visitor-keys": "7.4.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -12034,25 +12034,25 @@ } }, "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" + "eslint": "^8.56.0" }, "peerDependenciesMeta": { "typescript": { @@ -12061,12 +12061,12 @@ } }, "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==", + "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": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -12074,13 +12074,13 @@ } }, "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==", + "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": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", + "@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", @@ -12089,7 +12089,7 @@ "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -12102,41 +12102,41 @@ } }, "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" + "eslint": "^8.56.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==", + "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": "6.21.0", + "@typescript-eslint/types": "7.4.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -12235,26 +12235,26 @@ } }, "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": { @@ -12263,16 +12263,16 @@ } }, "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==", + "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": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0" + "@typescript-eslint/types": "7.4.0", + "@typescript-eslint/visitor-keys": "7.4.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -12280,12 +12280,12 @@ } }, "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==", + "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": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -12293,13 +12293,13 @@ } }, "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==", + "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": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", + "@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", @@ -12308,7 +12308,7 @@ "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -12321,16 +12321,16 @@ } }, "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==", + "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": "6.21.0", + "@typescript-eslint/types": "7.4.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", diff --git a/package.json b/package.json index 5a67feb253..88ba36e3c0 100644 --- a/package.json +++ b/package.json @@ -78,8 +78,8 @@ "@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", From 136226b6beb0dc69ac7b46c9f5a02a6fbf184126 Mon Sep 17 00:00:00 2001 From: Matt Gibson <mgibson@bitwarden.com> Date: Mon, 1 Apr 2024 14:15:54 -0500 Subject: [PATCH 071/351] Observable auth statuses (#8537) * Observable has token * Allow access to user key state observable * Create observable auth status * Fix DI --- .../service-factories/auth-service.factory.ts | 5 +- .../browser/src/background/main.background.ts | 1 + apps/cli/src/bw.ts | 1 + .../src/services/jslib-services.module.ts | 1 + .../src/auth/abstractions/auth.service.ts | 7 + .../src/auth/abstractions/token.service.ts | 7 + .../src/auth/services/auth.service.spec.ts | 144 +++++++++++++++--- libs/common/src/auth/services/auth.service.ts | 41 ++++- .../src/auth/services/token.service.spec.ts | 56 +++++++ .../common/src/auth/services/token.service.ts | 11 +- .../platform/abstractions/crypto.service.ts | 8 + .../src/platform/services/crypto.service.ts | 4 + 12 files changed, 260 insertions(+), 26 deletions(-) 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 bc4e621bc6..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 @@ -24,6 +24,7 @@ import { } 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; @@ -32,7 +33,8 @@ export type AuthServiceInitOptions = AuthServiceFactoryOptions & MessagingServiceInitOptions & CryptoServiceInitOptions & ApiServiceInitOptions & - StateServiceInitOptions; + StateServiceInitOptions & + TokenServiceInitOptions; export function authServiceFactory( cache: { authService?: AbstractAuthService } & CachedServices, @@ -49,6 +51,7 @@ export function authServiceFactory( await cryptoServiceFactory(cache, opts), await apiServiceFactory(cache, opts), await stateServiceFactory(cache, opts), + await tokenServiceFactory(cache, opts), ), ); } diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index ee17a7f1f0..49b4b96249 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -579,6 +579,7 @@ export default class MainBackground { this.cryptoService, this.apiService, this.stateService, + this.tokenService, ); this.billingAccountProfileStateService = new DefaultBillingAccountProfileStateService( diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index 7f23e6f2d0..d1105427f6 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -503,6 +503,7 @@ export class Main { this.cryptoService, this.apiService, this.stateService, + this.tokenService, ); this.configApiService = new ConfigApiService(this.apiService, this.tokenService); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index a31d5141c4..b08c53ec06 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -349,6 +349,7 @@ const safeProviders: SafeProvider[] = [ CryptoServiceAbstraction, ApiServiceAbstraction, StateServiceAbstraction, + TokenService, ], }), safeProvider({ diff --git a/libs/common/src/auth/abstractions/auth.service.ts b/libs/common/src/auth/abstractions/auth.service.ts index 9e4fd3cd0b..de08dbd4e9 100644 --- a/libs/common/src/auth/abstractions/auth.service.ts +++ b/libs/common/src/auth/abstractions/auth.service.ts @@ -1,10 +1,17 @@ import { Observable } from "rxjs"; +import { UserId } from "../../types/guid"; import { AuthenticationStatus } from "../enums/authentication-status"; export abstract class AuthService { /** Authentication status for the active user */ abstract activeAccountStatus$: Observable<AuthenticationStatus>; + /** + * 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<AuthenticationStatus>; /** @deprecated use {@link activeAccountStatus$} instead */ abstract getAuthStatus: (userId?: string) => Promise<AuthenticationStatus>; abstract logOut: (callback: () => void) => void; diff --git a/libs/common/src/auth/abstractions/token.service.ts b/libs/common/src/auth/abstractions/token.service.ts index 18366c5f1b..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<boolean>; /** * 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. diff --git a/libs/common/src/auth/services/auth.service.spec.ts b/libs/common/src/auth/services/auth.service.spec.ts index dd4daf8cfa..07e38def4b 100644 --- a/libs/common/src/auth/services/auth.service.spec.ts +++ b/libs/common/src/auth/services/auth.service.spec.ts @@ -1,13 +1,21 @@ import { MockProxy, mock } from "jest-mock-extended"; -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, of } from "rxjs"; -import { FakeAccountService, mockAccountServiceWith } from "../../../spec"; +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"; @@ -20,15 +28,18 @@ describe("AuthService", () => { let cryptoService: MockProxy<CryptoService>; let apiService: MockProxy<ApiService>; let stateService: MockProxy<StateService>; + let tokenService: MockProxy<TokenService>; const userId = Utils.newGuid() as UserId; + const userKey = new SymmetricCryptoKey(makeStaticByteArray(32) as Uint8Array) as UserKey; beforeEach(() => { accountService = mockAccountServiceWith(userId); - messagingService = mock<MessagingService>(); - cryptoService = mock<CryptoService>(); - apiService = mock<ApiService>(); - stateService = mock<StateService>(); + messagingService = mock(); + cryptoService = mock(); + apiService = mock(); + stateService = mock(); + tokenService = mock(); sut = new AuthService( accountService, @@ -36,26 +47,115 @@ describe("AuthService", () => { cryptoService, apiService, stateService, + tokenService, ); }); describe("activeAccountStatus$", () => { - test.each([ - AuthenticationStatus.LoggedOut, - AuthenticationStatus.Locked, - AuthenticationStatus.Unlocked, - ])( - `should emit %p when activeAccount$ emits an account with %p auth status`, - async (status) => { - accountService.activeAccountSubject.next({ - id: userId, - email: "email", - name: "name", - status, - }); + const accountInfo = { + status: AuthenticationStatus.Unlocked, + id: userId, + email: "email", + name: "name", + }; - expect(await firstValueFrom(sut.activeAccountStatus$)).toEqual(status); - }, - ); + 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("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 ae5dd30a36..de5eb66c06 100644 --- a/libs/common/src/auth/services/auth.service.ts +++ b/libs/common/src/auth/services/auth.service.ts @@ -1,12 +1,22 @@ -import { Observable, distinctUntilChanged, map, shareReplay } from "rxjs"; +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 { @@ -18,9 +28,36 @@ export class AuthService implements AuthServiceAbstraction { protected cryptoService: CryptoService, protected apiService: ApiService, protected stateService: StateService, + private tokenService: TokenService, ) { this.activeAccountStatus$ = this.accountService.activeAccount$.pipe( - map((account) => account.status), + map((account) => account?.id), + switchMap((userId) => { + return this.authStatusFor$(userId); + }), + ); + } + + authStatusFor$(userId: UserId): Observable<AuthenticationStatus> { + 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 }), ); diff --git a/libs/common/src/auth/services/token.service.spec.ts b/libs/common/src/auth/services/token.service.spec.ts index 8e8ed08853..c409263209 100644 --- a/libs/common/src/auth/services/token.service.spec.ts +++ b/libs/common/src/auth/services/token.service.spec.ts @@ -1,4 +1,5 @@ import { MockProxy, mock } from "jest-mock-extended"; +import { firstValueFrom } from "rxjs"; import { FakeSingleUserStateProvider, FakeGlobalStateProvider } from "../../../spec"; import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; @@ -104,6 +105,61 @@ describe("TokenService", () => { 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 () => { // Act diff --git a/libs/common/src/auth/services/token.service.ts b/libs/common/src/auth/services/token.service.ts index dd011eb40b..fb13c21870 100644 --- a/libs/common/src/auth/services/token.service.ts +++ b/libs/common/src/auth/services/token.service.ts @@ -1,4 +1,4 @@ -import { firstValueFrom } from "rxjs"; +import { Observable, combineLatest, firstValueFrom, map } from "rxjs"; import { Opaque } from "type-fest"; import { decodeJwtTokenToJson } from "@bitwarden/auth/common"; @@ -135,6 +135,15 @@ export class TokenService implements TokenServiceAbstraction { this.initializeState(); } + hasAccessToken$(userId: UserId): Observable<boolean> { + // 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. diff --git a/libs/common/src/platform/abstractions/crypto.service.ts b/libs/common/src/platform/abstractions/crypto.service.ts index 44ff521680..85b2bfe82e 100644 --- a/libs/common/src/platform/abstractions/crypto.service.ts +++ b/libs/common/src/platform/abstractions/crypto.service.ts @@ -13,6 +13,14 @@ import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; export abstract class CryptoService { abstract activeUserKey$: Observable<UserKey>; + + /** + * 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<UserKey>; /** * Sets the provided user key and stores * any other necessary versions (such as auto, biometrics, diff --git a/libs/common/src/platform/services/crypto.service.ts b/libs/common/src/platform/services/crypto.service.ts index fbb6a85293..dd3c497470 100644 --- a/libs/common/src/platform/services/crypto.service.ts +++ b/libs/common/src/platform/services/crypto.service.ts @@ -160,6 +160,10 @@ export class CryptoService implements CryptoServiceAbstraction { await this.setUserKey(key); } + getInMemoryUserKeyFor$(userId: UserId): Observable<UserKey> { + return this.stateProvider.getUserState$(USER_KEY, userId); + } + async getUserKey(userId?: UserId): Promise<UserKey> { let userKey = await firstValueFrom(this.stateProvider.getUserState$(USER_KEY, userId)); if (userKey) { From 45f9f5695ea26e5ce1cce313c8eccc2063289190 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Mon, 1 Apr 2024 14:29:04 -0500 Subject: [PATCH 072/351] Add Custom `ErrorHandler` (#8543) --- .../src/platform/services/logging-error-handler.ts | 14 ++++++++++++++ libs/angular/src/services/jslib-services.module.ts | 8 +++++++- 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 libs/angular/src/platform/services/logging-error-handler.ts 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..81cd537e7f --- /dev/null +++ b/libs/angular/src/platform/services/logging-error-handler.ts @@ -0,0 +1,14 @@ +import { ErrorHandler, Injectable } from "@angular/core"; + +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; + +@Injectable() +export class LoggingErrorHandler extends ErrorHandler { + constructor(private readonly logService: LogService) { + super(); + } + + override handleError(error: any): void { + this.logService.error(error); + } +} diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index b08c53ec06..c9a39eed0a 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -1,4 +1,4 @@ -import { LOCALE_ID, NgModule } from "@angular/core"; +import { ErrorHandler, LOCALE_ID, NgModule } from "@angular/core"; import { AuthRequestServiceAbstraction, @@ -238,6 +238,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"; @@ -1070,6 +1071,11 @@ const safeProviders: SafeProvider[] = [ useClass: DefaultOrganizationManagementPreferencesService, deps: [StateProvider], }), + safeProvider({ + provide: ErrorHandler, + useClass: LoggingErrorHandler, + deps: [LogService], + }), ]; function encryptServiceFactory( From bd7c10705d482a04415ea6114325c5eefbf12827 Mon Sep 17 00:00:00 2001 From: Jake Fink <jfink@bitwarden.com> Date: Mon, 1 Apr 2024 15:32:11 -0400 Subject: [PATCH 073/351] add clarification around null in state provider (#8567) * add clarification around null in state provider * Update libs/common/src/platform/state/user-state.ts Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> --------- Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> --- libs/common/src/platform/state/user-state.ts | 21 ++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) 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<T> = 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<T> { - /** - * Emits a stream of data. - */ - readonly state$: Observable<T>; + /** Emits a stream of data. Emits null if the user does not have specified state. */ + readonly state$: Observable<T | null>; - /** - * 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<CombinedState<T>>; } export const activeMarker: unique symbol = Symbol("active"); export interface ActiveUserState<T> extends UserState<T> { 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<T | null>; + /** * Updates backing stores for the active user. * @param configureState function that takes the current state and returns the new state From 94843bdd8b1101faddf0c385db827b77d05294c8 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Mon, 1 Apr 2024 14:36:39 -0500 Subject: [PATCH 074/351] [PM-5956] Delete Unused State (#8439) * Delete Unused State * Delete One More * Add Migration to Delete InstalledVersion * Update Error --- apps/cli/src/bw.ts | 6 --- apps/desktop/src/app/services/init.service.ts | 12 ----- .../platform/abstractions/state.service.ts | 6 --- .../src/platform/models/domain/account.ts | 10 ---- .../platform/models/domain/global-state.ts | 8 --- .../src/platform/services/state.service.ts | 53 ------------------- libs/common/src/state-migrations/migrate.ts | 6 ++- .../52-delete-installed-version.spec.ts | 35 ++++++++++++ .../migrations/52-delete-installed-version.ts | 19 +++++++ 9 files changed, 58 insertions(+), 97 deletions(-) create mode 100644 libs/common/src/state-migrations/migrations/52-delete-installed-version.spec.ts create mode 100644 libs/common/src/state-migrations/migrations/52-delete-installed-version.ts diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index d1105427f6..bba381b84a 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -713,12 +713,6 @@ export class Main { this.containerService.attachToGlobal(global); await this.i18nService.init(); this.twoFactorService.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/desktop/src/app/services/init.service.ts b/apps/desktop/src/app/services/init.service.ts index bb7d4e7b52..d1a83d468c 100644 --- a/apps/desktop/src/app/services/init.service.ts +++ b/apps/desktop/src/app/services/init.service.ts @@ -53,18 +53,6 @@ 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); diff --git a/libs/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index 79dc83868e..ab8b548951 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -50,8 +50,6 @@ export abstract class StateService<T extends Account = Account> { getAddEditCipherInfo: (options?: StorageOptions) => Promise<AddEditCipherInfo>; setAddEditCipherInfo: (value: AddEditCipherInfo, options?: StorageOptions) => Promise<void>; - getBiometricFingerprintValidated: (options?: StorageOptions) => Promise<boolean>; - setBiometricFingerprintValidated: (value: boolean, options?: StorageOptions) => Promise<void>; /** * Gets the user's master key */ @@ -161,8 +159,6 @@ export abstract class StateService<T extends Account = Account> { * @deprecated Do not call this directly, use SendService */ setDecryptedSends: (value: SendView[], options?: StorageOptions) => Promise<void>; - getDisableGa: (options?: StorageOptions) => Promise<boolean>; - setDisableGa: (value: boolean, options?: StorageOptions) => Promise<void>; getDuckDuckGoSharedKey: (options?: StorageOptions) => Promise<string>; setDuckDuckGoSharedKey: (value: string, options?: StorageOptions) => Promise<void>; getDeviceKey: (options?: StorageOptions) => Promise<DeviceKey | null>; @@ -220,8 +216,6 @@ export abstract class StateService<T extends Account = Account> { value: ForceSetPasswordReason, options?: StorageOptions, ) => Promise<void>; - getInstalledVersion: (options?: StorageOptions) => Promise<string>; - setInstalledVersion: (value: string, options?: StorageOptions) => Promise<void>; getIsAuthenticated: (options?: StorageOptions) => Promise<boolean>; getKdfConfig: (options?: StorageOptions) => Promise<KdfConfig>; setKdfConfig: (kdfConfig: KdfConfig, options?: StorageOptions) => Promise<void>; diff --git a/libs/common/src/platform/models/domain/account.ts b/libs/common/src/platform/models/domain/account.ts index 01660006c0..61bb3eeac5 100644 --- a/libs/common/src/platform/models/domain/account.ts +++ b/libs/common/src/platform/models/domain/account.ts @@ -65,13 +65,6 @@ export class DataEncryptionPair<TEncrypted, TDecrypted> { 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<TEncrypted> { - encrypted?: { [id: string]: TEncrypted }; -} - export class AccountData { ciphers?: DataEncryptionPair<CipherData, CipherView> = new DataEncryptionPair< CipherData, @@ -182,8 +175,6 @@ export class AccountProfile { export class AccountSettings { defaultUriMatch?: UriMatchStrategySetting; - disableGa?: boolean; - enableBiometric?: boolean; minimizeOnCopyToClipboard?: boolean; passwordGenerationOptions?: PasswordGeneratorOptions; usernameGenerationOptions?: UsernameGeneratorOptions; @@ -194,7 +185,6 @@ export class AccountSettings { vaultTimeout?: number; vaultTimeoutAction?: string = "lock"; approveLoginRequests?: boolean; - avatarColor?: string; trustDeviceChoiceForDecryption?: boolean; /** @deprecated July 2023, left for migration purposes*/ diff --git a/libs/common/src/platform/models/domain/global-state.ts b/libs/common/src/platform/models/domain/global-state.ts index b0a59e4617..cb9e3f71b3 100644 --- a/libs/common/src/platform/models/domain/global-state.ts +++ b/libs/common/src/platform/models/domain/global-state.ts @@ -1,15 +1,7 @@ -import { ThemeType } from "../../enums"; - export class GlobalState { - installedVersion?: string; organizationInvitation?: any; - theme?: ThemeType = ThemeType.System; - twoFactorToken?: string; - biometricFingerprintValidated?: boolean; vaultTimeout?: number; vaultTimeoutAction?: string; - loginRedirect?: any; - mainWindowSize?: number; enableBrowserIntegration?: boolean; enableBrowserIntegrationFingerprint?: boolean; deepLinkRedirectUrl?: string; diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index c0b2a8fa2e..0e524b6c4b 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -275,24 +275,6 @@ export class StateService< ); } - async getBiometricFingerprintValidated(options?: StorageOptions): Promise<boolean> { - return ( - (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) - ?.biometricFingerprintValidated ?? false - ); - } - - async setBiometricFingerprintValidated(value: boolean, options?: StorageOptions): Promise<void> { - const globals = await this.getGlobals( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - globals.biometricFingerprintValidated = value; - await this.saveGlobals( - globals, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - /** * @deprecated Do not save the Master Key. Use the User Symmetric Key instead */ @@ -650,24 +632,6 @@ export class StateService< ); } - async getDisableGa(options?: StorageOptions): Promise<boolean> { - return ( - (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) - ?.settings?.disableGa ?? false - ); - } - - async setDisableGa(value: boolean, options?: StorageOptions): Promise<void> { - 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<string> { options = this.reconcileOptions(options, await this.defaultSecureStorageOptions()); if (options?.userId == null) { @@ -982,23 +946,6 @@ export class StateService< ); } - async getInstalledVersion(options?: StorageOptions): Promise<string> { - return ( - await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.installedVersion; - } - - async setInstalledVersion(value: string, options?: StorageOptions): Promise<void> { - 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<boolean> { return ( (await this.tokenService.getAccessToken(options?.userId as UserId)) != null && diff --git a/libs/common/src/state-migrations/migrate.ts b/libs/common/src/state-migrations/migrate.ts index 5222ee7ad7..0758d49f59 100644 --- a/libs/common/src/state-migrations/migrate.ts +++ b/libs/common/src/state-migrations/migrate.ts @@ -48,6 +48,7 @@ import { AccountServerConfigMigrator } from "./migrations/49-move-account-server 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 { 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"; @@ -55,7 +56,7 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting import { MinVersionMigrator } from "./migrations/min-version"; export const MIN_VERSION = 3; -export const CURRENT_VERSION = 51; +export const CURRENT_VERSION = 52; export type MinVersion = typeof MIN_VERSION; @@ -109,7 +110,8 @@ export function createMigrationBuilder() { .with(MoveDdgToStateProviderMigrator, 47, 48) .with(AccountServerConfigMigrator, 48, 49) .with(KeyConnectorMigrator, 49, 50) - .with(RememberedEmailMigrator, 50, CURRENT_VERSION); + .with(RememberedEmailMigrator, 50, 51) + .with(DeleteInstalledVersion, 51, CURRENT_VERSION); } export async function currentVersion( 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<void> { + const legacyGlobal = await helper.get<ExpectedGlobal>("global"); + if (legacyGlobal?.installedVersion != null) { + delete legacyGlobal.installedVersion; + await helper.set("global", legacyGlobal); + } + } + rollback(helper: MigrationHelper): Promise<void> { + throw IRREVERSIBLE; + } +} From c202c93378f1d48e15767d74e902e4888210cf26 Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Mon, 1 Apr 2024 16:02:58 -0400 Subject: [PATCH 075/351] Auth/PM-5268 - DeviceTrustCryptoService state provider migration (#7882) * PM-5268 - Add DEVICE_TRUST_DISK to state definitions * PM-5268 - DeviceTrustCryptoService - Get most of state provider refactor done - WIP - commented out stuff for now. * PM-5268 - DeviceTrustCryptoServiceStateProviderMigrator - WIP - got first draft of migrator in place and working on tests. Rollback tests are failing for some reason TBD. * PM-5268 - more WIP on device trust crypto service migrator tests * PM-5268 - DeviceTrustCryptoServiceStateProviderMigrator - Refactor based on call with platform * PM-5268 - DeviceTrustCryptoServiceStateProviderMigrator - tests passing * PM-5268 - Update DeviceTrustCryptoService to convert over to state providers + update all service instantiations / dependencies to ensure state provider is passed in or injected. * PM-5268 - Register new migration * PM-5268 - Temporarily remove device trust crypto service from migrator to ease merge conflicts as there are 6 more migrators before I can apply mine in main. * PM-5268 - Update migration numbers of DeviceTrustCryptoServiceStateProviderMigrator based on latest migrations from main. * PM-5268 - (1) Export new KeyDefinitions from DeviceTrustCryptoService for use in test suite (2) Update DeviceTrustCryptoService test file to use state provider. * PM-5268 - Fix DeviceTrustCryptoServiceStateProviderMigrator tests to use proper versions * PM-5268 - Actually fix all instances of DeviceTrustCryptoServiceStateProviderMigrator test failures * PM-5268 - Clean up state service, account, and login strategy of all migrated references * PM-5268 - Account - finish cleaning up device key * PM-5268 - StateService - clean up last reference to device key * PM-5268 - Remove even more device key refs. *facepalm* * PM-5268 - Finish resolving merge conflicts by incrementing migration version from 22 to 23 * PM-5268 - bump migration versions * PM-5268 - DeviceTrustCryptoService - Implement secure storage functionality for getDeviceKey and setDeviceKey (to achieve feature parity with the ElectronStateService implementation prior to the state provider migration). Tests to follow shortly. * PM-5268 - DeviceTrustCryptoService tests - getDeviceKey now tested with all new secure storage scenarios. SetDeviceKey tests to follow. * PM-5268 - DeviceTrustCryptoService tests - test all setDeviceKey scenarios with state provider & secure storage * PM-5268 - Update DeviceTrustCryptoService deps to actually use secure storage svc on platforms that support it. * PM-5268 - Bump migration version due to merge conflicts. * PM-5268 - Bump migration version * PM-5268 - tweak jsdocs to be single line per PR feedback * PM-5268 - DeviceTrustCryptoSvc - improve debuggability. * PM-5268 - Remove state service as a dependency on the device trust crypto service (woo!) * PM-5268 - Update migration test json to correctly reflect reality. * PM-5268 - DeviceTrustCryptoSvc - getDeviceKey - add throw error for active user id missing. * PM-5268 - Fix tests * PM-5268 - WIP start on adding user id to every method on device trust crypto service. * PM-5268 - Update lock comp dependencies across clients * PM-5268 - Update login via auth request deps across clients to add acct service. * PM-5268 - UserKeyRotationSvc - add acct service to get active acct id for call to rotateDevicesTrust and then update tests. * PM-5268 - WIP on trying to fix device trust crypto svc tests. * PM-5268 - More WIP device trust crypto svc tests passing * PM-5268 - Device Trust crypto service - get all tests passing * PM-5268 - DeviceTrustCryptoService.getDeviceKey - fix secure storage b64 to symmetric crypto key conversion * PM-5268 - Add more tests and update test names * PM-5268 - rename state to indicate it was disk local * PM-5268 - DeviceTrustCryptoService - save symmetric key in JSON format * PM-5268 - Fix lock comp tests by adding acct service dep * PM-5268 - Update set device key tests to pass * PM-5268 - Bump migration versions again * PM-5268 - Fix user key rotation svc tests * PM-5268 - Update web jest config to allow use of common spec in user-key-rotation-svc tests * PM-5268 - Bump migration version * PM-5268 - Per PR feedback, save off user id * PM-5268 - bump migration version * PM-5268 - Per PR feedback, remove unnecessary await. * PM-5268 - Bump migration verson --- .../device-trust-crypto-service.factory.ts | 16 +- apps/browser/src/auth/popup/lock.component.ts | 3 + .../popup/login-via-auth-request.component.ts | 3 + .../browser/src/background/main.background.ts | 3 +- apps/cli/src/bw.ts | 3 +- apps/desktop/src/auth/lock.component.spec.ts | 11 + apps/desktop/src/auth/lock.component.ts | 3 + .../login/login-via-auth-request.component.ts | 3 + .../services/electron-state.service.ts | 35 -- apps/web/jest.config.js | 10 +- .../user-key-rotation.service.spec.ts | 7 + .../key-rotation/user-key-rotation.service.ts | 9 +- apps/web/src/app/auth/lock.component.ts | 3 + ...base-login-decryption-options.component.ts | 16 +- .../src/auth/components/lock.component.ts | 5 +- .../login-via-auth-request.component.ts | 7 +- .../src/services/jslib-services.module.ts | 3 +- .../auth-request-login.strategy.ts | 5 +- .../login-strategies/login.strategy.spec.ts | 25 +- .../common/login-strategies/login.strategy.ts | 12 - .../login-strategies/sso-login.strategy.ts | 9 +- ...device-trust-crypto.service.abstraction.ts | 22 +- ...ice-trust-crypto.service.implementation.ts | 138 +++++-- .../device-trust-crypto.service.spec.ts | 337 ++++++++++++------ .../platform/abstractions/state.service.ts | 6 +- .../models/domain/account-keys.spec.ts | 38 -- .../src/platform/models/domain/account.ts | 3 - .../src/platform/services/state.service.ts | 61 +--- .../src/platform/state/state-definitions.ts | 3 + libs/common/src/state-migrations/migrate.ts | 7 +- ...rust-crypto-svc-to-state-providers.spec.ts | 171 +++++++++ ...ice-trust-crypto-svc-to-state-providers.ts | 95 +++++ 32 files changed, 738 insertions(+), 334 deletions(-) create mode 100644 libs/common/src/state-migrations/migrations/53-migrate-device-trust-crypto-svc-to-state-providers.spec.ts create mode 100644 libs/common/src/state-migrations/migrations/53-migrate-device-trust-crypto-svc-to-state-providers.ts 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/popup/lock.component.ts b/apps/browser/src/auth/popup/lock.component.ts index f2c56a23ae..f232eca45a 100644 --- a/apps/browser/src/auth/popup/lock.component.ts +++ b/apps/browser/src/auth/popup/lock.component.ts @@ -9,6 +9,7 @@ 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 { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; @@ -62,6 +63,7 @@ export class LockComponent extends BaseLockComponent { pinCryptoService: PinCryptoServiceAbstraction, private routerService: BrowserRouterService, biometricStateService: BiometricStateService, + accountService: AccountService, ) { super( router, @@ -84,6 +86,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 8d438d5b78..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 @@ -9,6 +9,7 @@ import { 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"; @@ -49,6 +50,7 @@ export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent { deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, authRequestService: AuthRequestServiceAbstraction, loginStrategyService: LoginStrategyServiceAbstraction, + accountService: AccountService, private location: Location, ) { super( @@ -70,6 +72,7 @@ export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent { deviceTrustCryptoService, authRequestService, loginStrategyService, + accountService, ); super.onSuccessfulLogin = async () => { await syncService.fullSync(true); diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 49b4b96249..25befdcf80 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -556,11 +556,12 @@ export default class MainBackground { this.cryptoFunctionService, this.cryptoService, this.encryptService, - this.stateService, this.appIdService, this.devicesApiService, this.i18nService, this.platformUtilsService, + this.stateProvider, + this.secureStorageService, this.userDecryptionOptionsService, ); diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index bba381b84a..804b05e8e3 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -455,11 +455,12 @@ 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, ); diff --git a/apps/desktop/src/auth/lock.component.spec.ts b/apps/desktop/src/auth/lock.component.spec.ts index 6ecf93deb8..0339889bf7 100644 --- a/apps/desktop/src/auth/lock.component.spec.ts +++ b/apps/desktop/src/auth/lock.component.spec.ts @@ -12,6 +12,7 @@ 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 { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; @@ -23,7 +24,10 @@ 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 { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; +import { UserId } from "@bitwarden/common/types/guid"; import { DialogService } from "@bitwarden/components"; import { LockComponent } from "./lock.component"; @@ -49,6 +53,9 @@ describe("LockComponent", () => { let platformUtilsServiceMock: MockProxy<PlatformUtilsService>; let activatedRouteMock: MockProxy<ActivatedRoute>; + const mockUserId = Utils.newGuid() as UserId; + const accountService: FakeAccountService = mockAccountServiceWith(mockUserId); + beforeEach(async () => { stateServiceMock = mock<StateService>(); stateServiceMock.activeAccount$ = of(null); @@ -147,6 +154,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..8b1448c06f 100644 --- a/apps/desktop/src/auth/lock.component.ts +++ b/apps/desktop/src/auth/lock.component.ts @@ -9,6 +9,7 @@ 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 { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { DeviceType } from "@bitwarden/common/enums"; @@ -59,6 +60,7 @@ export class LockComponent extends BaseLockComponent { userVerificationService: UserVerificationService, pinCryptoService: PinCryptoServiceAbstraction, biometricStateService: BiometricStateService, + accountService: AccountService, ) { super( router, @@ -81,6 +83,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 28163d09d0..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 @@ -10,6 +10,7 @@ import { 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"; @@ -57,6 +58,7 @@ export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent { deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, authRequestService: AuthRequestServiceAbstraction, loginStrategyService: LoginStrategyServiceAbstraction, + accountService: AccountService, private location: Location, ) { super( @@ -78,6 +80,7 @@ export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent { deviceTrustCryptoService, authRequestService, loginStrategyService, + accountService, ); super.onSuccessfulLogin = () => { 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<GlobalState, Account> { - 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<DeviceKey | null> { - options = this.reconcileOptions(options, await this.defaultSecureStorageOptions()); - if (options?.userId == null) { - return; - } - - const b64DeviceKey = await this.secureStorageService.get<string>( - `${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<void> { - 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/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: ["<rootDir>/test.setup.ts"], - moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, { - prefix: "<rootDir>/", - }), + moduleNameMapper: pathsToModuleNameMapper( + // lets us use @bitwarden/common/spec in web tests + { "@bitwarden/common/spec": ["../../libs/common/spec"], ...(compilerOptions?.paths ?? {}) }, + { + prefix: "<rootDir>/", + }, + ), }; 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 7eabbbb5c1..09c7bf9ace 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 @@ -6,10 +6,13 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co 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 { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; 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"; @@ -41,6 +44,9 @@ describe("KeyRotationService", () => { let mockStateService: MockProxy<StateService>; let mockConfigService: MockProxy<ConfigService>; + const mockUserId = Utils.newGuid() as UserId; + const mockAccountService: FakeAccountService = mockAccountServiceWith(mockUserId); + beforeAll(() => { mockApiService = mock<UserKeyRotationApiService>(); mockCipherService = mock<CipherService>(); @@ -65,6 +71,7 @@ describe("KeyRotationService", () => { mockCryptoService, mockEncryptService, mockStateService, + mockAccountService, mockConfigService, ); }); 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 b53c71cb2e..03bc604b4d 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,6 +1,7 @@ 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 { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -34,6 +35,7 @@ export class UserKeyRotationService { private cryptoService: CryptoService, private encryptService: EncryptService, private stateService: StateService, + private accountService: AccountService, private configService: ConfigService, ) {} @@ -90,7 +92,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<EncryptedString | null> { diff --git a/apps/web/src/app/auth/lock.component.ts b/apps/web/src/app/auth/lock.component.ts index c4f8d276bb..a1d4724396 100644 --- a/apps/web/src/app/auth/lock.component.ts +++ b/apps/web/src/app/auth/lock.component.ts @@ -8,6 +8,7 @@ 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 { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -47,6 +48,7 @@ export class LockComponent extends BaseLockComponent { userVerificationService: UserVerificationService, pinCryptoService: PinCryptoServiceAbstraction, biometricStateService: BiometricStateService, + accountService: AccountService, ) { super( router, @@ -69,6 +71,7 @@ export class LockComponent extends BaseLockComponent { userVerificationService, pinCryptoService, biometricStateService, + accountService, ); } 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 6bb545c4b5..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 @@ -22,6 +22,7 @@ import { 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 { PasswordResetEnrollmentServiceAbstraction } from "@bitwarden/common/auth/abstractions/password-reset-enrollment.service.abstraction"; @@ -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], @@ -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$), ) @@ -278,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/lock.component.ts b/libs/angular/src/auth/components/lock.component.ts index c21ba1a75a..aa3b801ded 100644 --- a/libs/angular/src/auth/components/lock.component.ts +++ b/libs/angular/src/auth/components/lock.component.ts @@ -10,6 +10,7 @@ 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 { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; @@ -75,6 +76,7 @@ export class LockComponent implements OnInit, OnDestroy { protected userVerificationService: UserVerificationService, protected pinCryptoService: PinCryptoServiceAbstraction, protected biometricStateService: BiometricStateService, + protected accountService: AccountService, ) {} async ngOnInit() { @@ -269,7 +271,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); } 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 66b7c1918c..6ba94d3001 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,6 +1,6 @@ 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, @@ -9,6 +9,7 @@ import { 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"; @@ -87,6 +88,7 @@ export class LoginViaAuthRequestComponent private deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, private authRequestService: AuthRequestServiceAbstraction, private loginStrategyService: LoginStrategyServiceAbstraction, + private accountService: AccountService, ) { super(environmentService, i18nService, platformUtilsService); @@ -388,7 +390,8 @@ export class LoginViaAuthRequestComponent // 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 diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index c9a39eed0a..9a8a2bc6a2 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -912,11 +912,12 @@ const safeProviders: SafeProvider[] = [ CryptoFunctionServiceAbstraction, CryptoServiceAbstraction, EncryptService, - StateServiceAbstraction, AppIdServiceAbstraction, DevicesApiServiceAbstraction, I18nServiceAbstraction, PlatformUtilsServiceAbstraction, + StateProvider, + SECURE_STORAGE, UserDecryptionOptionsServiceAbstraction, ], }), 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..31a0cebbfe 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 @@ -16,6 +16,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"; @@ -128,8 +129,10 @@ export class AuthRequestLoginStrategy extends LoginStrategy { await this.cryptoService.setUserKey(authRequestCredentials.decryptedUserKey); } else { await this.trySetUserKeyWithMasterKey(); + + const userId = (await this.stateService.getUserId()) as UserId; // Establish trust if required after setting user key - await this.deviceTrustCryptoService.trustDeviceIfRequired(); + await this.deviceTrustCryptoService.trustDeviceIfRequired(userId); } } 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 42541808c8..0ac22047c5 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.spec.ts @@ -36,7 +36,7 @@ import { PasswordStrengthService, } from "@bitwarden/common/tools/password-strength"; import { CsprngArray } from "@bitwarden/common/types/csprng"; -import { UserKey, MasterKey, DeviceKey } from "@bitwarden/common/types/key"; +import { UserKey, MasterKey } from "@bitwarden/common/types/key"; import { LoginStrategyServiceAbstraction } from "../abstractions"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; @@ -215,29 +215,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; diff --git a/libs/auth/src/common/login-strategies/login.strategy.ts b/libs/auth/src/common/login-strategies/login.strategy.ts index 8e927c2cc4..4fe99b276c 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.ts @@ -26,7 +26,6 @@ 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, @@ -160,18 +159,8 @@ export abstract class LoginStrategy { protected async saveAccountInformation(tokenResponse: IdentityTokenResponse): Promise<void> { 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 }); @@ -204,7 +193,6 @@ export abstract class LoginStrategy { tokens: { ...new AccountTokens(), }, - keys: accountKeys, adminAuthRequest: adminAuthRequest?.toJSON(), }), ); 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..7745104bd1 100644 --- a/libs/auth/src/common/login-strategies/sso-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/sso-login.strategy.ts @@ -20,6 +20,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, @@ -284,7 +285,8 @@ 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(); + const userId = (await this.stateService.getUserId()) as UserId; + 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 @@ -298,7 +300,9 @@ export class SsoLoginStrategy extends LoginStrategy { private async trySetUserKeyWithDeviceKey(tokenResponse: IdentityTokenResponse): Promise<void> { 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 +311,7 @@ export class SsoLoginStrategy extends LoginStrategy { } const userKey = await this.deviceTrustCryptoService.decryptUserKeyWithDeviceKey( + userId, encDevicePrivateKey, encUserKey, deviceKey, 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<boolean | null>; - setShouldTrustDevice: (value: boolean) => Promise<void>; + getShouldTrustDevice: (userId: UserId) => Promise<boolean | null>; + setShouldTrustDevice: (userId: UserId, value: boolean) => Promise<void>; - trustDeviceIfRequired: () => Promise<void>; + trustDeviceIfRequired: (userId: UserId) => Promise<void>; - trustDevice: () => Promise<DeviceResponse>; - getDeviceKey: () => Promise<DeviceKey>; + trustDevice: (userId: UserId) => Promise<DeviceResponse>; + + /** Retrieves the device key if it exists from state or secure storage if supported for the active user. */ + getDeviceKey: (userId: UserId) => Promise<DeviceKey | null>; decryptUserKeyWithDeviceKey: ( + userId: UserId, encryptedDevicePrivateKey: EncString, encryptedUserKey: EncString, - deviceKey?: DeviceKey, + deviceKey: DeviceKey, ) => Promise<UserKey | null>; - rotateDevicesTrust: (newUserKey: UserKey, masterPasswordHash: string) => Promise<void>; + rotateDevicesTrust: ( + userId: UserId, + newUserKey: UserKey, + masterPasswordHash: string, + ) => Promise<void>; } 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..e65c5cd499 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, KeyDefinition, StateProvider } 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,25 @@ 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 KeyDefinition<DeviceKey>(DEVICE_TRUST_DISK_LOCAL, "deviceKey", { + deserializer: (deviceKey) => SymmetricCryptoKey.fromJSON(deviceKey) as DeviceKey, +}); + +/** Uses disk storage so that the shouldTrustDevice bool can persist across login. */ +export const SHOULD_TRUST_DEVICE = new KeyDefinition<boolean>( + DEVICE_TRUST_DISK_LOCAL, + "shouldTrustDevice", + { + deserializer: (shouldTrustDevice) => shouldTrustDevice, + }, +); + export class DeviceTrustCryptoService implements DeviceTrustCryptoServiceAbstraction { + private readonly platformSupportsSecureStorage = + this.platformUtilsService.supportsSecureStorage(); + private readonly deviceKeySecureStorageKey: string = "_deviceKey"; + supportsDeviceTrust$: Observable<boolean>; constructor( @@ -30,11 +52,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 +69,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<boolean> { - return await this.stateService.getShouldTrustDevice(); + async getShouldTrustDevice(userId: UserId): Promise<boolean> { + 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<void> { - await this.stateService.setShouldTrustDevice(value); + async setShouldTrustDevice(userId: UserId, value: boolean): Promise<void> { + 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<void> { - const shouldTrustDevice = await this.getShouldTrustDevice(); + async trustDeviceIfRequired(userId: UserId): Promise<void> { + 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<DeviceResponse> { + async trustDevice(userId: UserId): Promise<DeviceResponse> { + 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 +147,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<void> { - const currentDeviceKey = await this.getDeviceKey(); + async rotateDevicesTrust( + userId: UserId, + newUserKey: UserKey, + masterPasswordHash: string, + ): Promise<void> { + 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 +216,59 @@ export class DeviceTrustCryptoService implements DeviceTrustCryptoServiceAbstrac await this.devicesApiService.updateTrust(trustRequest, deviceIdentifier); } - async getDeviceKey(): Promise<DeviceKey> { - return await this.stateService.getDeviceKey(); + async getDeviceKey(userId: UserId): Promise<DeviceKey | null> { + if (!userId) { + throw new Error("UserId is required. Cannot get device key."); + } + + if (this.platformSupportsSecureStorage) { + const deviceKeyB64 = await this.secureStorageService.get< + ReturnType<SymmetricCryptoKey["toJSON"]> + >(`${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<void> { - await this.stateService.setDeviceKey(deviceKey); + private async setDeviceKey(userId: UserId, deviceKey: DeviceKey | null): Promise<void> { + if (!userId) { + throw new Error("UserId is required. Cannot set device key."); + } + + if (this.platformSupportsSecureStorage) { + await this.secureStorageService.save<DeviceKey>( + `${userId}${this.deviceKeySecureStorageKey}`, + deviceKey, + this.getSecureStorageOptions(userId), + ); + return; + } + + await this.stateProvider.setUserState(DEVICE_KEY, deviceKey?.toJSON(), userId); } private async makeDeviceKey(): Promise<DeviceKey> { // 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<UserKey | null> { - // 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 +291,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<CryptoFunctionService>(); const cryptoService = mock<CryptoService>(); const encryptService = mock<EncryptService>(); - const stateService = mock<StateService>(); const appIdService = mock<AppIdService>(); const devicesApiService = mock<DevicesApiServiceAbstraction>(); const i18nService = mock<I18nService>(); const platformUtilsService = mock<PlatformUtilsService>(); - const userDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>(); + const secureStorageService = mock<AbstractStorageService>(); + const userDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>(); const decryptionOptions = new BehaviorSubject<UserDecryptionOptions>(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<DeviceKey> = + 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/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index ab8b548951..9bc6d698a7 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -10,7 +10,7 @@ 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 { 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"; @@ -161,15 +161,11 @@ export abstract class StateService<T extends Account = Account> { setDecryptedSends: (value: SendView[], options?: StorageOptions) => Promise<void>; getDuckDuckGoSharedKey: (options?: StorageOptions) => Promise<string>; setDuckDuckGoSharedKey: (value: string, options?: StorageOptions) => Promise<void>; - getDeviceKey: (options?: StorageOptions) => Promise<DeviceKey | null>; - setDeviceKey: (value: DeviceKey | null, options?: StorageOptions) => Promise<void>; getAdminAuthRequest: (options?: StorageOptions) => Promise<AdminAuthRequestStorable | null>; setAdminAuthRequest: ( adminAuthRequest: AdminAuthRequestStorable, options?: StorageOptions, ) => Promise<void>; - getShouldTrustDevice: (options?: StorageOptions) => Promise<boolean | null>; - setShouldTrustDevice: (value: boolean, options?: StorageOptions) => Promise<void>; getEmail: (options?: StorageOptions) => Promise<string>; setEmail: (value: string, options?: StorageOptions) => Promise<void>; getEmailVerified: (options?: StorageOptions) => Promise<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..4a96da1b48 100644 --- a/libs/common/src/platform/models/domain/account-keys.spec.ts +++ b/libs/common/src/platform/models/domain/account-keys.spec.ts @@ -1,6 +1,4 @@ import { makeStaticByteArray } from "../../../../spec"; -import { CsprngArray } from "../../../types/csprng"; -import { DeviceKey } from "../../../types/key"; import { Utils } from "../../misc/utils"; import { AccountKeys, EncryptionPair } from "./account"; @@ -24,23 +22,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", () => { @@ -64,24 +45,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 61bb3eeac5..798a60600a 100644 --- a/libs/common/src/platform/models/domain/account.ts +++ b/libs/common/src/platform/models/domain/account.ts @@ -95,7 +95,6 @@ export class AccountData { export class AccountKeys { masterKey?: MasterKey; masterKeyEncryptedUserKey?: string; - deviceKey?: ReturnType<SymmetricCryptoKey["toJSON"]>; publicKey?: Uint8Array; /** @deprecated July 2023, left for migration purposes*/ @@ -125,7 +124,6 @@ export class AccountKeys { } return Object.assign(new AccountKeys(), obj, { masterKey: SymmetricCryptoKey.fromJSON(obj?.masterKey), - deviceKey: obj?.deviceKey, cryptoMasterKey: SymmetricCryptoKey.fromJSON(obj?.cryptoMasterKey), cryptoSymmetricKey: EncryptionPair.fromJSON( obj?.cryptoSymmetricKey, @@ -185,7 +183,6 @@ export class AccountSettings { vaultTimeout?: number; vaultTimeoutAction?: string = "lock"; approveLoginRequests?: boolean; - trustDeviceChoiceForDecryption?: boolean; /** @deprecated July 2023, left for migration purposes*/ pinProtected?: EncryptionPair<string, EncString> = new EncryptionPair<string, EncString>(); diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index 0e524b6c4b..57a2085ccf 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -14,7 +14,7 @@ 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 { 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"; @@ -650,39 +650,6 @@ export class StateService< : await this.secureStorageService.save(DDG_SHARED_KEY, value, options); } - async getDeviceKey(options?: StorageOptions): Promise<DeviceKey | null> { - 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<void> { - 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<AdminAuthRequestStorable | null> { options = this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()); @@ -714,31 +681,6 @@ export class StateService< await this.saveAccount(account, options); } - async getShouldTrustDevice(options?: StorageOptions): Promise<boolean | null> { - 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<void> { - 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<string> { return ( await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) @@ -1633,7 +1575,6 @@ export class StateService< protected resetAccount(account: TAccount) { const persistentAccountInformation = { settings: account.settings, - keys: { deviceKey: account.keys.deviceKey }, adminAuthRequest: account.adminAuthRequest, }; return Object.assign(this.createAccount(), persistentAccountInformation); diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index 814bf0280f..466c3a2c11 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -48,6 +48,9 @@ export const TOKEN_DISK_LOCAL = new StateDefinition("tokenDiskLocal", "disk", { web: "disk-local", }); export const TOKEN_MEMORY = new StateDefinition("token", "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 diff --git a/libs/common/src/state-migrations/migrate.ts b/libs/common/src/state-migrations/migrate.ts index 0758d49f59..4e1a0529fc 100644 --- a/libs/common/src/state-migrations/migrate.ts +++ b/libs/common/src/state-migrations/migrate.ts @@ -49,6 +49,7 @@ import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org- 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 { 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"; @@ -56,8 +57,7 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting import { MinVersionMigrator } from "./migrations/min-version"; export const MIN_VERSION = 3; -export const CURRENT_VERSION = 52; - +export const CURRENT_VERSION = 53; export type MinVersion = typeof MIN_VERSION; export function createMigrationBuilder() { @@ -111,7 +111,8 @@ export function createMigrationBuilder() { .with(AccountServerConfigMigrator, 48, 49) .with(KeyConnectorMigrator, 49, 50) .with(RememberedEmailMigrator, 50, 51) - .with(DeleteInstalledVersion, 51, CURRENT_VERSION); + .with(DeleteInstalledVersion, 51, 52) + .with(DeviceTrustCryptoServiceStateProviderMigrator, 52, CURRENT_VERSION); } export async function currentVersion( 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<MigrationHelper>; + 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<void> { + const accounts = await helper.getAccounts<ExpectedAccountType>(); + async function migrateAccount(userId: string, account: ExpectedAccountType): Promise<void> { + 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<void> { + const accounts = await helper.getAccounts<ExpectedAccountType>(); + async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise<void> { + // 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<boolean>( + 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))]); + } +} From 2316222e180d218516bc56e09183880f96273c9a Mon Sep 17 00:00:00 2001 From: Matt Gibson <mgibson@bitwarden.com> Date: Mon, 1 Apr 2024 15:24:04 -0500 Subject: [PATCH 076/351] Update SAST preset to query set (#8569) --- .checkmarx/config.yml | 1 + 1 file changed, 1 insertion(+) 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/**" From bdb1aa0a045a201dd9b6c1b3891be3952019f6c0 Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Mon, 1 Apr 2024 14:11:07 -0700 Subject: [PATCH 077/351] fix dep (#8570) --- libs/angular/src/services/jslib-services.module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 9a8a2bc6a2..114c60c193 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -350,7 +350,7 @@ const safeProviders: SafeProvider[] = [ CryptoServiceAbstraction, ApiServiceAbstraction, StateServiceAbstraction, - TokenService, + TokenServiceAbstraction, ], }), safeProvider({ From 11c40036e28ff9f1483536a2a515b99e9d00d42e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 2 Apr 2024 11:22:57 +0200 Subject: [PATCH 078/351] [deps] Platform: Update Rust crate arboard to v3.3.2 (#8186) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- apps/desktop/desktop_native/Cargo.lock | 69 ++++++-------------------- apps/desktop/desktop_native/Cargo.toml | 2 +- 2 files changed, 17 insertions(+), 54 deletions(-) diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index e99d8b4fc4..446bce87a0 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -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,7 +56,6 @@ dependencies = [ "objc_id", "parking_lot", "thiserror", - "winapi", "wl-clipboard-rs", "x11rb", ] @@ -176,13 +175,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]] @@ -348,7 +345,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" dependencies = [ - "libloading 0.7.4", + "libloading", ] [[package]] @@ -375,13 +372,9 @@ 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" @@ -476,12 +469,12 @@ 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]] @@ -659,16 +652,6 @@ version = "0.2.152" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" -[[package]] -name = "libloading" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" -dependencies = [ - "cfg-if", - "winapi", -] - [[package]] name = "libloading" version = "0.8.3" @@ -830,7 +813,7 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2503fa6af34dc83fb74888df8b22afe933b58d37daf7d80424b1c60c68196b8b" dependencies = [ - "libloading 0.8.3", + "libloading", ] [[package]] @@ -1211,12 +1194,6 @@ version = "1.13.1" 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" - [[package]] name = "syn" version = "1.0.109" @@ -1516,15 +1493,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" @@ -1714,22 +1682,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 cf1082d81c..a1625020e5 100644 --- a/apps/desktop/desktop_native/Cargo.toml +++ b/apps/desktop/desktop_native/Cargo.toml @@ -15,7 +15,7 @@ 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.16.0", features = ["async"] } From b338e14623bb263d196196ba0e055d788a43f0d1 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Tue, 2 Apr 2024 08:18:34 -0500 Subject: [PATCH 079/351] LocalBackedSessionStorage Updates (#8542) --- .../browser/src/background/main.background.ts | 27 +++-- .../storage-service.factory.ts | 24 +++- ...cal-backed-session-storage.service.spec.ts | 104 +++++++++++------- .../local-backed-session-storage.service.ts | 94 +++++++++++----- 4 files changed, 162 insertions(+), 87 deletions(-) diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 25befdcf80..102dad80a7 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -207,6 +207,7 @@ import { BrowserStateService as StateServiceAbstraction } from "../platform/serv import { BrowserCryptoService } from "../platform/services/browser-crypto.service"; import { BrowserEnvironmentService } from "../platform/services/browser-environment.service"; import BrowserLocalStorageService from "../platform/services/browser-local-storage.service"; +import BrowserMemoryStorageService from "../platform/services/browser-memory-storage.service"; import BrowserMessagingPrivateModeBackgroundService from "../platform/services/browser-messaging-private-mode-background.service"; import BrowserMessagingService from "../platform/services/browser-messaging.service"; import { BrowserStateService } from "../platform/services/browser-state.service"; @@ -230,7 +231,7 @@ import RuntimeBackground from "./runtime.background"; export default class MainBackground { messagingService: MessagingServiceAbstraction; - storageService: AbstractStorageService; + storageService: AbstractStorageService & ObservableStorageService; secureStorageService: AbstractStorageService; memoryStorageService: AbstractMemoryStorageService; memoryStorageForStateProviders: AbstractMemoryStorageService & ObservableStorageService; @@ -365,22 +366,28 @@ export default class MainBackground { this.cryptoFunctionService = new WebCryptoFunctionService(self); this.keyGenerationService = new KeyGenerationService(this.cryptoFunctionService); this.storageService = new BrowserLocalStorageService(); + + const mv3MemoryStorageCreator = (partitionName: string) => { + // TODO: Consider using multithreaded encrypt service in popup only context + return new LocalBackedSessionStorageService( + new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, false), + this.keyGenerationService, + new BrowserLocalStorageService(), + new BrowserMemoryStorageService(), + partitionName, + ); + }; + this.secureStorageService = this.storageService; // secure storage is not supported in browsers, so we use local storage and warn users when it is used this.memoryStorageService = BrowserApi.isManifestVersion(3) - ? new LocalBackedSessionStorageService( - new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, false), - this.keyGenerationService, - ) + ? mv3MemoryStorageCreator("stateService") : new MemoryStorageService(); this.memoryStorageForStateProviders = BrowserApi.isManifestVersion(3) - ? new LocalBackedSessionStorageService( - new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, false), - this.keyGenerationService, - ) + ? mv3MemoryStorageCreator("stateProviders") : new BackgroundMemoryStorageService(); const storageServiceProvider = new StorageServiceProvider( - this.storageService as BrowserLocalStorageService, + this.storageService, this.memoryStorageForStateProviders, ); diff --git a/apps/browser/src/platform/background/service-factories/storage-service.factory.ts b/apps/browser/src/platform/background/service-factories/storage-service.factory.ts index 6a854255f5..19d5a9c140 100644 --- a/apps/browser/src/platform/background/service-factories/storage-service.factory.ts +++ b/apps/browser/src/platform/background/service-factories/storage-service.factory.ts @@ -7,6 +7,7 @@ import { MemoryStorageService } from "@bitwarden/common/platform/services/memory import { BrowserApi } from "../../browser/browser-api"; import BrowserLocalStorageService from "../../services/browser-local-storage.service"; +import BrowserMemoryStorageService from "../../services/browser-memory-storage.service"; import { LocalBackedSessionStorageService } from "../../services/local-backed-session-storage.service"; import { BackgroundMemoryStorageService } from "../../storage/background-memory-storage.service"; @@ -17,13 +18,14 @@ import { keyGenerationServiceFactory, } from "./key-generation-service.factory"; -type StorageServiceFactoryOptions = FactoryOptions; - -export type DiskStorageServiceInitOptions = StorageServiceFactoryOptions; -export type SecureStorageServiceInitOptions = StorageServiceFactoryOptions; -export type MemoryStorageServiceInitOptions = StorageServiceFactoryOptions & +export type DiskStorageServiceInitOptions = FactoryOptions; +export type SecureStorageServiceInitOptions = FactoryOptions; +export type SessionStorageServiceInitOptions = FactoryOptions; +export type MemoryStorageServiceInitOptions = FactoryOptions & EncryptServiceInitOptions & - KeyGenerationServiceInitOptions; + KeyGenerationServiceInitOptions & + DiskStorageServiceInitOptions & + SessionStorageServiceInitOptions; export function diskStorageServiceFactory( cache: { diskStorageService?: AbstractStorageService } & CachedServices, @@ -47,6 +49,13 @@ export function secureStorageServiceFactory( return factory(cache, "secureStorageService", opts, () => new BrowserLocalStorageService()); } +export function sessionStorageServiceFactory( + cache: { sessionStorageService?: AbstractStorageService } & CachedServices, + opts: SessionStorageServiceInitOptions, +): Promise<AbstractStorageService> { + return factory(cache, "sessionStorageService", opts, () => new BrowserMemoryStorageService()); +} + export function memoryStorageServiceFactory( cache: { memoryStorageService?: AbstractMemoryStorageService } & CachedServices, opts: MemoryStorageServiceInitOptions, @@ -56,6 +65,9 @@ export function memoryStorageServiceFactory( return new LocalBackedSessionStorageService( await encryptServiceFactory(cache, opts), await keyGenerationServiceFactory(cache, opts), + await diskStorageServiceFactory(cache, opts), + await sessionStorageServiceFactory(cache, opts), + "serviceFactories", ); } return new MemoryStorageService(); diff --git a/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts b/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts index fff9f2c28f..7740a22071 100644 --- a/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts +++ b/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts @@ -2,45 +2,70 @@ import { mock, MockProxy } from "jest-mock-extended"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; +import { + AbstractMemoryStorageService, + AbstractStorageService, + StorageUpdate, +} from "@bitwarden/common/platform/abstractions/storage.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 BrowserLocalStorageService from "./browser-local-storage.service"; -import BrowserMemoryStorageService from "./browser-memory-storage.service"; import { LocalBackedSessionStorageService } from "./local-backed-session-storage.service"; -describe("Browser Session Storage Service", () => { +describe("LocalBackedSessionStorage", () => { let encryptService: MockProxy<EncryptService>; let keyGenerationService: MockProxy<KeyGenerationService>; + let localStorageService: MockProxy<AbstractStorageService>; + let sessionStorageService: MockProxy<AbstractMemoryStorageService>; let cache: Map<string, any>; const testObj = { a: 1, b: 2 }; - let localStorage: BrowserLocalStorageService; - let sessionStorage: BrowserMemoryStorageService; - const key = new SymmetricCryptoKey(Utils.fromUtf8ToArray("00000000000000000000000000000000")); let getSessionKeySpy: jest.SpyInstance; + let sendUpdateSpy: jest.SpyInstance<void, [storageUpdate: StorageUpdate]>; const mockEnc = (input: string) => Promise.resolve(new EncString("ENCRYPTED" + input)); let sut: LocalBackedSessionStorageService; + const mockExistingSessionKey = (key: SymmetricCryptoKey) => { + sessionStorageService.get.mockImplementation((storageKey) => { + if (storageKey === "localEncryptionKey_test") { + return Promise.resolve(key?.toJSON()); + } + + return Promise.reject("No implementation for " + storageKey); + }); + }; + beforeEach(() => { encryptService = mock<EncryptService>(); keyGenerationService = mock<KeyGenerationService>(); + localStorageService = mock<AbstractStorageService>(); + sessionStorageService = mock<AbstractMemoryStorageService>(); - sut = new LocalBackedSessionStorageService(encryptService, keyGenerationService); + sut = new LocalBackedSessionStorageService( + encryptService, + keyGenerationService, + localStorageService, + sessionStorageService, + "test", + ); cache = sut["cache"]; - localStorage = sut["localStorage"]; - sessionStorage = sut["sessionStorage"]; + + keyGenerationService.createKeyWithPurpose.mockResolvedValue({ + derivedKey: key, + salt: "bitwarden-ephemeral", + material: null, // Not used + }); + getSessionKeySpy = jest.spyOn(sut, "getSessionEncKey"); getSessionKeySpy.mockResolvedValue(key); - }); - it("should exist", () => { - expect(sut).toBeInstanceOf(LocalBackedSessionStorageService); + sendUpdateSpy = jest.spyOn(sut, "sendUpdate"); + sendUpdateSpy.mockReturnValue(); }); describe("get", () => { @@ -54,7 +79,7 @@ describe("Browser Session Storage Service", () => { const session = { test: testObj }; beforeEach(() => { - jest.spyOn(sut, "getSessionEncKey").mockResolvedValue(key); + mockExistingSessionKey(key); }); describe("no session retrieved", () => { @@ -62,6 +87,7 @@ describe("Browser Session Storage Service", () => { let spy: jest.SpyInstance; beforeEach(async () => { spy = jest.spyOn(sut, "getLocalSession").mockResolvedValue(null); + localStorageService.get.mockResolvedValue(null); result = await sut.get("test"); }); @@ -123,31 +149,31 @@ describe("Browser Session Storage Service", () => { describe("remove", () => { it("should save null", async () => { - const spy = jest.spyOn(sut, "save"); - spy.mockResolvedValue(null); await sut.remove("test"); - expect(spy).toHaveBeenCalledWith("test", null); + expect(sendUpdateSpy).toHaveBeenCalledWith({ key: "test", updateType: "remove" }); }); }); describe("save", () => { describe("caching", () => { beforeEach(() => { - jest.spyOn(localStorage, "get").mockResolvedValue(null); - jest.spyOn(sessionStorage, "get").mockResolvedValue(null); - jest.spyOn(localStorage, "save").mockResolvedValue(); - jest.spyOn(sessionStorage, "save").mockResolvedValue(); + localStorageService.get.mockResolvedValue(null); + sessionStorageService.get.mockResolvedValue(null); + + localStorageService.save.mockResolvedValue(); + sessionStorageService.save.mockResolvedValue(); encryptService.encrypt.mockResolvedValue(mockEnc("{}")); }); it("should remove key from cache if value is null", async () => { cache.set("test", {}); - const deleteSpy = jest.spyOn(cache, "delete"); + const cacheSetSpy = jest.spyOn(cache, "set"); expect(cache.has("test")).toBe(true); await sut.save("test", null); - expect(cache.has("test")).toBe(false); - expect(deleteSpy).toHaveBeenCalledWith("test"); + // Don't remove from cache, just replace with null + expect(cache.get("test")).toBe(null); + expect(cacheSetSpy).toHaveBeenCalledWith("test", null); }); it("should set cache if value is non-null", async () => { @@ -197,7 +223,7 @@ describe("Browser Session Storage Service", () => { }); it("should return the stored symmetric crypto key", async () => { - jest.spyOn(sessionStorage, "get").mockResolvedValue({ ...key }); + sessionStorageService.get.mockResolvedValue({ ...key }); const result = await sut.getSessionEncKey(); expect(result).toStrictEqual(key); @@ -205,7 +231,6 @@ describe("Browser Session Storage Service", () => { describe("new key creation", () => { beforeEach(() => { - jest.spyOn(sessionStorage, "get").mockResolvedValue(null); keyGenerationService.createKeyWithPurpose.mockResolvedValue({ salt: "salt", material: null, @@ -218,25 +243,24 @@ describe("Browser Session Storage Service", () => { const result = await sut.getSessionEncKey(); expect(result).toStrictEqual(key); - expect(keyGenerationService.createKeyWithPurpose).toBeCalledTimes(1); + expect(keyGenerationService.createKeyWithPurpose).toHaveBeenCalledTimes(1); }); it("should store a symmetric crypto key if it makes one", async () => { const spy = jest.spyOn(sut, "setSessionEncKey").mockResolvedValue(); await sut.getSessionEncKey(); - expect(spy).toBeCalledWith(key); + expect(spy).toHaveBeenCalledWith(key); }); }); }); describe("getLocalSession", () => { it("should return null if session is null", async () => { - const spy = jest.spyOn(localStorage, "get").mockResolvedValue(null); const result = await sut.getLocalSession(key); expect(result).toBeNull(); - expect(spy).toBeCalledWith("session"); + expect(localStorageService.get).toHaveBeenCalledWith("session_test"); }); describe("non-null sessions", () => { @@ -245,7 +269,7 @@ describe("Browser Session Storage Service", () => { const decryptedSession = JSON.stringify(session); beforeEach(() => { - jest.spyOn(localStorage, "get").mockResolvedValue(encSession.encryptedString); + localStorageService.get.mockResolvedValue(encSession.encryptedString); }); it("should decrypt returned sessions", async () => { @@ -267,13 +291,12 @@ describe("Browser Session Storage Service", () => { it("should remove state if decryption fails", async () => { encryptService.decryptToUtf8.mockResolvedValue(null); const setSessionEncKeySpy = jest.spyOn(sut, "setSessionEncKey").mockResolvedValue(); - const removeLocalSessionSpy = jest.spyOn(localStorage, "remove").mockResolvedValue(); const result = await sut.getLocalSession(key); expect(result).toBeNull(); expect(setSessionEncKeySpy).toHaveBeenCalledWith(null); - expect(removeLocalSessionSpy).toHaveBeenCalledWith("session"); + expect(localStorageService.remove).toHaveBeenCalledWith("session_test"); }); }); }); @@ -284,7 +307,7 @@ describe("Browser Session Storage Service", () => { it("should encrypt a stringified session", async () => { encryptService.encrypt.mockImplementation(mockEnc); - jest.spyOn(localStorage, "save").mockResolvedValue(); + localStorageService.save.mockResolvedValue(); await sut.setLocalSession(testSession, key); expect(encryptService.encrypt).toHaveBeenNthCalledWith(1, testJSON, key); @@ -292,32 +315,31 @@ describe("Browser Session Storage Service", () => { it("should remove local session if null", async () => { encryptService.encrypt.mockResolvedValue(null); - const spy = jest.spyOn(localStorage, "remove").mockResolvedValue(); await sut.setLocalSession(null, key); - expect(spy).toHaveBeenCalledWith("session"); + expect(localStorageService.remove).toHaveBeenCalledWith("session_test"); }); it("should save encrypted string", async () => { encryptService.encrypt.mockImplementation(mockEnc); - const spy = jest.spyOn(localStorage, "save").mockResolvedValue(); await sut.setLocalSession(testSession, key); - expect(spy).toHaveBeenCalledWith("session", (await mockEnc(testJSON)).encryptedString); + expect(localStorageService.save).toHaveBeenCalledWith( + "session_test", + (await mockEnc(testJSON)).encryptedString, + ); }); }); describe("setSessionKey", () => { it("should remove if null", async () => { - const spy = jest.spyOn(sessionStorage, "remove").mockResolvedValue(); await sut.setSessionEncKey(null); - expect(spy).toHaveBeenCalledWith("localEncryptionKey"); + expect(sessionStorageService.remove).toHaveBeenCalledWith("localEncryptionKey_test"); }); it("should save key when not null", async () => { - const spy = jest.spyOn(sessionStorage, "save").mockResolvedValue(); await sut.setSessionEncKey(key); - expect(spy).toHaveBeenCalledWith("localEncryptionKey", key); + expect(sessionStorageService.save).toHaveBeenCalledWith("localEncryptionKey_test", key); }); }); }); diff --git a/apps/browser/src/platform/services/local-backed-session-storage.service.ts b/apps/browser/src/platform/services/local-backed-session-storage.service.ts index b2823ffe4b..3f01e4169e 100644 --- a/apps/browser/src/platform/services/local-backed-session-storage.service.ts +++ b/apps/browser/src/platform/services/local-backed-session-storage.service.ts @@ -1,40 +1,60 @@ -import { Subject } from "rxjs"; +import { Observable, Subject, filter, map, merge, share, tap } from "rxjs"; import { Jsonify } from "type-fest"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { AbstractMemoryStorageService, + AbstractStorageService, + ObservableStorageService, StorageUpdate, } from "@bitwarden/common/platform/abstractions/storage.service"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { MemoryStorageOptions } from "@bitwarden/common/platform/models/domain/storage-options"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { fromChromeEvent } from "../browser/from-chrome-event"; import { devFlag } from "../decorators/dev-flag.decorator"; import { devFlagEnabled } from "../flags"; -import BrowserLocalStorageService from "./browser-local-storage.service"; -import BrowserMemoryStorageService from "./browser-memory-storage.service"; - -const keys = { - encKey: "localEncryptionKey", - sessionKey: "session", -}; - -export class LocalBackedSessionStorageService extends AbstractMemoryStorageService { +export class LocalBackedSessionStorageService + extends AbstractMemoryStorageService + implements ObservableStorageService +{ private cache = new Map<string, unknown>(); - private localStorage = new BrowserLocalStorageService(); - private sessionStorage = new BrowserMemoryStorageService(); private updatesSubject = new Subject<StorageUpdate>(); - updates$; + + private commandName = `localBackedSessionStorage_${this.name}`; + private encKey = `localEncryptionKey_${this.name}`; + private sessionKey = `session_${this.name}`; + + updates$: Observable<StorageUpdate>; constructor( private encryptService: EncryptService, private keyGenerationService: KeyGenerationService, + private localStorage: AbstractStorageService, + private sessionStorage: AbstractStorageService, + private name: string, ) { super(); - this.updates$ = this.updatesSubject.asObservable(); + + const remoteObservable = fromChromeEvent(chrome.runtime.onMessage).pipe( + filter(([msg]) => msg.command === this.commandName), + map(([msg]) => msg.update as StorageUpdate), + tap((update) => { + if (update.updateType === "remove") { + this.cache.set(update.key, null); + } else { + this.cache.delete(update.key); + } + }), + share(), + ); + + remoteObservable.subscribe(); + + this.updates$ = merge(this.updatesSubject.asObservable(), remoteObservable); } get valuesRequireDeserialization(): boolean { @@ -70,23 +90,37 @@ export class LocalBackedSessionStorageService extends AbstractMemoryStorageServi async save<T>(key: string, obj: T): Promise<void> { if (obj == null) { - this.cache.delete(key); - } else { - this.cache.set(key, obj); + return await this.remove(key); } + this.cache.set(key, obj); + await this.updateLocalSessionValue(key, obj); + this.sendUpdate({ key, updateType: "save" }); + } + + async remove(key: string): Promise<void> { + this.cache.set(key, null); + await this.updateLocalSessionValue(key, null); + this.sendUpdate({ key, updateType: "remove" }); + } + + sendUpdate(storageUpdate: StorageUpdate) { + this.updatesSubject.next(storageUpdate); + void chrome.runtime.sendMessage({ + command: this.commandName, + update: storageUpdate, + }); + } + + private async updateLocalSessionValue<T>(key: string, obj: T) { const sessionEncKey = await this.getSessionEncKey(); const localSession = (await this.getLocalSession(sessionEncKey)) ?? {}; localSession[key] = obj; await this.setLocalSession(localSession, sessionEncKey); } - async remove(key: string): Promise<void> { - await this.save(key, null); - } - async getLocalSession(encKey: SymmetricCryptoKey): Promise<Record<string, unknown>> { - const local = await this.localStorage.get<string>(keys.sessionKey); + const local = await this.localStorage.get<string>(this.sessionKey); if (local == null) { return null; @@ -100,7 +134,7 @@ export class LocalBackedSessionStorageService extends AbstractMemoryStorageServi if (sessionJson == null) { // Error with decryption -- session is lost, delete state and key and start over await this.setSessionEncKey(null); - await this.localStorage.remove(keys.sessionKey); + await this.localStorage.remove(this.sessionKey); return null; } return JSON.parse(sessionJson); @@ -119,9 +153,9 @@ export class LocalBackedSessionStorageService extends AbstractMemoryStorageServi // Make sure we're storing the jsonified version of the session const jsonSession = JSON.parse(JSON.stringify(session)); if (session == null) { - await this.localStorage.remove(keys.sessionKey); + await this.localStorage.remove(this.sessionKey); } else { - await this.localStorage.save(keys.sessionKey, jsonSession); + await this.localStorage.save(this.sessionKey, jsonSession); } } @@ -130,13 +164,13 @@ export class LocalBackedSessionStorageService extends AbstractMemoryStorageServi const encSession = await this.encryptService.encrypt(jsonSession, key); if (encSession == null) { - return await this.localStorage.remove(keys.sessionKey); + return await this.localStorage.remove(this.sessionKey); } - await this.localStorage.save(keys.sessionKey, encSession.encryptedString); + await this.localStorage.save(this.sessionKey, encSession.encryptedString); } async getSessionEncKey(): Promise<SymmetricCryptoKey> { - let storedKey = await this.sessionStorage.get<SymmetricCryptoKey>(keys.encKey); + let storedKey = await this.sessionStorage.get<SymmetricCryptoKey>(this.encKey); if (storedKey == null || Object.keys(storedKey).length == 0) { const generatedKey = await this.keyGenerationService.createKeyWithPurpose( 128, @@ -153,9 +187,9 @@ export class LocalBackedSessionStorageService extends AbstractMemoryStorageServi async setSessionEncKey(input: SymmetricCryptoKey): Promise<void> { if (input == null) { - await this.sessionStorage.remove(keys.encKey); + await this.sessionStorage.remove(this.encKey); } else { - await this.sessionStorage.save(keys.encKey, input); + await this.sessionStorage.save(this.encKey, input); } } } From 22cca018f83a806b77a48efc59ac578c706ee5f6 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Tue, 2 Apr 2024 09:28:36 -0400 Subject: [PATCH 080/351] Don't let users who can't edit sub reactivate sub pending cancelation (#8433) --- .../organization-subscription-cloud.component.html | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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..5f767d85c4 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 @@ -25,7 +25,13 @@ *ngIf="subscriptionMarkedForCancel" > <p>{{ "subscriptionPendingCanceled" | i18n }}</p> - <button bitButton buttonType="secondary" [bitAction]="reinstate" type="button"> + <button + *ngIf="userOrg.canEditSubscription" + bitButton + buttonType="secondary" + [bitAction]="reinstate" + type="button" + > {{ "reinstateSubscription" | i18n }} </button> </bit-callout> From a201e9cff13b58210bff50d3353462ff040fee41 Mon Sep 17 00:00:00 2001 From: Will Martin <contact@willmartian.com> Date: Tue, 2 Apr 2024 10:21:01 -0400 Subject: [PATCH 081/351] [PM-7229] Fix circular dependency in ErrorHandler (#8573) * call injector explicitly in ErrorHandler * Fallback to `super` on early error Co-authored-by: Matt Gibson <mgibson@bitwarden.com> --------- Co-authored-by: Matt Gibson <mgibson@bitwarden.com>, --- .../platform/services/logging-error-handler.ts | 18 +++++++++++++----- .../src/services/jslib-services.module.ts | 2 +- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/libs/angular/src/platform/services/logging-error-handler.ts b/libs/angular/src/platform/services/logging-error-handler.ts index 81cd537e7f..522412dd28 100644 --- a/libs/angular/src/platform/services/logging-error-handler.ts +++ b/libs/angular/src/platform/services/logging-error-handler.ts @@ -1,14 +1,22 @@ -import { ErrorHandler, Injectable } from "@angular/core"; +import { ErrorHandler, Injectable, Injector, inject } from "@angular/core"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @Injectable() export class LoggingErrorHandler extends ErrorHandler { - constructor(private readonly logService: LogService) { - super(); - } + /** + * 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 { - this.logService.error(error); + try { + const logService = this.injector.get(LogService, null); + logService.error(error); + } catch { + super.handleError(error); + } } } diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 114c60c193..6378eed755 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -1075,7 +1075,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: ErrorHandler, useClass: LoggingErrorHandler, - deps: [LogService], + deps: [], }), ]; From af5f45443d870cf6012c3bdcdb6180786c951b30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Gon=C3=A7alves?= <cgoncalves@bitwarden.com> Date: Tue, 2 Apr 2024 16:23:05 +0100 Subject: [PATCH 082/351] [PM-5434] Create VaultBrowserStateService and migrate components from BrowserStateService (#8017) * PM-5434 Initial work on migration * PM-5434 Migration and tests * PM-5434 Remove unnecessary comments * PM-5434 Add unit tests * PM-5434 Reverted last changes * PM-5434 Added unit test for deserialize * PM-5434 Minor changes * PM-5434 Fix pr comments --- .../abstractions/browser-state.service.ts | 13 --- .../services/browser-state.service.spec.ts | 22 ----- .../services/browser-state.service.ts | 45 ---------- apps/browser/src/popup/app.component.ts | 6 +- .../src/popup/services/services.module.ts | 8 ++ .../vault/vault-filter.component.ts | 12 +-- .../components/vault/vault-items.component.ts | 4 +- .../vault-browser-state.service.spec.ts | 87 +++++++++++++++++++ .../services/vault-browser-state.service.ts | 65 ++++++++++++++ .../src/platform/state/state-definitions.ts | 1 + 10 files changed, 173 insertions(+), 90 deletions(-) create mode 100644 apps/browser/src/vault/services/vault-browser-state.service.spec.ts create mode 100644 apps/browser/src/vault/services/vault-browser-state.service.ts diff --git a/apps/browser/src/platform/services/abstractions/browser-state.service.ts b/apps/browser/src/platform/services/abstractions/browser-state.service.ts index 88c2312762..82ec54975a 100644 --- a/apps/browser/src/platform/services/abstractions/browser-state.service.ts +++ b/apps/browser/src/platform/services/abstractions/browser-state.service.ts @@ -3,22 +3,9 @@ import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage import { Account } from "../../../models/account"; import { BrowserComponentState } from "../../../models/browserComponentState"; -import { BrowserGroupingsComponentState } from "../../../models/browserGroupingsComponentState"; import { BrowserSendComponentState } from "../../../models/browserSendComponentState"; export abstract class BrowserStateService extends BaseStateServiceAbstraction<Account> { - getBrowserGroupingComponentState: ( - options?: StorageOptions, - ) => Promise<BrowserGroupingsComponentState>; - setBrowserGroupingComponentState: ( - value: BrowserGroupingsComponentState, - options?: StorageOptions, - ) => Promise<void>; - getBrowserVaultItemsComponentState: (options?: StorageOptions) => Promise<BrowserComponentState>; - setBrowserVaultItemsComponentState: ( - value: BrowserComponentState, - options?: StorageOptions, - ) => Promise<void>; getBrowserSendComponentState: (options?: StorageOptions) => Promise<BrowserSendComponentState>; setBrowserSendComponentState: ( value: BrowserSendComponentState, diff --git a/apps/browser/src/platform/services/browser-state.service.spec.ts b/apps/browser/src/platform/services/browser-state.service.spec.ts index 3069b8f174..7e75b9b707 100644 --- a/apps/browser/src/platform/services/browser-state.service.spec.ts +++ b/apps/browser/src/platform/services/browser-state.service.spec.ts @@ -18,7 +18,6 @@ import { UserId } from "@bitwarden/common/types/guid"; import { Account } from "../../models/account"; import { BrowserComponentState } from "../../models/browserComponentState"; -import { BrowserGroupingsComponentState } from "../../models/browserGroupingsComponentState"; import { BrowserSendComponentState } from "../../models/browserSendComponentState"; import { BrowserStateService } from "./browser-state.service"; @@ -86,27 +85,6 @@ describe("Browser State Service", () => { ); }); - describe("getBrowserGroupingComponentState", () => { - it("should return a BrowserGroupingsComponentState", async () => { - state.accounts[userId].groupings = new BrowserGroupingsComponentState(); - - const actual = await sut.getBrowserGroupingComponentState(); - expect(actual).toBeInstanceOf(BrowserGroupingsComponentState); - }); - }); - - describe("getBrowserVaultItemsComponentState", () => { - it("should return a BrowserComponentState", async () => { - const componentState = new BrowserComponentState(); - componentState.scrollY = 0; - componentState.searchText = "test"; - state.accounts[userId].ciphers = componentState; - - const actual = await sut.getBrowserVaultItemsComponentState(); - expect(actual).toStrictEqual(componentState); - }); - }); - describe("getBrowserSendComponentState", () => { it("should return a BrowserSendComponentState", async () => { const sendState = new BrowserSendComponentState(); diff --git a/apps/browser/src/platform/services/browser-state.service.ts b/apps/browser/src/platform/services/browser-state.service.ts index f7ee74be21..ea410ee83a 100644 --- a/apps/browser/src/platform/services/browser-state.service.ts +++ b/apps/browser/src/platform/services/browser-state.service.ts @@ -16,7 +16,6 @@ import { StateService as BaseStateService } from "@bitwarden/common/platform/ser import { Account } from "../../models/account"; import { BrowserComponentState } from "../../models/browserComponentState"; -import { BrowserGroupingsComponentState } from "../../models/browserGroupingsComponentState"; import { BrowserSendComponentState } from "../../models/browserSendComponentState"; import { BrowserApi } from "../browser/browser-api"; import { browserSession, sessionSync } from "../decorators/session-sync-observable"; @@ -116,50 +115,6 @@ export class BrowserStateService ); } - async getBrowserGroupingComponentState( - options?: StorageOptions, - ): Promise<BrowserGroupingsComponentState> { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) - )?.groupings; - } - - async setBrowserGroupingComponentState( - value: BrowserGroupingsComponentState, - options?: StorageOptions, - ): Promise<void> { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - account.groupings = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - } - - async getBrowserVaultItemsComponentState( - options?: StorageOptions, - ): Promise<BrowserComponentState> { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) - )?.ciphers; - } - - async setBrowserVaultItemsComponentState( - value: BrowserComponentState, - options?: StorageOptions, - ): Promise<void> { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - account.ciphers = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - } - async getBrowserSendComponentState(options?: StorageOptions): Promise<BrowserSendComponentState> { return ( await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) diff --git a/apps/browser/src/popup/app.component.ts b/apps/browser/src/popup/app.component.ts index 9aa438d3b3..e0d898481b 100644 --- a/apps/browser/src/popup/app.component.ts +++ b/apps/browser/src/popup/app.component.ts @@ -12,6 +12,7 @@ import { BrowserApi } from "../platform/browser/browser-api"; import { ZonedMessageListenerService } from "../platform/browser/zoned-message-listener.service"; import { BrowserStateService } from "../platform/services/abstractions/browser-state.service"; import { ForegroundPlatformUtilsService } from "../platform/services/platform-utils/foreground-platform-utils.service"; +import { VaultBrowserStateService } from "../vault/services/vault-browser-state.service"; import { routerTransition } from "./app-routing.animations"; import { DesktopSyncVerificationDialogComponent } from "./components/desktop-sync-verification-dialog.component"; @@ -37,6 +38,7 @@ export class AppComponent implements OnInit, OnDestroy { private i18nService: I18nService, private router: Router, private stateService: BrowserStateService, + private vaultBrowserStateService: VaultBrowserStateService, private changeDetectorRef: ChangeDetectorRef, private ngZone: NgZone, private platformUtilsService: ForegroundPlatformUtilsService, @@ -227,8 +229,8 @@ export class AppComponent implements OnInit, OnDestroy { } await Promise.all([ - this.stateService.setBrowserGroupingComponentState(null), - this.stateService.setBrowserVaultItemsComponentState(null), + this.vaultBrowserStateService.setBrowserGroupingsComponentState(null), + this.vaultBrowserStateService.setBrowserVaultItemsComponentState(null), this.stateService.setBrowserSendComponentState(null), this.stateService.setBrowserSendTypeComponentState(null), ]); diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index fbeabca462..6d0f73f206 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -102,6 +102,7 @@ import { ForegroundPlatformUtilsService } from "../../platform/services/platform import { ForegroundDerivedStateProvider } from "../../platform/state/foreground-derived-state.provider"; import { ForegroundMemoryStorageService } from "../../platform/storage/foreground-memory-storage.service"; import { FilePopoutUtilsService } from "../../tools/popup/services/file-popout-utils.service"; +import { VaultBrowserStateService } from "../../vault/services/vault-browser-state.service"; import { VaultFilterService } from "../../vault/services/vault-filter.service"; import { DebounceNavigationService } from "./debounce-navigation.service"; @@ -377,6 +378,13 @@ const safeProviders: SafeProvider[] = [ provide: OBSERVABLE_DISK_STORAGE, useExisting: AbstractStorageService, }), + safeProvider({ + provide: VaultBrowserStateService, + useFactory: (stateProvider: StateProvider) => { + return new VaultBrowserStateService(stateProvider); + }, + deps: [StateProvider], + }), safeProvider({ provide: StateServiceAbstraction, useFactory: ( 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..2510e2f966 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 @@ -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"; @@ -84,8 +84,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 +95,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 +120,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) { @@ -413,11 +413,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<boolean> { - 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/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<string, number>(), + folderCounts: new Map<string, number>(), + typeCounts: new Map<CipherType, number>(), + }; + + const result = sut.deserializer( + JSON.parse(JSON.stringify(expectedState)) as Jsonify<BrowserGroupingsComponentState>, + ); + + 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<BrowserGroupingsComponentState>( + VAULT_BROWSER_MEMORY, + "vault_browser_groupings_component", + { + deserializer: (obj: Jsonify<BrowserGroupingsComponentState>) => + BrowserGroupingsComponentState.fromJSON(obj), + }, +); + +export const VAULT_BROWSER_COMPONENT = new KeyDefinition<BrowserComponentState>( + VAULT_BROWSER_MEMORY, + "vault_browser_component", + { + deserializer: (obj: Jsonify<BrowserComponentState>) => BrowserComponentState.fromJSON(obj), + }, +); + +export class VaultBrowserStateService { + vaultBrowserGroupingsComponentState$: Observable<BrowserGroupingsComponentState>; + vaultBrowserComponentState$: Observable<BrowserComponentState>; + + private activeUserVaultBrowserGroupingsComponentState: ActiveUserState<BrowserGroupingsComponentState>; + private activeUserVaultBrowserComponentState: ActiveUserState<BrowserComponentState>; + + 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<BrowserGroupingsComponentState> { + return await firstValueFrom(this.vaultBrowserGroupingsComponentState$); + } + + async setBrowserGroupingsComponentState(value: BrowserGroupingsComponentState): Promise<void> { + await this.activeUserVaultBrowserGroupingsComponentState.update(() => value); + } + + async getBrowserVaultItemsComponentState(): Promise<BrowserComponentState> { + return await firstValueFrom(this.vaultBrowserComponentState$); + } + + async setBrowserVaultItemsComponentState(value: BrowserComponentState): Promise<void> { + await this.activeUserVaultBrowserComponentState.update(() => value); + } +} diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index 466c3a2c11..9fca0e9445 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -118,3 +118,4 @@ 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"); From 2e6d977ef1aec00423b709de01c9c75cbec14655 Mon Sep 17 00:00:00 2001 From: Jake Fink <jfink@bitwarden.com> Date: Tue, 2 Apr 2024 11:23:35 -0400 Subject: [PATCH 083/351] init observable on service (#8577) --- .../auth-request/auth-request.service.spec.ts | 17 +++++++++++++++++ .../auth-request/auth-request.service.ts | 4 +++- 2 files changed, 20 insertions(+), 1 deletion(-) 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..80d00b2a01 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,6 +2,7 @@ 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 { 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"; @@ -30,6 +31,22 @@ describe("AuthRequestService", () => { 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("approveOrDenyAuthRequest", () => { beforeEach(() => { cryptoService.rsaEncrypt.mockResolvedValue({ 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 ff33eadfba..eb39659f53 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 @@ -22,7 +22,9 @@ export class AuthRequestService implements AuthRequestServiceAbstraction { private cryptoService: CryptoService, private apiService: ApiService, private stateService: StateService, - ) {} + ) { + this.authRequestPushNotification$ = this.authRequestPushNotificationSubject.asObservable(); + } async approveOrDenyAuthRequest( approve: boolean, From b9771c1e426746a202a4633089ce638fba10886a Mon Sep 17 00:00:00 2001 From: Cesar Gonzalez <cesar.a.gonzalezcs@gmail.com> Date: Tue, 2 Apr 2024 10:24:16 -0500 Subject: [PATCH 084/351] [PM-5584] Set up a stay alive method to allow service worker in manifest v3 to stay alive indefinitely (#8535) --- apps/browser/src/platform/background.ts | 65 +++++++++++-------------- 1 file changed, 29 insertions(+), 36 deletions(-) diff --git a/apps/browser/src/platform/background.ts b/apps/browser/src/platform/background.ts index 5aa2820e5f..9c3510178c 100644 --- a/apps/browser/src/platform/background.ts +++ b/apps/browser/src/platform/background.ts @@ -1,42 +1,35 @@ +import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; + import MainBackground from "../background/main.background"; -import { onAlarmListener } from "./alarms/on-alarm-listener"; -import { registerAlarms } from "./alarms/register-alarms"; import { BrowserApi } from "./browser/browser-api"; -import { - contextMenusClickedListener, - onCommandListener, - onInstallListener, - runtimeMessageListener, - windowsOnFocusChangedListener, - tabsOnActivatedListener, - tabsOnReplacedListener, - tabsOnUpdatedListener, -} from "./listeners"; -if (BrowserApi.isManifestVersion(3)) { - chrome.commands.onCommand.addListener(onCommandListener); - chrome.runtime.onInstalled.addListener(onInstallListener); - chrome.alarms.onAlarm.addListener(onAlarmListener); - registerAlarms(); - chrome.windows.onFocusChanged.addListener(windowsOnFocusChangedListener); - chrome.tabs.onActivated.addListener(tabsOnActivatedListener); - chrome.tabs.onReplaced.addListener(tabsOnReplacedListener); - chrome.tabs.onUpdated.addListener(tabsOnUpdatedListener); - chrome.contextMenus.onClicked.addListener(contextMenusClickedListener); - BrowserApi.messageListener( - "runtime.background", - (message: { command: string }, sender, sendResponse) => { - // 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 - runtimeMessageListener(message, sender); - }, - ); -} else { - const bitwardenMain = ((self as any).bitwardenMain = new MainBackground()); - // 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 - bitwardenMain.bootstrap().then(() => { +const logService = new ConsoleLogService(false); +const bitwardenMain = ((self as any).bitwardenMain = new MainBackground()); +bitwardenMain + .bootstrap() + .then(() => { // Finished bootstrapping - }); + if (BrowserApi.isManifestVersion(3)) { + startHeartbeat().catch((error) => logService.error(error)); + } + }) + .catch((error) => logService.error(error)); + +/** + * Tracks when a service worker was last alive and extends the service worker + * lifetime by writing the current time to extension storage every 20 seconds. + */ +async function runHeartbeat() { + await chrome.storage.local.set({ "last-heartbeat": new Date().getTime() }); +} + +/** + * Starts the heartbeat interval which keeps the service worker alive. + */ +async function startHeartbeat() { + // Run the heartbeat once at service worker startup, then again every 20 seconds. + runHeartbeat() + .then(() => setInterval(runHeartbeat, 20 * 1000)) + .catch((error) => logService.error(error)); } From 9956f020e75697c1af5df823b96a479702fa5439 Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Tue, 2 Apr 2024 17:04:02 +0100 Subject: [PATCH 085/351] [AC-1911] Clients: Create components to manage client organization seat allocation (#8505) * implementing the clients changes * resolve pr comments on message.json * moved the method to billing-api.service * move the request and response files to billing folder * remove the adding existing orgs * resolve the routing issue * resolving the pr comments * code owner changes * fix the assignedseat * resolve the warning message * resolve the error on update * passing the right id * resolve the unassign value * removed unused logservice * Adding the loader on submit button --- .github/CODEOWNERS | 1 + apps/web/src/locales/en/messages.json | 36 ++++ .../providers/clients/clients.component.ts | 33 +++- .../providers/providers-layout.component.html | 6 +- .../providers/providers-layout.component.ts | 5 + .../providers/providers-routing.module.ts | 7 + .../providers/providers.module.ts | 5 + ...t-organization-subscription.component.html | 49 ++++++ ...ent-organization-subscription.component.ts | 115 +++++++++++++ ...manage-client-organizations.component.html | 90 ++++++++++ .../manage-client-organizations.component.ts | 160 ++++++++++++++++++ .../billilng-api.service.abstraction.ts | 8 + .../provider-subscription-update.request.ts | 3 + .../provider-subscription-response.ts | 38 +++++ .../billing/services/billing-api.service.ts | 27 +++ libs/common/src/enums/feature-flag.enum.ts | 1 + 16 files changed, 575 insertions(+), 9 deletions(-) create mode 100644 bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-organization-subscription.component.html create mode 100644 bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-organization-subscription.component.ts create mode 100644 bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-organizations.component.html create mode 100644 bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-organizations.component.ts create mode 100644 libs/common/src/billing/models/request/provider-subscription-update.request.ts create mode 100644 libs/common/src/billing/models/response/provider-subscription-response.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index bfad3f2628..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 diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 95d1b03e72..b8e5a5ff4d 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Add existing organization" }, + "addNewOrganization": { + "message": "Add new organization" + }, "myProvider": { "message": "My Provider" }, @@ -7642,5 +7645,38 @@ }, "items": { "message": "Items" + }, + "assignedSeats": { + "message": "Assigned seats" + }, + "assigned": { + "message": "Assigned" + }, + "used": { + "message": "Used" + }, + "remaining": { + "message": "Remaining" + }, + "unlinkOrganization": { + "message": "Unlink organization" + }, + "manageSeats": { + "message": "MANAGE SEATS" + }, + "manageSeatsDescription": { + "message": "Adjustments to seats will be reflected in the next billing cycle." + }, + "unassignedSeatsDescription": { + "message": "Unassigned subscription seats" + }, + "purchaseSeatDescription": { + "message": "Additional seats purchased" + }, + "assignedSeatCannotUpdate": { + "message": "Assigned Seats can not be updated. Please contact your organization owner for assistance." + }, + "subscriptionUpdateFailed": { + "message": "Subscription update failed" } } diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts index dc3dea3c9d..20e98ce084 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit } from "@angular/core"; -import { ActivatedRoute } from "@angular/router"; +import { ActivatedRoute, Router } from "@angular/router"; import { firstValueFrom } from "rxjs"; import { first } from "rxjs/operators"; @@ -13,6 +13,8 @@ import { ProviderUserType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response"; import { PlanType } from "@bitwarden/common/billing/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"; @@ -50,8 +52,14 @@ export class ClientsComponent implements OnInit { protected actionPromise: Promise<unknown>; private pagedClientsCount = 0; + protected enableConsolidatedBilling$ = this.configService.getFeatureFlag$( + FeatureFlag.EnableConsolidatedBilling, + false, + ); + constructor( private route: ActivatedRoute, + private router: Router, private providerService: ProviderService, private apiService: ApiService, private searchService: SearchService, @@ -64,20 +72,29 @@ export class ClientsComponent implements OnInit { private organizationService: OrganizationService, private organizationApiService: OrganizationApiServiceAbstraction, private dialogService: DialogService, + private configService: ConfigService, ) {} 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(); + const enableConsolidatedBilling = await firstValueFrom(this.enableConsolidatedBilling$); - /* 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; + if (enableConsolidatedBilling) { + await this.router.navigate(["../manage-client-organizations"], { relativeTo: this.route }); + } else { + // 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; + }); }); - }); + } } async load() { diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html index 333ea66e26..fe7f051652 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html @@ -4,7 +4,11 @@ <bit-icon [icon]="logo"></bit-icon> </a> - <bit-nav-item icon="bwi-bank" [text]="'clients' | i18n" route="clients"></bit-nav-item> + <bit-nav-item + icon="bwi-bank" + [text]="'clients' | i18n" + [route]="(enableConsolidatedBilling$ | async) ? 'manage-client-organizations' : 'clients'" + ></bit-nav-item> <bit-nav-group icon="bwi-sliders" [text]="'manage' | i18n" route="manage" *ngIf="showManageTab"> <bit-nav-item [text]="'people' | i18n" diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.ts index b8afe1c235..a28cf1ef37 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.ts @@ -37,6 +37,11 @@ export class ProvidersLayoutComponent { false, ); + protected enableConsolidatedBilling$ = this.configService.getFeatureFlag$( + FeatureFlag.EnableConsolidatedBilling, + false, + ); + constructor( private route: ActivatedRoute, private providerService: ProviderService, diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts index 8e3886f6e2..a42b10d88f 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts @@ -7,6 +7,8 @@ import { ProvidersComponent } from "@bitwarden/web-vault/app/admin-console/provi import { FrontendLayoutComponent } from "@bitwarden/web-vault/app/layouts/frontend-layout.component"; import { UserLayoutComponent } from "@bitwarden/web-vault/app/layouts/user-layout.component"; +import { ManageClientOrganizationsComponent } from "../../billing/providers/clients/manage-client-organizations.component"; + import { ClientsComponent } from "./clients/clients.component"; import { CreateOrganizationComponent } from "./clients/create-organization.component"; import { ProviderPermissionsGuard } from "./guards/provider-permissions.guard"; @@ -64,6 +66,11 @@ const routes: Routes = [ { path: "", pathMatch: "full", redirectTo: "clients" }, { path: "clients/create", component: CreateOrganizationComponent }, { path: "clients", component: ClientsComponent, data: { titleId: "clients" } }, + { + path: "manage-client-organizations", + component: ManageClientOrganizationsComponent, + data: { titleId: "manage-client-organizations" }, + }, { path: "manage", children: [ diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts index 512053d032..70ae19f770 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts @@ -8,6 +8,9 @@ import { OrganizationPlansComponent } from "@bitwarden/web-vault/app/billing"; import { PaymentMethodWarningsModule } from "@bitwarden/web-vault/app/billing/shared"; import { OssModule } from "@bitwarden/web-vault/app/oss.module"; +import { ManageClientOrganizationSubscriptionComponent } from "../../billing/providers/clients/manage-client-organization-subscription.component"; +import { ManageClientOrganizationsComponent } from "../../billing/providers/clients/manage-client-organizations.component"; + import { AddOrganizationComponent } from "./clients/add-organization.component"; import { ClientsComponent } from "./clients/clients.component"; import { CreateOrganizationComponent } from "./clients/create-organization.component"; @@ -50,6 +53,8 @@ import { SetupComponent } from "./setup/setup.component"; SetupComponent, SetupProviderComponent, UserAddEditComponent, + ManageClientOrganizationsComponent, + ManageClientOrganizationSubscriptionComponent, ], providers: [WebProviderService, ProviderPermissionsGuard], }) 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 @@ +<bit-dialog dialogSize="large" [loading]="loading"> + <span bitDialogTitle> + {{ "manageSeats" | i18n }} + <small class="tw-text-muted" *ngIf="clientName">{{ clientName }}</small> + </span> + <div bitDialogContent> + <p> + {{ "manageSeatsDescription" | i18n }} + </p> + <bit-form-field> + <bit-label> + {{ "assignedSeats" | i18n }} + </bit-label> + <input + id="assignedSeats" + type="number" + appAutoFocus + bitInput + required + [(ngModel)]="assignedSeats" + /> + </bit-form-field> + <ng-container *ngIf="remainingOpenSeats > 0"> + <p> + <small class="tw-text-muted">{{ unassignedSeats }}</small> + <small class="tw-text-muted">{{ "unassignedSeatsDescription" | i18n }}</small> + </p> + <p> + <small class="tw-text-muted">{{ AdditionalSeatPurchased }}</small> + <small class="tw-text-muted">{{ "purchaseSeatDescription" | i18n }}</small> + </p> + </ng-container> + </div> + <ng-container bitDialogFooter> + <button + type="submit" + bitButton + buttonType="primary" + bitFormButton + (click)="updateSubscription(assignedSeats)" + > + <i class="bwi bwi-refresh bwi-fw" aria-hidden="true"></i> + {{ "save" | i18n }} + </button> + <button bitButton type="button" buttonType="secondary" bitDialogClose> + {{ "cancel" | i18n }} + </button> + </ng-container> +</bit-dialog> 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 @@ +<app-header> + <bit-search [placeholder]="'search' | i18n" [(ngModel)]="searchText"></bit-search> + <a bitButton routerLink="create" *ngIf="manageOrganizations" buttonType="primary"> + <i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i> + {{ "addNewOrganization" | i18n }} + </a> +</app-header> + +<ng-container *ngIf="loading"> + <i + class="bwi bwi-spinner bwi-spin text-muted" + title="{{ 'loading' | i18n }}" + aria-hidden="true" + ></i> + <span class="sr-only">{{ "loading" | i18n }}</span> +</ng-container> + +<ng-container + *ngIf="!loading && (clients | search: searchText : 'organizationName' : 'id') as searchedClients" +> + <p *ngIf="!searchedClients.length">{{ "noClientsInList" | i18n }}</p> + <ng-container *ngIf="searchedClients.length"> + <bit-table + *ngIf="searchedClients?.length >= 1" + [dataSource]="dataSource" + class="table table-hover table-list" + infiniteScroll + [infiniteScrollDistance]="1" + [infiniteScrollDisabled]="!isPaging()" + (scrolled)="loadMore()" + > + <ng-container header> + <tr> + <th colspan="2" bitCell bitSortable="organizationName" default>{{ "client" | i18n }}</th> + <th bitCell bitSortable="seats">{{ "assigned" | i18n }}</th> + <th bitCell bitSortable="userCount">{{ "used" | i18n }}</th> + <th bitCell bitSortable="userCount">{{ "remaining" | i18n }}</th> + <th bitCell bitSortable="plan">{{ "billingPlan" | i18n }}</th> + <th></th> + </tr> + </ng-container> + <ng-template body let-rows$> + <tr bitRow *ngFor="let client of rows$ | async"> + <td bitCell width="30"> + <bit-avatar [text]="client.organizationName" [id]="client.id" size="small"></bit-avatar> + </td> + <td bitCell> + <div class="tw-flex tw-items-center tw-gap-4 tw-break-all"> + <a bitLink [routerLink]="['/organizations', client.organizationId]">{{ + client.organizationName + }}</a> + </div> + </td> + <td bitCell class="tw-whitespace-nowrap"> + <span>{{ client.seats }}</span> + </td> + <td bitCell class="tw-whitespace-nowrap"> + <span>{{ client.userCount }}</span> + </td> + <td bitCell class="tw-whitespace-nowrap"> + <span>{{ client.seats - client.userCount }}</span> + </td> + <td> + <span>{{ client.plan }}</span> + </td> + <td bitCell> + <button + [bitMenuTriggerFor]="rowMenu" + type="button" + bitIconButton="bwi-ellipsis-v" + size="small" + appA11yTitle="{{ 'options' | i18n }}" + ></button> + <bit-menu #rowMenu> + <button type="button" bitMenuItem (click)="manageSubscription(client)"> + <i aria-hidden="true" class="bwi bwi-question-circle"></i> + {{ "manageSubscription" | i18n }} + </button> + <button type="button" bitMenuItem (click)="remove(client)"> + <span class="tw-text-danger"> + <i aria-hidden="true" class="bwi bwi-close"></i> {{ "unlinkOrganization" | i18n }} + </span> + </button> + </bit-menu> + </td> + </tr> + </ng-template> + </bit-table> + </ng-container> +</ng-container> 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..79dd25e891 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-organizations.component.ts @@ -0,0 +1,160 @@ +import { SelectionModel } from "@angular/cdk/collections"; +import { Component, OnInit } from "@angular/core"; +import { ActivatedRoute } from "@angular/router"; +import { firstValueFrom } from "rxjs"; +import { first } 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 { + providerId: string; + loading = true; + manageOrganizations = false; + + set searchText(search: string) { + this.selection.clear(); + this.dataSource.filter = search; + } + + clients: ProviderOrganizationOrganizationDetailsResponse[]; + pagedClients: ProviderOrganizationOrganizationDetailsResponse[]; + + protected didScroll = false; + protected pageSize = 100; + protected actionPromise: Promise<unknown>; + private pagedClientsCount = 0; + selection = new SelectionModel<string>(true, []); + protected dataSource = new TableDataSource<ProviderOrganizationOrganizationDetailsResponse>(); + + 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; + }); + }); + } + + 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; + } + + isSearching() { + return this.searchService.isSearchable(this.searchText); + } + + 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/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<void>; cancelPremiumUserSubscription: (request: SubscriptionCancellationRequest) => Promise<void>; getBillingStatus: (id: string) => Promise<OrganizationBillingStatusResponse>; + getProviderClientSubscriptions: (providerId: string) => Promise<ProviderSubscriptionResponse>; + putProviderClientSubscriptions: ( + providerId: string, + organizationId: string, + request: ProviderSubscriptionUpdateRequest, + ) => Promise<any>; } diff --git a/libs/common/src/billing/models/request/provider-subscription-update.request.ts b/libs/common/src/billing/models/request/provider-subscription-update.request.ts new file mode 100644 index 0000000000..f2bf4c7e97 --- /dev/null +++ b/libs/common/src/billing/models/request/provider-subscription-update.request.ts @@ -0,0 +1,3 @@ +export class ProviderSubscriptionUpdateRequest { + assignedSeats: number; +} diff --git a/libs/common/src/billing/models/response/provider-subscription-response.ts b/libs/common/src/billing/models/response/provider-subscription-response.ts new file mode 100644 index 0000000000..522c518725 --- /dev/null +++ b/libs/common/src/billing/models/response/provider-subscription-response.ts @@ -0,0 +1,38 @@ +import { BaseResponse } from "../../../models/response/base.response"; + +export class ProviderSubscriptionResponse extends BaseResponse { + status: string; + currentPeriodEndDate: Date; + discountPercentage?: number | null; + plans: Plans[] = []; + + constructor(response: any) { + super(response); + this.status = this.getResponseProperty("status"); + this.currentPeriodEndDate = new Date(this.getResponseProperty("currentPeriodEndDate")); + this.discountPercentage = this.getResponseProperty("discountPercentage"); + const plans = this.getResponseProperty("plans"); + if (plans != null) { + this.plans = plans.map((i: any) => 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/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<ProviderSubscriptionResponse> { + 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<any> { + return await this.apiService.send( + "PUT", + "/providers/" + providerId + "/organizations/" + organizationId, + request, + true, + false, + ); + } } diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index ca5ccc17b5..9470db9447 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -7,6 +7,7 @@ export enum FeatureFlag { KeyRotationImprovements = "key-rotation-improvements", FlexibleCollectionsMigration = "flexible-collections-migration", ShowPaymentMethodWarningBanners = "show-payment-method-warning-banners", + EnableConsolidatedBilling = "enable-consolidated-billing", } // Replace this with a type safe lookup of the feature flag values in PM-2282 From a6e178f1e60e8de42bd205e27dd4295a6ab79a6a Mon Sep 17 00:00:00 2001 From: Tom <144813356+ttalty@users.noreply.github.com> Date: Tue, 2 Apr 2024 12:39:06 -0400 Subject: [PATCH 086/351] [PM-5574] sends state provider (#8373) * Adding the key definitions and tests and initial send state service * Adding the abstraction and implementing * Planning comments * Everything but fixing the send tests * Moving send tests over to the state provider * jslib needed name refactor * removing get/set encrypted sends from web vault state service * browser send state service factory * Fixing conflicts * Removing send service from services module and fixing send service observable * Commenting the migrator to be clear on why only encrypted * No need for service factories in browser * browser send service is no longer needed * Key def test cases to use toStrictEqual * Running prettier * Creating send test data to avoid code duplication * Adding state provider and account service to send in cli * Fixing the send service test cases * Fixing state definition keys * Moving to observables and implementing encryption service * Fixing key def tests * The cli was using the deprecated get method * The observables init doesn't need to happen in constructor * Missed commented out code * If enc key is null get user key * Service factory fix --- .../browser/src/background/main.background.ts | 7 +- .../service-factories/send-service.factory.ts | 17 +- .../send-state-provider.factory.ts | 28 ++ apps/cli/src/bw.ts | 7 +- .../send/commands/remove-password.command.ts | 2 +- apps/web/src/app/core/state/state.service.ts | 14 - .../src/services/jslib-services.module.ts | 10 +- libs/angular/src/tools/send/send.component.ts | 14 +- .../platform/abstractions/state.service.ts | 18 -- .../src/platform/models/domain/account.ts | 3 - .../src/platform/services/state.service.ts | 41 --- .../src/platform/state/state-definitions.ts | 4 + libs/common/src/state-migrations/migrate.ts | 7 +- .../54-move-encrypted-sends.spec.ts | 236 +++++++++++++++ .../migrations/54-move-encrypted-sends.ts | 67 +++++ .../send/services/key-definitions.spec.ts | 21 ++ .../tools/send/services/key-definitions.ts | 13 + .../send-state.provider.abstraction.ts | 17 ++ .../send/services/send-state.provider.spec.ts | 48 ++++ .../send/services/send-state.provider.ts | 47 +++ .../send/services/send.service.abstraction.ts | 5 - .../tools/send/services/send.service.spec.ts | 270 +++++++----------- .../src/tools/send/services/send.service.ts | 117 +++----- .../services/test-data/send-tests.data.ts | 79 +++++ .../src/vault/services/sync/sync.service.ts | 2 +- 25 files changed, 767 insertions(+), 327 deletions(-) create mode 100644 apps/browser/src/background/service-factories/send-state-provider.factory.ts create mode 100644 libs/common/src/state-migrations/migrations/54-move-encrypted-sends.spec.ts create mode 100644 libs/common/src/state-migrations/migrations/54-move-encrypted-sends.ts create mode 100644 libs/common/src/tools/send/services/key-definitions.spec.ts create mode 100644 libs/common/src/tools/send/services/key-definitions.ts create mode 100644 libs/common/src/tools/send/services/send-state.provider.abstraction.ts create mode 100644 libs/common/src/tools/send/services/send-state.provider.spec.ts create mode 100644 libs/common/src/tools/send/services/send-state.provider.ts create mode 100644 libs/common/src/tools/send/services/test-data/send-tests.data.ts diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 102dad80a7..ea43aecff9 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -146,6 +146,7 @@ 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 } from "@bitwarden/common/tools/send/services/send-state.provider"; import { SendService } from "@bitwarden/common/tools/send/services/send.service"; import { InternalSendService as InternalSendServiceAbstraction } from "@bitwarden/common/tools/send/services/send.service.abstraction"; import { UserId } from "@bitwarden/common/types/guid"; @@ -276,6 +277,7 @@ export default class MainBackground { eventUploadService: EventUploadServiceAbstraction; policyService: InternalPolicyServiceAbstraction; sendService: InternalSendServiceAbstraction; + sendStateProvider: SendStateProvider; fileUploadService: FileUploadServiceAbstraction; cipherFileUploadService: CipherFileUploadServiceAbstraction; organizationService: InternalOrganizationServiceAbstraction; @@ -707,11 +709,14 @@ export default class MainBackground { logoutCallback, ); this.containerService = new ContainerService(this.cryptoService, this.encryptService); + + this.sendStateProvider = new SendStateProvider(this.stateProvider); this.sendService = new SendService( this.cryptoService, this.i18nService, this.keyGenerationService, - this.stateService, + this.sendStateProvider, + this.encryptService, ); this.sendApiService = new SendApiService( this.apiService, diff --git a/apps/browser/src/background/service-factories/send-service.factory.ts b/apps/browser/src/background/service-factories/send-service.factory.ts index 7c64bc076a..942861b926 100644 --- a/apps/browser/src/background/service-factories/send-service.factory.ts +++ b/apps/browser/src/background/service-factories/send-service.factory.ts @@ -5,6 +5,10 @@ import { CryptoServiceInitOptions, cryptoServiceFactory, } from "../../platform/background/service-factories/crypto-service.factory"; +import { + EncryptServiceInitOptions, + encryptServiceFactory, +} from "../../platform/background/service-factories/encrypt-service.factory"; import { FactoryOptions, CachedServices, @@ -18,10 +22,11 @@ import { KeyGenerationServiceInitOptions, keyGenerationServiceFactory, } from "../../platform/background/service-factories/key-generation-service.factory"; + import { - stateServiceFactory, - StateServiceInitOptions, -} from "../../platform/background/service-factories/state-service.factory"; + SendStateProviderInitOptions, + sendStateProviderFactory, +} from "./send-state-provider.factory"; type SendServiceFactoryOptions = FactoryOptions; @@ -29,7 +34,8 @@ export type SendServiceInitOptions = SendServiceFactoryOptions & CryptoServiceInitOptions & I18nServiceInitOptions & KeyGenerationServiceInitOptions & - StateServiceInitOptions; + SendStateProviderInitOptions & + EncryptServiceInitOptions; export function sendServiceFactory( cache: { sendService?: InternalSendService } & CachedServices, @@ -44,7 +50,8 @@ export function sendServiceFactory( await cryptoServiceFactory(cache, opts), await i18nServiceFactory(cache, opts), await keyGenerationServiceFactory(cache, opts), - await stateServiceFactory(cache, opts), + await sendStateProviderFactory(cache, opts), + await encryptServiceFactory(cache, opts), ), ); } diff --git a/apps/browser/src/background/service-factories/send-state-provider.factory.ts b/apps/browser/src/background/service-factories/send-state-provider.factory.ts new file mode 100644 index 0000000000..01319756e4 --- /dev/null +++ b/apps/browser/src/background/service-factories/send-state-provider.factory.ts @@ -0,0 +1,28 @@ +import { SendStateProvider } from "@bitwarden/common/tools/send/services/send-state.provider"; + +import { + CachedServices, + FactoryOptions, + factory, +} from "../../platform/background/service-factories/factory-options"; +import { + StateProviderInitOptions, + stateProviderFactory, +} from "../../platform/background/service-factories/state-provider.factory"; + +type SendStateProviderFactoryOptions = FactoryOptions; + +export type SendStateProviderInitOptions = SendStateProviderFactoryOptions & + StateProviderInitOptions; + +export function sendStateProviderFactory( + cache: { sendStateProvider?: SendStateProvider } & CachedServices, + opts: SendStateProviderInitOptions, +): Promise<SendStateProvider> { + return factory( + cache, + "sendStateProvider", + opts, + async () => new SendStateProvider(await stateProviderFactory(cache, opts)), + ); +} diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index 804b05e8e3..3815fc773b 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -106,6 +106,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"; @@ -194,6 +195,7 @@ export class Main { sendProgram: SendProgram; logService: ConsoleLogService; sendService: SendService; + sendStateProvider: SendStateProvider; fileUploadService: FileUploadService; cipherFileUploadService: CipherFileUploadService; keyConnectorService: KeyConnectorService; @@ -388,11 +390,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( 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/web/src/app/core/state/state.service.ts b/apps/web/src/app/core/state/state.service.ts index a80384d179..54e456d34c 100644 --- a/apps/web/src/app/core/state/state.service.ts +++ b/apps/web/src/app/core/state/state.service.ts @@ -18,7 +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"; @@ -71,19 +70,6 @@ export class StateService extends BaseStateService<GlobalState, Account> { 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<void> { - options = this.reconcileOptions(options, await this.defaultInMemoryOptions()); - return await super.setEncryptedSends(value, options); - } - override async getLastSync(options?: StorageOptions): Promise<string> { options = this.reconcileOptions(options, await this.defaultInMemoryOptions()); return await super.getLastSync(options); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 6378eed755..73f2bb4a32 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -191,6 +191,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, @@ -567,9 +569,15 @@ const safeProviders: SafeProvider[] = [ CryptoServiceAbstraction, I18nServiceAbstraction, KeyGenerationServiceAbstraction, - StateServiceAbstraction, + SendStateProviderAbstraction, + EncryptService, ], }), + safeProvider({ + provide: SendStateProviderAbstraction, + useClass: SendStateProvider, + deps: [StateProvider], + }), safeProvider({ provide: SendApiServiceAbstraction, useClass: SendApiService, diff --git a/libs/angular/src/tools/send/send.component.ts b/libs/angular/src/tools/send/send.component.ts index b3b871a177..90d9b39e8c 100644 --- a/libs/angular/src/tools/send/send.component.ts +++ b/libs/angular/src/tools/send/send.component.ts @@ -1,5 +1,5 @@ import { Directive, NgZone, OnDestroy, OnInit } from "@angular/core"; -import { Subject, firstValueFrom, takeUntil } from "rxjs"; +import { Subject, firstValueFrom, mergeMap, takeUntil } from "rxjs"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; @@ -77,9 +77,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 { diff --git a/libs/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index 9bc6d698a7..4c876316cd 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -7,8 +7,6 @@ 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 { MasterKey } from "../../types/key"; import { CipherData } from "../../vault/models/data/cipher.data"; @@ -151,14 +149,6 @@ export abstract class StateService<T extends Account = Account> { * @deprecated For migration purposes only, use setDecryptedUserKeyPin instead */ setDecryptedPinProtected: (value: EncString, options?: StorageOptions) => Promise<void>; - /** - * @deprecated Do not call this directly, use SendService - */ - getDecryptedSends: (options?: StorageOptions) => Promise<SendView[]>; - /** - * @deprecated Do not call this directly, use SendService - */ - setDecryptedSends: (value: SendView[], options?: StorageOptions) => Promise<void>; getDuckDuckGoSharedKey: (options?: StorageOptions) => Promise<string>; setDuckDuckGoSharedKey: (value: string, options?: StorageOptions) => Promise<void>; getAdminAuthRequest: (options?: StorageOptions) => Promise<AdminAuthRequestStorable | null>; @@ -197,14 +187,6 @@ export abstract class StateService<T extends Account = Account> { * @deprecated For migration purposes only, use setEncryptedUserKeyPin instead */ setEncryptedPinProtected: (value: string, options?: StorageOptions) => Promise<void>; - /** - * @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<void>; getEverBeenUnlocked: (options?: StorageOptions) => Promise<boolean>; setEverBeenUnlocked: (value: boolean, options?: StorageOptions) => Promise<void>; getForceSetPasswordReason: (options?: StorageOptions) => Promise<ForceSetPasswordReason>; diff --git a/libs/common/src/platform/models/domain/account.ts b/libs/common/src/platform/models/domain/account.ts index 798a60600a..4ed36fd389 100644 --- a/libs/common/src/platform/models/domain/account.ts +++ b/libs/common/src/platform/models/domain/account.ts @@ -9,8 +9,6 @@ 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"; @@ -71,7 +69,6 @@ export class AccountData { CipherView >(); localData?: any; - sends?: DataEncryptionPair<SendData, SendView> = new DataEncryptionPair<SendData, SendView>(); passwordGenerationHistory?: EncryptionPair< GeneratedPasswordHistory[], GeneratedPasswordHistory[] diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index 57a2085ccf..fb62af250b 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -11,8 +11,6 @@ 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 { MasterKey } from "../../types/key"; import { CipherData } from "../../vault/models/data/cipher.data"; @@ -614,24 +612,6 @@ export class StateService< ); } - @withPrototypeForArrayMembers(SendView) - async getDecryptedSends(options?: StorageOptions): Promise<SendView[]> { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) - )?.data?.sends?.decrypted; - } - - async setDecryptedSends(value: SendView[], options?: StorageOptions): Promise<void> { - 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 getDuckDuckGoSharedKey(options?: StorageOptions): Promise<string> { options = this.reconcileOptions(options, await this.defaultSecureStorageOptions()); if (options?.userId == null) { @@ -825,27 +805,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<void> { - 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<boolean> { return ( (await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))) diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index 9fca0e9445..d50a3e6ac7 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -102,6 +102,10 @@ 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 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 diff --git a/libs/common/src/state-migrations/migrate.ts b/libs/common/src/state-migrations/migrate.ts index 4e1a0529fc..faccddb0af 100644 --- a/libs/common/src/state-migrations/migrate.ts +++ b/libs/common/src/state-migrations/migrate.ts @@ -50,6 +50,7 @@ import { KeyConnectorMigrator } from "./migrations/50-move-key-connector-to-stat 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 { 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"; @@ -57,7 +58,8 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting import { MinVersionMigrator } from "./migrations/min-version"; export const MIN_VERSION = 3; -export const CURRENT_VERSION = 53; +export const CURRENT_VERSION = 54; + export type MinVersion = typeof MIN_VERSION; export function createMigrationBuilder() { @@ -112,7 +114,8 @@ export function createMigrationBuilder() { .with(KeyConnectorMigrator, 49, 50) .with(RememberedEmailMigrator, 50, 51) .with(DeleteInstalledVersion, 51, 52) - .with(DeviceTrustCryptoServiceStateProviderMigrator, 52, CURRENT_VERSION); + .with(DeviceTrustCryptoServiceStateProviderMigrator, 52, 53) + .with(SendMigrator, 53, 54); } export async function currentVersion( 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<MigrationHelper>; + 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<string, SendData>; + }; + }; +}; + +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<void> { + const accounts = await helper.getAccounts<ExpectedSendState>(); + + async function migrateAccount(userId: string, account: ExpectedSendState): Promise<void> { + 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<void> { + const accounts = await helper.getAccounts<ExpectedSendState>(); + + async function rollbackAccount(userId: string, account: ExpectedSendState): Promise<void> { + 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/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..b117c52268 --- /dev/null +++ b/libs/common/src/tools/send/services/key-definitions.ts @@ -0,0 +1,13 @@ +import { KeyDefinition, SEND_DISK, SEND_MEMORY } 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 = KeyDefinition.record<SendData>(SEND_DISK, "sendUserEncrypted", { + deserializer: (obj: SendData) => obj, +}); + +/** Decrypted send state stored in memory */ +export const SEND_USER_DECRYPTED = new KeyDefinition<SendView[]>(SEND_MEMORY, "sendUserDecrypted", { + deserializer: (obj) => obj, +}); 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..fc793dba67 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,23 @@ -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 { AuthenticationStatus } from "../../../auth/enums/authentication-status"; 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 +25,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 +43,53 @@ 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", + status: AuthenticationStatus.Unlocked, }); - 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 +101,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 +116,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 +133,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 +147,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 +158,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 +172,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 +183,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 +197,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 +210,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 +224,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 +235,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 +249,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 +260,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 +274,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 +285,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 +299,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 +312,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 +325,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 +333,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 +345,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 +367,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 +379,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 +396,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 +421,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 +439,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/vault/services/sync/sync.service.ts b/libs/common/src/vault/services/sync/sync.service.ts index 3e8bd92a7a..d4601d9621 100644 --- a/libs/common/src/vault/services/sync/sync.service.ts +++ b/libs/common/src/vault/services/sync/sync.service.ts @@ -244,7 +244,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) From f79d1592773574f7495337c885c437ceeccc31d4 Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Tue, 2 Apr 2024 10:16:42 -0700 Subject: [PATCH 087/351] [PM-5500] Implement StateProvider in RouterService (#8119) * implement StateProvider in RouterService * Remove 'export' Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com> * Skip parameter Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com> --------- Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com> --- apps/web/src/app/core/router.service.ts | 29 +++++++++++++++---- .../platform/abstractions/state.service.ts | 13 --------- .../platform/models/domain/global-state.ts | 1 - .../src/platform/services/state.service.ts | 17 ----------- .../src/platform/state/state-definitions.ts | 1 + 5 files changed, 25 insertions(+), 36 deletions(-) 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<string>; + 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<void> { - await this.stateService.setDeepLinkRedirectUrl(url); + await this.deepLinkRedirectUrlState.update(() => url); } /** * Fetch and clear persisted LoginRedirectUrl if present in state */ async getAndClearLoginRedirectUrl(): Promise<string> | 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/libs/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index 4c876316cd..4971481381 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -243,18 +243,5 @@ export abstract class StateService<T extends Account = Account> { setVaultTimeoutAction: (value: string, options?: StorageOptions) => Promise<void>; getApproveLoginRequests: (options?: StorageOptions) => Promise<boolean>; setApproveLoginRequests: (value: boolean, options?: StorageOptions) => Promise<void>; - /** - * 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<string>; - /** - * 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<void>; nextUpActiveUser: () => Promise<UserId>; } diff --git a/libs/common/src/platform/models/domain/global-state.ts b/libs/common/src/platform/models/domain/global-state.ts index cb9e3f71b3..703a998d1c 100644 --- a/libs/common/src/platform/models/domain/global-state.ts +++ b/libs/common/src/platform/models/domain/global-state.ts @@ -4,5 +4,4 @@ export class GlobalState { vaultTimeoutAction?: string; enableBrowserIntegration?: boolean; enableBrowserIntegrationFingerprint?: boolean; - deepLinkRedirectUrl?: string; } diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index fb62af250b..a35659a7ac 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -1173,23 +1173,6 @@ export class StateService< ); } - async getDeepLinkRedirectUrl(options?: StorageOptions): Promise<string> { - return ( - await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.deepLinkRedirectUrl; - } - - async setDeepLinkRedirectUrl(url: string, options?: StorageOptions): Promise<void> { - 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<TGlobalState> { let globals: TGlobalState; if (this.useMemory(options.storageLocation)) { diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index d50a3e6ac7..979321c1e3 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -38,6 +38,7 @@ export const BILLING_DISK = new StateDefinition("billing", "disk"); export const KEY_CONNECTOR_DISK = new StateDefinition("keyConnector", "disk"); export const ACCOUNT_MEMORY = new StateDefinition("account", "memory"); 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", }); From 7df3304a25a60c89da3da9368868d22734044ad9 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Tue, 2 Apr 2024 13:53:13 -0400 Subject: [PATCH 088/351] [AC-1759] Update subscription status section (#8578) * Resolve subscription status confusion * Add feature flag --- .../organization-billing.module.ts | 2 + ...nization-subscription-cloud.component.html | 93 +++++---- ...ganization-subscription-cloud.component.ts | 13 +- .../subscription-status.component.html | 32 +++ .../subscription-status.component.ts | 184 ++++++++++++++++++ apps/web/src/locales/en/messages.json | 52 +++++ .../models/response/subscription.response.ts | 8 + libs/common/src/enums/feature-flag.enum.ts | 1 + 8 files changed, 341 insertions(+), 44 deletions(-) create mode 100644 apps/web/src/app/billing/organizations/subscription-status.component.html create mode 100644 apps/web/src/app/billing/organizations/subscription-status.component.ts 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-subscription-cloud.component.html b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html index 5f767d85c4..b4fac65854 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,51 +12,58 @@ ></app-org-subscription-hidden> <ng-container *ngIf="sub && firstLoaded"> - <bit-callout - type="warning" - title="{{ 'canceled' | i18n }}" - *ngIf="subscription && subscription.cancelled" - > - {{ "subscriptionCanceled" | i18n }}</bit-callout - > - <bit-callout - type="warning" - title="{{ 'pendingCancellation' | i18n }}" - *ngIf="subscriptionMarkedForCancel" - > - <p>{{ "subscriptionPendingCanceled" | i18n }}</p> - <button - *ngIf="userOrg.canEditSubscription" - bitButton - buttonType="secondary" - [bitAction]="reinstate" - type="button" + <ng-container *ngIf="!(showUpdatedSubscriptionStatusSection$ | async)"> + <bit-callout + type="warning" + title="{{ 'canceled' | i18n }}" + *ngIf="subscription && subscription.cancelled" > - {{ "reinstateSubscription" | i18n }} - </button> - </bit-callout> + {{ "subscriptionCanceled" | i18n }}</bit-callout + > + <bit-callout + type="warning" + title="{{ 'pendingCancellation' | i18n }}" + *ngIf="subscriptionMarkedForCancel" + > + <p>{{ "subscriptionPendingCanceled" | i18n }}</p> + <button + *ngIf="userOrg.canEditSubscription" + bitButton + buttonType="secondary" + [bitAction]="reinstate" + type="button" + > + {{ "reinstateSubscription" | i18n }} + </button> + </bit-callout> - <dl class="tw-grid tw-grid-flow-col tw-grid-rows-2"> - <dt>{{ "billingPlan" | i18n }}</dt> - <dd>{{ sub.plan.name }}</dd> - <ng-container *ngIf="subscription"> - <dt>{{ "status" | i18n }}</dt> - <dd> - <span class="tw-capitalize">{{ - isSponsoredSubscription ? "sponsored" : subscription.status || "-" - }}</span> - <span bitBadge variant="warning" *ngIf="subscriptionMarkedForCancel">{{ - "pendingCancellation" | i18n - }}</span> - </dd> - <dt [ngClass]="{ 'tw-text-danger': isExpired }"> - {{ "subscriptionExpiration" | i18n }} - </dt> - <dd [ngClass]="{ 'tw-text-danger': isExpired }"> - {{ nextInvoice ? (nextInvoice.date | date: "mediumDate") : "-" }} - </dd> - </ng-container> - </dl> + <dl class="tw-grid tw-grid-flow-col tw-grid-rows-2"> + <dt>{{ "billingPlan" | i18n }}</dt> + <dd>{{ sub.plan.name }}</dd> + <ng-container *ngIf="subscription"> + <dt>{{ "status" | i18n }}</dt> + <dd> + <span class="tw-capitalize">{{ + isSponsoredSubscription ? "sponsored" : subscription.status || "-" + }}</span> + <span bitBadge variant="warning" *ngIf="subscriptionMarkedForCancel">{{ + "pendingCancellation" | i18n + }}</span> + </dd> + <dt [ngClass]="{ 'tw-text-danger': isExpired }"> + {{ "subscriptionExpiration" | i18n }} + </dt> + <dd [ngClass]="{ 'tw-text-danger': isExpired }"> + {{ nextInvoice ? (nextInvoice.date | date: "mediumDate") : "-" }} + </dd> + </ng-container> + </dl> + </ng-container> + <app-subscription-status + *ngIf="showUpdatedSubscriptionStatusSection$ | async" + [organizationSubscriptionResponse]="sub" + (reinstatementRequested)="reinstate()" + ></app-subscription-status> <ng-container *ngIf="userOrg.canEditSubscription"> <div class="tw-flex-col"> <strong class="tw-block tw-border-0 tw-border-b tw-border-solid tw-border-secondary-300">{{ 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..0810f79b8e 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,6 +11,8 @@ 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"; @@ -41,6 +43,8 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy showSecretsManagerSubscribe = false; firstLoaded = false; loading: boolean; + locale: string; + showUpdatedSubscriptionStatusSection$: Observable<boolean>; protected readonly teamsStarter = ProductType.TeamsStarter; @@ -55,6 +59,7 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy private organizationApiService: OrganizationApiServiceAbstraction, private route: ActivatedRoute, private dialogService: DialogService, + private configService: ConfigService, ) {} async ngOnInit() { @@ -74,6 +79,11 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy takeUntil(this.destroy$), ) .subscribe(); + + this.showUpdatedSubscriptionStatusSection$ = this.configService.getFeatureFlag$( + FeatureFlag.AC1795_UpdatedSubscriptionStatusSection, + false, + ); } ngOnDestroy() { @@ -86,6 +96,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); 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..4bb2c91b85 --- /dev/null +++ b/apps/web/src/app/billing/organizations/subscription-status.component.html @@ -0,0 +1,32 @@ +<ng-container> + <bit-callout *ngIf="data.callout" [type]="data.callout.severity" [title]="data.callout.header"> + <p>{{ data.callout.body }}</p> + <button + *ngIf="data.callout.showReinstatementButton" + bitButton + buttonType="secondary" + [bitAction]="requestReinstatement" + type="button" + > + {{ "reinstateSubscription" | i18n }} + </button> + </bit-callout> + <dl class="tw-grid tw-grid-flow-col tw-grid-rows-2"> + <dt>{{ "billingPlan" | i18n }}</dt> + <dd>{{ planName }}</dd> + <ng-container> + <dt>{{ data.status.label }}</dt> + <dd> + <span class="tw-capitalize"> + {{ displayedStatus }} + </span> + </dd> + <dt> + {{ data.date.label | titlecase }} + </dt> + <dd> + {{ data.date.value | date: "mediumDate" }} + </dd> + </ng-container> + </dl> +</ng-container> 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..54af940be5 --- /dev/null +++ b/apps/web/src/app/billing/organizations/subscription-status.component.ts @@ -0,0 +1,184 @@ +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<void>(); + + 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.status != "canceled" && this.subscription.cancelAtEndDate + ? "pending_cancellation" + : this.subscription.status; + } + + 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 "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/locales/en/messages.json b/apps/web/src/locales/en/messages.json index b8e5a5ff4d..05697461a9 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -7678,5 +7678,57 @@ }, "subscriptionUpdateFailed": { "message": "Subscription update failed" + }, + "trial": { + "message": "Trial", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Past due", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Subscription expired", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Unpaid invoice", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "To reactivate your subscription, please resolve the past due invoices.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Cancellation date", + "description": "The date header used when a subscription is cancelled." } } 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/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 9470db9447..9d427034bd 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -8,6 +8,7 @@ export enum FeatureFlag { FlexibleCollectionsMigration = "flexible-collections-migration", ShowPaymentMethodWarningBanners = "show-payment-method-warning-banners", EnableConsolidatedBilling = "enable-consolidated-billing", + AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section", } // Replace this with a type safe lookup of the feature flag values in PM-2282 From 2f3e37d71340cce7ec57815e3eb18c7064f7373c Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Tue, 2 Apr 2024 14:58:54 -0500 Subject: [PATCH 089/351] [SM-889] FIX - bulk activate SM members (#8375) * fix: add accessSecretsManager property and filter checked users, refs SM-889 * fix: load members list on bulk sm complete, refs SM-889 --- .../organizations/core/views/organization-user.view.ts | 4 ++++ .../admin-console/organizations/members/people.component.ts | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) 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/members/people.component.ts b/apps/web/src/app/admin-console/organizations/members/people.component.ts index b2aedacc80..8a303ddfe5 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 @@ -571,7 +571,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 +589,7 @@ export class PeopleComponent await lastValueFrom(dialogRef.closed); this.selectAll(false); + await this.load(); } async events(user: OrganizationUserView) { From 165f9c460a74caa94c5f5f984c15159439bb8326 Mon Sep 17 00:00:00 2001 From: Will Martin <contact@willmartian.com> Date: Tue, 2 Apr 2024 16:00:08 -0400 Subject: [PATCH 090/351] remove unused OrganizationBillingTabComponent (#8435) Co-authored-by: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> --- .../organization-billing-tab.component.ts | 31 ------------------- 1 file changed, 31 deletions(-) delete mode 100644 apps/web/src/app/billing/organizations/organization-billing-tab.component.ts 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<boolean>; - - 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, - ), - ); - } -} From a4475e82324b3ffb13fe1e3571686417753ea686 Mon Sep 17 00:00:00 2001 From: Oscar Hinton <Hinton@users.noreply.github.com> Date: Tue, 2 Apr 2024 22:31:12 +0200 Subject: [PATCH 091/351] [PM-6418] Fix environment selector on desktop (#8046) Fix environment selector being broken on desktop. When selecting self-hosted and filling in a url, the selector fails to update when returning and instead produces the following console error. --- .../desktop/src/auth/login/login.component.ts | 3 - .../environment-selector.component.html | 134 +++++++++--------- .../environment-selector.component.ts | 8 +- 3 files changed, 71 insertions(+), 74 deletions(-) diff --git a/apps/desktop/src/auth/login/login.component.ts b/apps/desktop/src/auth/login/login.component.ts index eb7b924362..a810a29a26 100644 --- a/apps/desktop/src/auth/login/login.component.ts +++ b/apps/desktop/src/auth/login/login.component.ts @@ -3,7 +3,6 @@ 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"; @@ -37,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<void> = new Subject(); webVaultHostname = ""; 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 @@ -<div class="environment-selector-btn"> - {{ "loggingInOn" | i18n }}: - <button - type="button" - (click)="toggle(null)" - cdkOverlayOrigin - #trigger="cdkOverlayOrigin" - aria-haspopup="dialog" - aria-controls="cdk-overlay-container" - > - <span class="text-primary"> - <ng-container *ngIf="selectedRegion$ | async as selectedRegion; else fallback"> - {{ selectedRegion.domain }} - </ng-container> - <ng-template #fallback> - {{ "selfHostedServer" | i18n }} - </ng-template> - </span> - <i class="bwi bwi-fw bwi-sm bwi-angle-down" aria-hidden="true"></i> - </button> -</div> - -<ng-template - cdkConnectedOverlay - [cdkConnectedOverlayOrigin]="trigger" - [cdkConnectedOverlayOpen]="isOpen" - [cdkConnectedOverlayPositions]="overlayPosition" - [cdkConnectedOverlayHasBackdrop]="true" - [cdkConnectedOverlayBackdropClass]="'cdk-overlay-transparent-backdrop'" - (backdropClick)="isOpen = false" - (detach)="close()" +<ng-container + *ngIf="{ + selectedRegion: selectedRegion$ | async + } as data" > - <div class="box-content"> - <div - class="environment-selector-dialog" - [@transformPanel]="'open'" - cdkTrapFocus - cdkTrapFocusAutoCapture - role="dialog" - aria-modal="true" + <div class="environment-selector-btn"> + {{ "loggingInOn" | i18n }}: + <button + type="button" + (click)="toggle(null)" + cdkOverlayOrigin + #trigger="cdkOverlayOrigin" + aria-haspopup="dialog" + aria-controls="cdk-overlay-container" > - <ng-container *ngFor="let region of availableRegions"> + <span class="text-primary"> + <ng-container *ngIf="data.selectedRegion; else fallback"> + {{ data.selectedRegion.domain }} + </ng-container> + <ng-template #fallback> + {{ "selfHostedServer" | i18n }} + </ng-template> + </span> + <i class="bwi bwi-fw bwi-sm bwi-angle-down" aria-hidden="true"></i> + </button> + </div> + + <ng-template + cdkConnectedOverlay + [cdkConnectedOverlayOrigin]="trigger" + [cdkConnectedOverlayOpen]="isOpen" + [cdkConnectedOverlayPositions]="overlayPosition" + [cdkConnectedOverlayHasBackdrop]="true" + [cdkConnectedOverlayBackdropClass]="'cdk-overlay-transparent-backdrop'" + (backdropClick)="isOpen = false" + (detach)="close()" + > + <div class="box-content"> + <div + class="environment-selector-dialog" + [@transformPanel]="'open'" + cdkTrapFocus + cdkTrapFocusAutoCapture + role="dialog" + aria-modal="true" + > + <ng-container *ngFor="let region of availableRegions"> + <button + type="button" + class="environment-selector-dialog-item" + (click)="toggle(region.key)" + [attr.aria-pressed]="data.selectedRegion === region ? 'true' : 'false'" + > + <i + class="bwi bwi-fw bwi-sm bwi-check" + style="padding-bottom: 1px" + aria-hidden="true" + [style.visibility]="data.selectedRegion === region ? 'visible' : 'hidden'" + ></i> + <span>{{ region.domain }}</span> + </button> + <br /> + </ng-container> <button type="button" class="environment-selector-dialog-item" - (click)="toggle(region.key)" - [attr.aria-pressed]="selectedEnvironment === region.key ? 'true' : 'false'" + (click)="toggle(ServerEnvironmentType.SelfHosted)" + [attr.aria-pressed]="data.selectedRegion ? 'false' : 'true'" > <i class="bwi bwi-fw bwi-sm bwi-check" style="padding-bottom: 1px" aria-hidden="true" - [style.visibility]="selectedEnvironment === region.key ? 'visible' : 'hidden'" + [style.visibility]="data.selectedRegion ? 'hidden' : 'visible'" ></i> - <span>{{ region.domain }}</span> + <span>{{ "selfHostedServer" | i18n }}</span> </button> - <br /> - </ng-container> - <button - type="button" - class="environment-selector-dialog-item" - (click)="toggle(ServerEnvironmentType.SelfHosted)" - [attr.aria-pressed]=" - selectedEnvironment === ServerEnvironmentType.SelfHosted ? 'true' : 'false' - " - > - <i - class="bwi bwi-fw bwi-sm bwi-check" - style="padding-bottom: 1px" - aria-hidden="true" - [style.visibility]=" - selectedEnvironment === ServerEnvironmentType.SelfHosted ? 'visible' : 'hidden' - " - ></i> - <span>{{ "selfHostedServer" | i18n }}</span> - </button> + </div> </div> - </div> -</ng-template> + </ng-template> +</ng-container> 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", From 7ea717aa5dd921b56c4c5e8e82c8ff89605d19a8 Mon Sep 17 00:00:00 2001 From: Ariful Alam <swazan.arif@gmail.com> Date: Wed, 3 Apr 2024 02:35:23 +0600 Subject: [PATCH 092/351] Use `nullValidator` to accept No folder while moving (#5645) Co-authored-by: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> --- .../bulk-move-dialog/bulk-move-dialog.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component.ts b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component.ts index 09522beadc..cdf45d0669 100644 --- a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component.ts +++ b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component.ts @@ -41,7 +41,7 @@ export class BulkMoveDialogComponent implements OnInit { cipherIds: string[] = []; formGroup = this.formBuilder.group({ - folderId: ["", [Validators.required]], + folderId: ["", [Validators.nullValidator]], }); folders$: Observable<FolderView[]>; From f87286b1dff10ee142503d3ca6c3bd9bede86071 Mon Sep 17 00:00:00 2001 From: Cesar Gonzalez <cesar.a.gonzalezcs@gmail.com> Date: Tue, 2 Apr 2024 15:41:59 -0500 Subject: [PATCH 093/351] [PM-2871] Submenu UI elements cannot be accessed by keyboard navigation when screen reader is active (#8357) * [PM-2871] Submenu UI elements cannot be navigated through keyboard when screen reader is active * [PM-2871] Adding an aria-hidden value to menu divider elements --- libs/components/src/menu/menu-divider.component.html | 1 + libs/components/src/menu/menu-trigger-for.directive.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/libs/components/src/menu/menu-divider.component.html b/libs/components/src/menu/menu-divider.component.html index c7c2c739d2..98048261cf 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" role="separator" + aria-hidden="true" ></div> diff --git a/libs/components/src/menu/menu-trigger-for.directive.ts b/libs/components/src/menu/menu-trigger-for.directive.ts index 20ae0b1ce7..7e392f241f 100644 --- a/libs/components/src/menu/menu-trigger-for.directive.ts +++ b/libs/components/src/menu/menu-trigger-for.directive.ts @@ -88,6 +88,7 @@ export class MenuTriggerForDirective implements OnDestroy { } this.destroyMenu(); }); + this.menu.keyManager.setFirstItemActive(); this.keyDownEventsSub = this.menu.keyManager && this.overlayRef From 45dc244cc41755a0442b9d40908fce37511c9662 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 2 Apr 2024 16:55:04 -0500 Subject: [PATCH 094/351] [deps] AC: Update mini-css-extract-plugin to v2.8.1 (#8475) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 83 ++++++++++++++++++++++++++++++++++++++++++++--- package.json | 2 +- 2 files changed, 79 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index ef23904967..7947512749 100644 --- a/package-lock.json +++ b/package-lock.json @@ -155,7 +155,7 @@ "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", @@ -907,6 +907,78 @@ "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/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/mini-css-extract-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/mini-css-extract-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "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/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -28638,12 +28710,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" diff --git a/package.json b/package.json index 88ba36e3c0..f161c223ed 100644 --- a/package.json +++ b/package.json @@ -116,7 +116,7 @@ "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", From e79662adf776bcf5180dbaa77465135489c2fb00 Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Wed, 3 Apr 2024 09:27:49 +1000 Subject: [PATCH 095/351] Remove unused @Input() decorator on multi-select (#8396) --- libs/components/src/multi-select/multi-select.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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; From 978846a6c54d0187d914e90d73706faeeb50a8c0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 2 Apr 2024 20:12:31 -0500 Subject: [PATCH 096/351] [deps] AC: Update html-loader to v5 (#8479) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7947512749..cfc0baf297 100644 --- a/package-lock.json +++ b/package-lock.json @@ -147,7 +147,7 @@ "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.6.0", "husky": "9.0.11", @@ -22828,16 +22828,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", diff --git a/package.json b/package.json index f161c223ed..466ee7e777 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "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.6.0", "husky": "9.0.11", From ac84b43782345fc8927103f60731af8c262b4093 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 2 Apr 2024 20:12:59 -0500 Subject: [PATCH 097/351] [deps] AC: Update postcss to v8.4.38 (#8366) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 16 ++++++++-------- package.json | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index cfc0baf297..79a7947703 100644 --- a/package-lock.json +++ b/package-lock.json @@ -158,7 +158,7 @@ "mini-css-extract-plugin": "2.8.1", "node-ipc": "9.2.1", "pkg": "5.8.1", - "postcss": "8.4.35", + "postcss": "8.4.38", "postcss-loader": "8.1.1", "prettier": "3.2.2", "prettier-plugin-tailwindcss": "0.5.13", @@ -31472,9 +31472,9 @@ } }, "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": [ { @@ -31493,7 +31493,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" @@ -34936,9 +34936,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" diff --git a/package.json b/package.json index 466ee7e777..622f92f6eb 100644 --- a/package.json +++ b/package.json @@ -119,7 +119,7 @@ "mini-css-extract-plugin": "2.8.1", "node-ipc": "9.2.1", "pkg": "5.8.1", - "postcss": "8.4.35", + "postcss": "8.4.38", "postcss-loader": "8.1.1", "prettier": "3.2.2", "prettier-plugin-tailwindcss": "0.5.13", From 5fe8f9b76a7eb9f5a27cd3640c8260dd26dd7641 Mon Sep 17 00:00:00 2001 From: Matt Gibson <mgibson@bitwarden.com> Date: Wed, 3 Apr 2024 07:18:26 -0500 Subject: [PATCH 098/351] Autofill: Use UserKeyDefinitions for user-scoped data (#8588) * Do not clear badge settings on user events * Do not clear default uri match strategy * Use explicit clearOn events for autofill settings --- .../services/autofill-settings.service.ts | 19 +++++++++++++------ .../services/badge-settings.service.ts | 5 +++-- .../services/domain-settings.service.ts | 3 ++- 3 files changed, 18 insertions(+), 9 deletions(-) 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: [], }, ); From b53b211bd46f11a9c6ff19f078ca9f3b48608ad8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 3 Apr 2024 15:48:04 +0200 Subject: [PATCH 099/351] [deps]: Lock file maintenance (#7700) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [deps]: Lock file maintenance * Fix jest breaking changes --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Daniel García <dani-garcia@users.noreply.github.com> --- .../insert-autofill-content.service.spec.ts | 2 +- .../browser-platform-utils.service.spec.ts | 4 +- apps/desktop/desktop_native/Cargo.lock | 403 +- .../package-lock.json | 32 +- apps/desktop/src/package-lock.json | 592 +- .../platform/services/app-id.service.spec.ts | 2 +- package-lock.json | 13596 ++++++++-------- 7 files changed, 7373 insertions(+), 7258 deletions(-) diff --git a/apps/browser/src/autofill/services/insert-autofill-content.service.spec.ts b/apps/browser/src/autofill/services/insert-autofill-content.service.spec.ts index 5a123bf835..72bbfbf2e2 100644 --- a/apps/browser/src/autofill/services/insert-autofill-content.service.spec.ts +++ b/apps/browser/src/autofill/services/insert-autofill-content.service.spec.ts @@ -98,7 +98,7 @@ describe("InsertAutofillContentService", () => { }); afterEach(() => { - jest.resetAllMocks(); + jest.restoreAllMocks(); windowLocationSpy.mockRestore(); confirmSpy.mockRestore(); document.body.innerHTML = ""; diff --git a/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.spec.ts b/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.spec.ts index cf6816f405..0df8f26344 100644 --- a/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.spec.ts +++ b/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.spec.ts @@ -172,7 +172,7 @@ describe("Browser Utils Service", () => { }); afterEach(() => { - jest.resetAllMocks(); + jest.clearAllMocks(); }); it("sends a copy to clipboard message to the desktop application if a user is using the safari browser", async () => { @@ -264,7 +264,7 @@ describe("Browser Utils Service", () => { }); afterEach(() => { - jest.resetAllMocks(); + jest.clearAllMocks(); }); it("sends a ready from clipboard message to the desktop application if a user is using the safari browser", async () => { diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 446bce87a0..7646b63001 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", ] @@ -62,15 +62,15 @@ dependencies = [ [[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", @@ -95,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" @@ -123,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" @@ -140,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", @@ -163,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" @@ -219,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", ] @@ -238,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", @@ -260,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", @@ -270,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]] @@ -378,9 +375,9 @@ 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" @@ -396,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", @@ -422,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", @@ -479,9 +476,9 @@ dependencies = [ [[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", @@ -490,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" @@ -527,11 +524,11 @@ dependencies = [ [[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", @@ -549,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]] @@ -583,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", @@ -640,17 +646,11 @@ 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" @@ -699,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" @@ -715,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" @@ -730,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" @@ -751,9 +742,9 @@ 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", ] @@ -764,7 +755,7 @@ version = "2.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54a63d0570e4c3e0daf7a8d380563610e159f538e20448d6c911337246f40e84" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.5.0", "ctor", "napi-derive", "napi-sys", @@ -789,14 +780,14 @@ dependencies = [ "napi-derive-backend", "proc-macro2", "quote", - "syn 2.0.38", + "syn", ] [[package]] name = "napi-derive-backend" -version = "1.0.62" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f785a8b8d7b83e925f5aa6d2ae3c159d17fe137ac368dc185bef410e7acdaeb4" +checksum = "ce5126b64f6ad9e28e30e6d15213dd378626b38f556454afebc42f7f02a90902" dependencies = [ "convert_case", "once_cell", @@ -804,7 +795,7 @@ dependencies = [ "quote", "regex", "semver", - "syn 2.0.38", + "syn", ] [[package]] @@ -818,15 +809,14 @@ dependencies = [ [[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]] @@ -880,9 +870,9 @@ 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", ] @@ -938,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" @@ -950,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" @@ -971,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", ] @@ -1037,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", @@ -1049,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", @@ -1060,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" @@ -1081,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", @@ -1135,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", ] @@ -1190,26 +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" +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", @@ -1218,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", @@ -1231,28 +1210,27 @@ 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", ] @@ -1274,14 +1252,14 @@ checksum = "01742297787513b79cf8e29d1056ede1313e2420b7b3b15d0a768b4921f549df" 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", @@ -1290,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]] @@ -1309,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" @@ -1330,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", @@ -1361,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" @@ -1373,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" @@ -1391,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", @@ -1405,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", @@ -1433,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", @@ -1442,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", @@ -1653,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", diff --git a/apps/desktop/native-messaging-test-runner/package-lock.json b/apps/desktop/native-messaging-test-runner/package-lock.json index 3ea3cb7943..fbf27703d2 100644 --- a/apps/desktop/native-messaging-test-runner/package-lock.json +++ b/apps/desktop/native-messaging-test-runner/package-lock.json @@ -25,12 +25,10 @@ } }, "../../../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", @@ -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/src/package-lock.json b/apps/desktop/src/package-lock.json index b379e13d57..167c32cc81 100644 --- a/apps/desktop/src/package-lock.json +++ b/apps/desktop/src/package-lock.json @@ -9,19 +9,607 @@ "version": "2024.3.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/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/package-lock.json b/package-lock.json index 79a7947703..0666ae8d21 100644 --- a/package-lock.json +++ b/package-lock.json @@ -245,22 +245,6 @@ "@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.1" @@ -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,6 +635,16 @@ "postcss": "^8.1.0" } }, + "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": { + "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", @@ -638,52 +669,6 @@ "webpack": "^5.1.0" } }, - "node_modules/@angular-devkit/build-angular/node_modules/copy-webpack-plugin/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/copy-webpack-plugin/node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/@angular-devkit/build-angular/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", - "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/copy-webpack-plugin/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, "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", @@ -703,32 +688,6 @@ "url": "https://opencollective.com/webpack" } }, - "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==", - "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" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, "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", @@ -777,22 +736,6 @@ "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==", - "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/@angular-devkit/build-angular/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -813,6 +756,18 @@ "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", @@ -871,15 +826,6 @@ "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==", - "dev": true, - "dependencies": { - "tslib": "^2.1.0" - } - }, "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", @@ -889,18 +835,6 @@ "node": ">= 10" } }, - "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" - } - }, "node_modules/@angular-devkit/build-angular/node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -926,40 +860,6 @@ "webpack": "^5.0.0" } }, - "node_modules/@angular-devkit/build-angular/node_modules/mini-css-extract-plugin/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/mini-css-extract-plugin/node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/mini-css-extract-plugin/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, "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", @@ -979,6 +879,31 @@ "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", @@ -1029,6 +954,15 @@ "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", @@ -1116,6 +1050,31 @@ "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", @@ -1234,40 +1193,6 @@ } } }, - "node_modules/@angular-devkit/build-angular/node_modules/webpack-dev-server/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/webpack-dev-server/node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/webpack-dev-server/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": 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", @@ -1310,47 +1235,90 @@ "webpack": "^4.0.0 || ^5.0.0" } }, - "node_modules/@angular-devkit/build-angular/node_modules/ws": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", - "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "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.0.0" + "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": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" + "chokidar": "^3.5.2" }, "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { + "chokidar": { "optional": true } } }, - "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/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/architect": "0.1602.11", + "@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==", @@ -1377,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", @@ -1433,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", @@ -1534,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", @@ -1560,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": { @@ -1686,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": { @@ -1701,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" @@ -1836,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", @@ -1949,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": { @@ -1963,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" @@ -2042,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", @@ -2082,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", @@ -2092,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" @@ -2140,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", @@ -2176,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" @@ -2212,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" @@ -2253,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" } @@ -2278,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": { @@ -2329,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" } @@ -2367,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" @@ -2484,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" }, @@ -2495,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" @@ -2510,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" @@ -2527,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" @@ -2546,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", @@ -2564,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" @@ -2579,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", @@ -2607,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", @@ -2695,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" @@ -2710,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" @@ -2725,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" @@ -2764,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" @@ -2881,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" @@ -2912,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" @@ -2927,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" }, @@ -2962,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" @@ -2977,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" @@ -2992,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" @@ -3008,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": { @@ -3025,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" }, @@ -3047,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" @@ -3063,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" @@ -3092,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" @@ -3108,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" @@ -3123,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": { @@ -3139,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" @@ -3155,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": { @@ -3171,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" @@ -3187,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": { @@ -3203,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" @@ -3220,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": { @@ -3236,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" @@ -3251,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": { @@ -3267,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" @@ -3282,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" @@ -3298,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": { @@ -3314,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": { @@ -3332,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" @@ -3364,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" @@ -3379,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": { @@ -3395,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": { @@ -3411,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" @@ -3430,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" @@ -3446,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": { @@ -3462,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" }, @@ -3479,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" @@ -3494,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" @@ -3510,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": { @@ -3528,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" @@ -3543,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": { @@ -3559,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" @@ -3603,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" @@ -3618,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": { @@ -3634,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" @@ -3649,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" @@ -3664,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" @@ -3679,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" @@ -3697,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" @@ -3712,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" @@ -3728,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" @@ -3744,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" @@ -3863,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" @@ -3880,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", @@ -3892,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" @@ -3915,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": { @@ -4084,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": { @@ -4104,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": { @@ -4118,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", @@ -4356,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": { @@ -4373,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", @@ -4404,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" }, @@ -4505,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": { @@ -4612,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", @@ -4654,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", @@ -4726,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", @@ -4754,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", @@ -4842,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", @@ -4871,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", @@ -4916,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" @@ -4983,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", @@ -5023,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", @@ -5063,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", @@ -5078,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", @@ -5455,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" @@ -5508,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" @@ -5541,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", @@ -5569,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" @@ -5591,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", @@ -5623,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", @@ -5646,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" } }, @@ -5736,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", @@ -5820,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" @@ -5929,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": { @@ -5947,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" }, @@ -5994,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", @@ -6088,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", @@ -6112,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", @@ -6133,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" }, @@ -6161,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" }, @@ -6177,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": { @@ -6193,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", @@ -6225,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": "*", @@ -6242,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": { @@ -6286,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", @@ -6334,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", @@ -6362,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": { @@ -6447,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", @@ -6478,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", @@ -6530,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", @@ -6538,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", @@ -6618,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": { @@ -6702,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", @@ -6722,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", @@ -7507,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" @@ -7563,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", @@ -7611,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", @@ -7628,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": { @@ -7651,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": { @@ -7681,18 +7855,18 @@ } }, "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" @@ -7730,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", @@ -7848,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", @@ -7998,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", @@ -8175,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", @@ -8348,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": { @@ -8430,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", @@ -8555,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", @@ -8730,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", @@ -8799,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==", @@ -8817,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", @@ -8987,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": { @@ -9004,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", @@ -9035,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" }, @@ -9120,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": { @@ -9189,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", @@ -9219,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" }, @@ -9259,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": { @@ -9276,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", @@ -9307,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" }, @@ -9383,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" @@ -9506,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", @@ -9621,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==", @@ -9665,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", @@ -9805,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", @@ -9943,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" @@ -10048,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", @@ -10161,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", @@ -10359,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", @@ -10389,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", @@ -10421,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", @@ -10490,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", @@ -10518,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", @@ -10529,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": { @@ -10597,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", @@ -10638,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": { @@ -10677,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", @@ -10716,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" }, @@ -10737,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" @@ -10754,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": { @@ -10771,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" @@ -10791,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" ], @@ -10807,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" ], @@ -10822,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" ], @@ -10839,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" ], @@ -10855,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" ], @@ -10871,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" ], @@ -10887,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" ], @@ -10903,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" ], @@ -10919,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" ], @@ -10935,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", @@ -10959,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", @@ -10977,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" }, @@ -11031,6 +9787,7 @@ }, "peerDependencies": { "@jest/globals": ">= 28", + "@types/bun": "latest", "@types/jest": ">= 28", "jest": ">= 28", "vitest": ">= 0.32" @@ -11039,6 +9796,9 @@ "@jest/globals": { "optional": true }, + "@types/bun": { + "optional": true + }, "@types/jest": { "optional": true }, @@ -11063,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", @@ -11117,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", @@ -11178,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": "*" @@ -11224,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", @@ -11237,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", @@ -11256,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": "*", @@ -11306,9 +10039,9 @@ } }, "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": "*" @@ -11325,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": "*", @@ -11352,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": { @@ -11385,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": "*", @@ -11401,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": "*", @@ -11411,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": { @@ -11435,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": "*", @@ -11447,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": { @@ -11483,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": { @@ -11504,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": "*" @@ -11541,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": "*" @@ -11574,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", @@ -11595,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": { @@ -11607,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": { @@ -11665,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": "*" @@ -11683,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": { @@ -11704,36 +10469,36 @@ "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": { @@ -11788,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": { @@ -11803,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": { @@ -11819,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": { @@ -11848,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": { @@ -11871,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": "*" @@ -11895,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", @@ -11937,9 +10695,9 @@ } }, "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": { @@ -11952,48 +10710,48 @@ } }, "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 }, @@ -12008,9 +10766,9 @@ } }, "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": { @@ -12023,24 +10781,24 @@ } }, "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": { @@ -12088,23 +10846,6 @@ } } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { - "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": "7.4.0", - "@typescript-eslint/visitor-keys": "7.4.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": { "version": "7.4.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.4.0.tgz", @@ -12132,47 +10873,6 @@ } } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": { - "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": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": { - "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": "7.4.0", - "@typescript-eslint/visitor-keys": "7.4.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": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { "version": "7.4.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.4.0.tgz", @@ -12198,54 +10898,13 @@ "eslint": "^8.56.0" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { - "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": "7.4.0", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^18.18.0 || >=20.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" - } - }, "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" @@ -12258,54 +10917,6 @@ "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": "7.4.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.4.0.tgz", @@ -12334,7 +10945,7 @@ } } }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { + "node_modules/@typescript-eslint/scope-manager": { "version": "7.4.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.4.0.tgz", "integrity": "sha512-68VqENG5HK27ypafqLVs8qO+RkNc7TezCduYrx8YJpXq2QGZ30vmNZGJJJC48+MVn4G2dCV8m5ZTVnzRexTVtw==", @@ -12351,105 +10962,6 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { - "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": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { - "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": "7.4.0", - "@typescript-eslint/visitor-keys": "7.4.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": "^18.18.0 || >=20.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": "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": "7.4.0", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^18.18.0 || >=20.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==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "5.59.7", - "@typescript-eslint/visitor-keys": "5.59.7" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, "node_modules/@typescript-eslint/type-utils": { "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", @@ -12535,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", @@ -12548,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", @@ -12697,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", @@ -12732,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", @@ -12754,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": { @@ -12777,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": { @@ -12813,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", @@ -12842,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", @@ -12868,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" } }, @@ -12959,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", @@ -13019,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", @@ -13191,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", @@ -13213,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", @@ -13263,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", @@ -13349,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": { @@ -13392,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" } @@ -13428,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", @@ -13508,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": { @@ -13579,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", @@ -13801,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": { @@ -13916,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", @@ -13937,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": { @@ -13970,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", @@ -14018,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" @@ -14030,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": { @@ -14090,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" @@ -14129,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": { @@ -14239,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" @@ -14294,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": { @@ -14346,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": { @@ -14390,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": { @@ -14521,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" }, @@ -14533,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" } @@ -14571,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" @@ -14739,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": { @@ -14755,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": { @@ -14818,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", @@ -14843,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": { @@ -14894,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", @@ -15014,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": { @@ -15077,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", @@ -15106,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", @@ -15120,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" }, @@ -15147,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", @@ -15259,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": { @@ -15317,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", @@ -15368,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", @@ -15488,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": { @@ -15538,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", @@ -15556,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": { @@ -15645,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" @@ -15708,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", @@ -15773,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", @@ -15806,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" @@ -15865,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", @@ -16068,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", @@ -16081,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", @@ -16114,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" @@ -16224,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" }, @@ -16235,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" @@ -16288,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", @@ -16369,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", @@ -16405,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 }, @@ -16527,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", @@ -16548,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", @@ -16627,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", @@ -16669,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", @@ -16737,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", @@ -16844,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", @@ -16900,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" @@ -16986,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", @@ -17053,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", @@ -17066,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", @@ -17084,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": { @@ -17161,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": { @@ -17276,38 +15752,6 @@ } } }, - "node_modules/css-loader/node_modules/postcss-modules-local-by-default": { - "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", - "postcss-selector-parser": "^6.0.2", - "postcss-value-parser": "^4.1.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/css-loader/node_modules/postcss-modules-scope": { - "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" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, "node_modules/css-select": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", @@ -17355,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": { @@ -17372,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", @@ -17405,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": { @@ -17536,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", @@ -17693,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": { @@ -17714,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" }, @@ -17743,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": { @@ -17770,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", @@ -17790,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", @@ -17863,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" } @@ -17930,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" @@ -17949,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", @@ -17990,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": { @@ -18066,9 +16592,9 @@ "optional": 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" @@ -18131,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": { @@ -18203,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": { @@ -18234,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", @@ -18255,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", @@ -18322,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", @@ -18432,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": { @@ -18469,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": { @@ -18505,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", @@ -18531,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", @@ -18606,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", @@ -18639,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" @@ -18679,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" @@ -18731,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", @@ -18752,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": { @@ -18798,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": { @@ -18830,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", @@ -18874,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": { @@ -18963,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" } @@ -18988,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": { @@ -19197,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" @@ -19253,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", @@ -19274,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", @@ -19414,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", @@ -19459,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", @@ -19471,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", @@ -19534,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", @@ -19607,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": { @@ -19713,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": { @@ -19818,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", @@ -19902,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": { @@ -19965,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", @@ -19974,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", @@ -20145,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", @@ -20182,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" @@ -20309,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", @@ -20708,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", @@ -20740,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", @@ -20756,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" @@ -20780,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": [ { @@ -20847,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" @@ -20917,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", @@ -20941,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", @@ -21020,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", @@ -21027,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", @@ -21037,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", @@ -21074,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", @@ -21085,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": { @@ -21096,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, @@ -21171,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", @@ -21199,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" } @@ -21222,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" @@ -21264,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" @@ -21280,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" } @@ -21298,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", @@ -21341,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" @@ -21392,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", @@ -21434,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", @@ -21652,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", @@ -21697,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", @@ -21711,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", @@ -21724,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", @@ -22152,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" @@ -22275,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", @@ -22312,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", @@ -22344,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", @@ -22415,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", @@ -22436,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", @@ -22490,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", @@ -22546,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" } @@ -22575,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" }, @@ -22608,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" @@ -22696,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" }, @@ -22792,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": [ { @@ -22976,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", @@ -23005,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", @@ -23018,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": { @@ -23118,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", @@ -23188,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": [ { @@ -23211,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" @@ -23223,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": { @@ -23301,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" @@ -23321,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", @@ -23364,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": { @@ -23502,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" } @@ -23562,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": { @@ -23638,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", @@ -23670,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": { @@ -23707,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" @@ -23826,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": { @@ -23868,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": { @@ -24038,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" } @@ -24072,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" @@ -24205,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" @@ -24268,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" @@ -24321,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" } @@ -24342,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" @@ -24386,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/" @@ -24412,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" @@ -24437,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": { @@ -24486,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": { @@ -24534,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", @@ -24551,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" @@ -24579,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": { @@ -24593,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" @@ -24624,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": { @@ -24660,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" }, @@ -24705,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", @@ -24726,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": { @@ -24755,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": { @@ -24809,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", @@ -24841,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": { @@ -24876,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", @@ -24921,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", @@ -24982,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": { @@ -25007,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" }, @@ -25095,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" }, @@ -25144,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", @@ -25171,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", @@ -25217,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" @@ -25248,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" }, @@ -25316,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" }, @@ -25349,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", @@ -25370,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": { @@ -25381,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", @@ -25423,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", @@ -25453,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": { @@ -25474,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" }, @@ -25513,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" } @@ -25531,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": { @@ -25559,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", @@ -25629,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", @@ -25668,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", @@ -25768,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" @@ -25791,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", @@ -25885,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", @@ -25973,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", @@ -26016,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" @@ -26136,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", @@ -26199,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", @@ -26307,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": { @@ -26405,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": { @@ -26514,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", @@ -26536,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" @@ -26601,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", @@ -26673,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", @@ -26708,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", @@ -26974,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" }, @@ -27081,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", @@ -27112,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", @@ -27142,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" @@ -27276,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" } @@ -27316,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", @@ -27404,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", @@ -27555,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" @@ -27820,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", @@ -28008,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" @@ -28034,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", @@ -28076,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": [ { @@ -28111,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": [ { @@ -28165,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", @@ -28181,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", @@ -28201,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", @@ -28219,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", @@ -28249,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", @@ -28266,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": [ { @@ -28287,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": [ { @@ -28309,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": [ { @@ -28329,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": [ { @@ -28347,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": [ { @@ -28374,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": [ { @@ -28394,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": [ { @@ -28413,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": [ { @@ -28434,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": [ { @@ -28454,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": [ { @@ -28473,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": [ { @@ -28495,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": [ { @@ -28511,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": [ { @@ -28527,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": [ { @@ -28546,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": [ { @@ -28565,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": [ { @@ -28586,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": [ { @@ -28608,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": [ { @@ -28624,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": [ { @@ -28736,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": { @@ -28755,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": { @@ -29004,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": "*" @@ -29148,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", @@ -29196,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", @@ -29236,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 }, @@ -29304,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" }, @@ -29321,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", @@ -29409,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" @@ -29427,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", @@ -29452,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", @@ -29472,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": { @@ -29505,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", @@ -29529,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", @@ -29551,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", @@ -29590,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", @@ -29620,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", @@ -29885,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", @@ -29933,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", @@ -29959,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", @@ -30008,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", @@ -30042,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": { @@ -30125,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" @@ -30170,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" }, @@ -30203,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" @@ -30220,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": { @@ -30270,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" @@ -30292,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", @@ -30432,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", @@ -30532,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": { @@ -30600,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", @@ -30789,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", @@ -30822,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", @@ -30856,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", @@ -30945,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", @@ -30977,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": { @@ -31006,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", @@ -31031,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", @@ -31105,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" @@ -31230,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", @@ -31398,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": { @@ -31441,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" @@ -31471,6 +31049,15 @@ "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.38", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", @@ -31536,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" @@ -31564,15 +31157,6 @@ } } }, - "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": "8.1.1", "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.1.1.tgz", @@ -31661,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", @@ -31678,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" @@ -31727,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", @@ -31892,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": { @@ -32138,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", @@ -32195,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", @@ -32235,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": [ { @@ -32265,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" @@ -32321,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", @@ -32383,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", @@ -32404,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", @@ -32496,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": { @@ -32527,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", @@ -32688,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", @@ -32728,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", @@ -32855,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" @@ -32873,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", @@ -32901,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", @@ -32937,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", @@ -32999,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": { @@ -33057,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" @@ -33097,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" @@ -33257,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", @@ -33392,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": { @@ -33434,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", @@ -33541,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", @@ -33657,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": { @@ -33680,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", @@ -33754,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 }, @@ -33850,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", @@ -33870,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", @@ -33883,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" }, @@ -33915,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" } @@ -33997,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": { @@ -34041,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", @@ -34125,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" @@ -34163,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", @@ -34328,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", @@ -34339,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" @@ -34445,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", @@ -34480,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" @@ -34516,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", @@ -34564,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", @@ -34581,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": { @@ -34795,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": { @@ -34898,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" } }, @@ -35048,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": { @@ -35064,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": { @@ -35099,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", @@ -35144,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", @@ -35203,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", @@ -35329,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": { @@ -35559,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": { @@ -35606,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": { @@ -35630,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", @@ -35653,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", @@ -35662,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", @@ -35731,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" @@ -35748,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" @@ -35859,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", @@ -35877,7 +35227,7 @@ "sucrase-node": "bin/sucrase-node" }, "engines": { - "node": ">=8" + "node": ">=16 || 14 >=14.17" } }, "node_modules/sucrase/node_modules/commander": { @@ -35889,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", @@ -35961,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" @@ -36050,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", @@ -36060,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", @@ -36109,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" } @@ -36199,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", @@ -36219,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", @@ -36275,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": { @@ -36319,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" @@ -36404,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", @@ -36437,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" }, @@ -36474,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", @@ -36494,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", @@ -36558,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", @@ -36590,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": { @@ -36637,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": { @@ -36691,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", @@ -36779,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", @@ -36837,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" } @@ -36855,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", @@ -36874,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" @@ -37118,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", @@ -37166,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", @@ -37183,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": { @@ -37205,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": { @@ -37256,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" @@ -37288,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" @@ -37307,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" @@ -37344,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", @@ -37539,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": { @@ -37664,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" } @@ -37686,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": { @@ -37856,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" @@ -37963,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" } @@ -37989,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", @@ -38008,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", @@ -38088,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", @@ -38126,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": { @@ -38170,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", @@ -38180,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", @@ -38198,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", @@ -38210,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", @@ -38299,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": { @@ -38339,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", @@ -38585,12 +38185,6 @@ } } }, - "node_modules/webpack-dev-server/node_modules/@types/retry": { - "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/webpack-dev-server/node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -38652,9 +38246,9 @@ } }, "node_modules/webpack-dev-server/node_modules/memfs": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.8.0.tgz", - "integrity": "sha512-fcs7trFxZlOMadmTw5nyfOwS3il9pr3y+6xzLfXNwmuR/D0i4wz6rJURxArAbcJDGalbpbMvQ/IFI0NojRZgRg==", + "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" @@ -38685,36 +38279,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/webpack-dev-server/node_modules/p-retry": { - "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.2", - "is-network-error": "^1.0.0", - "retry": "^0.13.1" - }, - "engines": { - "node": ">=16.17" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/webpack-dev-server/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/webpack-dev-server/node_modules/webpack-dev-middleware": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.1.1.tgz", - "integrity": "sha512-NmRVq4AvRQs66dFWyDR4GsFDJggtSi2Yn38MXLk0nffgF9n/AIP4TFBg2TQKYaRAN4sHuKOTiz9BnNCENDLEVA==", + "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", @@ -38740,31 +38308,10 @@ } } }, - "node_modules/webpack-dev-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/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", @@ -38925,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": { @@ -38988,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" @@ -39009,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" @@ -39103,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", @@ -39119,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": { @@ -39172,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": { @@ -39198,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": { @@ -39252,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", @@ -39369,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", @@ -39390,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": { From 2977616be4b6e849386671c2d166c039403d09ea Mon Sep 17 00:00:00 2001 From: Matt Gibson <mgibson@bitwarden.com> Date: Wed, 3 Apr 2024 09:28:36 -0500 Subject: [PATCH 100/351] Use user definition clearon events for billing (#8589) --- libs/common/src/billing/models/billing-keys.state.ts | 5 +++-- .../account/billing-account-profile-state.service.ts | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) 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<PaymentMethodWarning>( +export const PAYMENT_METHOD_WARNINGS_KEY = UserKeyDefinition.record<PaymentMethodWarning>( BILLING_DISK, "paymentMethodWarnings", { @@ -9,5 +9,6 @@ export const PAYMENT_METHOD_WARNINGS_KEY = KeyDefinition.record<PaymentMethodWar ...warnings, savedAt: new Date(warnings.savedAt), }), + clearOn: ["logout"], }, ); 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 336021c993..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 @@ -3,19 +3,20 @@ import { map, Observable, of, switchMap } from "rxjs"; import { ActiveUserState, 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<BillingAccountProfile>( +export const BILLING_ACCOUNT_PROFILE_KEY_DEFINITION = new UserKeyDefinition<BillingAccountProfile>( BILLING_DISK, "accountProfile", { deserializer: (billingAccountProfile) => billingAccountProfile, + clearOn: ["logout"], }, ); From ff3ff89e20ff50bc5b2452e0d1e6f8773e732b47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Wed, 3 Apr 2024 15:51:21 +0100 Subject: [PATCH 101/351] [AC-1913] Update OrganizationLayoutComponent to use PolicyService to check if the user has the SingleOrg policy and use the value to hide the 'New Org' button (#8349) --- .../layouts/organization-layout.component.html | 2 +- .../organizations/layouts/organization-layout.component.ts | 6 ++++++ .../src/app/layouts/org-switcher/org-switcher.component.ts | 1 - 3 files changed, 7 insertions(+), 2 deletions(-) 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..b9a277a2e9 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 @@ <a routerLink="." class="tw-m-5 tw-mt-7 tw-block" [appA11yTitle]="'adminConsole' | i18n"> <bit-icon [icon]="logo"></bit-icon> </a> - <org-switcher [filter]="orgFilter"></org-switcher> + <org-switcher [filter]="orgFilter" [hideNewButton]="hideNewOrgButton$ | async"></org-switcher> <bit-nav-item icon="bwi-collection" 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 1924476327..680a9155e1 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,6 +15,8 @@ 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 { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -48,6 +50,7 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy { organization$: Observable<Organization>; showPaymentAndHistory$: Observable<boolean>; + hideNewOrgButton$: Observable<boolean>; private _destroy = new Subject<void>(); @@ -61,6 +64,7 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy { private organizationService: OrganizationService, private platformUtilsService: PlatformUtilsService, private configService: ConfigService, + private policyService: PolicyService, ) {} async ngOnInit() { @@ -85,6 +89,8 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy { org?.canEditPaymentMethods, ), ); + + this.hideNewOrgButton$ = this.policyService.policyAppliesToActiveUser$(PolicyType.SingleOrg); } ngOnDestroy() { 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; From b579bc8f96e18a242884621103519afdb3bcad0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=A8=20Audrey=20=E2=9C=A8?= <ajensen@bitwarden.com> Date: Wed, 3 Apr 2024 13:48:33 -0400 Subject: [PATCH 102/351] [PM-6818] legacy generator service adapter (#8582) * introduce legacy generators * introduce generator navigation service * Introduce default options. These accept a userId so that they can be policy-defined * replace `GeneratorOptions` with backwards compatible `GeneratorNavigation` --- .../components/generator.component.ts | 11 +- ...enerator-navigation.service.abstraction.ts | 42 + .../generator-strategy.abstraction.ts | 3 + .../generator.service.abstraction.ts | 3 + .../src/tools/generator/abstractions/index.ts | 1 + ...password-generation.service.abstraction.ts | 11 +- ...username-generation.service.abstraction.ts | 3 +- .../default-generator.service.spec.ts | 16 + .../generator/default-generator.service.ts | 11 +- .../src/tools/generator/generator-options.ts | 8 +- .../src/tools/generator/generator-type.ts | 2 + .../tools/generator/key-definition.spec.ts | 15 +- .../src/tools/generator/key-definitions.ts | 22 +- ...legacy-password-generation.service.spec.ts | 470 +++++++++++ .../legacy-password-generation.service.ts | 184 +++++ ...legacy-username-generation.service.spec.ts | 748 ++++++++++++++++++ .../legacy-username-generation.service.ts | 383 +++++++++ ...fault-generator-nativation.service.spec.ts | 100 +++ .../default-generator-navigation.service.ts | 71 ++ .../generator-navigation-evaluator.spec.ts | 64 ++ .../generator-navigation-evaluator.ts | 43 + .../generator-navigation-policy.spec.ts | 63 ++ .../navigation/generator-navigation-policy.ts | 39 + .../navigation/generator-navigation.ts | 26 + .../src/tools/generator/navigation/index.ts | 3 + .../src/tools/generator/passphrase/index.ts | 5 +- .../passphrase-generator-strategy.spec.ts | 19 +- .../passphrase-generator-strategy.ts | 14 +- .../src/tools/generator/password/index.ts | 2 +- .../password/password-generation.service.ts | 20 +- .../password/password-generator-options.ts | 12 +- .../password-generator-strategy.spec.ts | 11 + .../password/password-generator-strategy.ts | 14 +- .../username/catchall-generator-options.ts | 23 +- .../catchall-generator-strategy.spec.ts | 24 +- .../username/catchall-generator-strategy.ts | 16 +- .../eff-username-generator-options.ts | 12 +- .../eff-username-generator-strategy.spec.ts | 13 + .../eff-username-generator-strategy.ts | 14 +- .../forwarder-generator-strategy.spec.ts | 5 + .../username/forwarder-generator-strategy.ts | 5 +- .../username/forwarders/addy-io.spec.ts | 17 +- .../generator/username/forwarders/addy-io.ts | 22 + .../username/forwarders/duck-duck-go.spec.ts | 17 +- .../username/forwarders/duck-duck-go.ts | 18 + .../username/forwarders/fastmail.spec.ts | 17 +- .../generator/username/forwarders/fastmail.ts | 22 + .../username/forwarders/firefox-relay.spec.ts | 17 +- .../username/forwarders/firefox-relay.ts | 18 + .../username/forwarders/forward-email.spec.ts | 17 +- .../username/forwarders/forward-email.ts | 20 + .../username/forwarders/simple-login.spec.ts | 17 +- .../username/forwarders/simple-login.ts | 20 + .../src/tools/generator/username/index.ts | 2 +- .../generator/username/options/constants.ts | 69 -- .../username/options/generator-options.ts | 107 +-- .../tools/generator/username/options/index.ts | 2 - .../username/options/utilities.spec.ts | 243 ------ .../generator/username/options/utilities.ts | 72 -- .../username/subaddress-generator-options.ts | 18 +- .../subaddress-generator-strategy.spec.ts | 27 +- .../username/subaddress-generator-strategy.ts | 25 +- .../username/username-generation-options.ts | 40 +- .../username/username-generation.service.ts | 3 +- 64 files changed, 2759 insertions(+), 622 deletions(-) create mode 100644 libs/common/src/tools/generator/abstractions/generator-navigation.service.abstraction.ts rename libs/common/src/tools/generator/{password => abstractions}/password-generation.service.abstraction.ts (69%) rename libs/common/src/tools/generator/{username => abstractions}/username-generation.service.abstraction.ts (75%) create mode 100644 libs/common/src/tools/generator/generator-type.ts create mode 100644 libs/common/src/tools/generator/legacy-password-generation.service.spec.ts create mode 100644 libs/common/src/tools/generator/legacy-password-generation.service.ts create mode 100644 libs/common/src/tools/generator/legacy-username-generation.service.spec.ts create mode 100644 libs/common/src/tools/generator/legacy-username-generation.service.ts create mode 100644 libs/common/src/tools/generator/navigation/default-generator-nativation.service.spec.ts create mode 100644 libs/common/src/tools/generator/navigation/default-generator-navigation.service.ts create mode 100644 libs/common/src/tools/generator/navigation/generator-navigation-evaluator.spec.ts create mode 100644 libs/common/src/tools/generator/navigation/generator-navigation-evaluator.ts create mode 100644 libs/common/src/tools/generator/navigation/generator-navigation-policy.spec.ts create mode 100644 libs/common/src/tools/generator/navigation/generator-navigation-policy.ts create mode 100644 libs/common/src/tools/generator/navigation/generator-navigation.ts create mode 100644 libs/common/src/tools/generator/navigation/index.ts delete mode 100644 libs/common/src/tools/generator/username/options/utilities.spec.ts delete mode 100644 libs/common/src/tools/generator/username/options/utilities.ts 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/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<GeneratorNavigation>; + + /** Gets the default options. */ + defaults$: (userId: UserId) => Observable<GeneratorNavigation>; + + /** 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<PolicyEvaluator<GeneratorNavigationPolicy, GeneratorNavigation>>; + + /** 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<GeneratorNavigation>; + + /** 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<void>; +} 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 eda02f7cdc..7cfe320abe 100644 --- a/libs/common/src/tools/generator/abstractions/generator-strategy.abstraction.ts +++ b/libs/common/src/tools/generator/abstractions/generator-strategy.abstraction.ts @@ -17,6 +17,9 @@ export abstract class GeneratorStrategy<Options, Policy> { */ durableState: (userId: UserId) => SingleUserState<Options>; + /** Gets the default options. */ + defaults$: (userId: UserId) => Observable<Options>; + /** Identifies the policy enforced by the generator. */ policy: PolicyType; 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<Options, Policy> { */ evaluator$: (userId: UserId) => Observable<PolicyEvaluator<Policy, Options>>; + /** Gets the default options. */ + defaults$: (userId: UserId) => Observable<Options>; + /** 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<string>; generatePassphrase: (options: PasswordGeneratorOptions) => Promise<string>; @@ -10,13 +10,8 @@ export abstract class PasswordGenerationServiceAbstraction { enforcePasswordGeneratorPoliciesOnOptions: ( options: PasswordGeneratorOptions, ) => Promise<[PasswordGeneratorOptions, PasswordGeneratorPolicyOptions]>; - getPasswordGeneratorPolicyOptions: () => Promise<PasswordGeneratorPolicyOptions>; saveOptions: (options: PasswordGeneratorOptions) => Promise<void>; getHistory: () => Promise<GeneratedPasswordHistory[]>; addHistory: (password: string) => Promise<void>; clear: (userId?: string) => Promise<void>; - 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<string>; generateWord: (options: UsernameGeneratorOptions) => Promise<string>; 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 53a46c4963..c93aec44d9 100644 --- a/libs/common/src/tools/generator/default-generator.service.spec.ts +++ b/libs/common/src/tools/generator/default-generator.service.spec.ts @@ -37,6 +37,7 @@ function mockGeneratorStrategy(config?: { userState?: SingleUserState<any>; policy?: PolicyType; evaluator?: any; + defaults?: any; }) { const durableState = config?.userState ?? new FakeSingleUserState<PasswordGenerationOptions>(SomeUser); @@ -45,6 +46,7 @@ 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, toEvaluator: jest.fn(() => pipe(map(() => config?.evaluator ?? mock<PolicyEvaluator<any, any>>())), @@ -72,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(); diff --git a/libs/common/src/tools/generator/default-generator.service.ts b/libs/common/src/tools/generator/default-generator.service.ts index 34aacee695..7fd794472c 100644 --- a/libs/common/src/tools/generator/default-generator.service.ts +++ b/libs/common/src/tools/generator/default-generator.service.ts @@ -21,17 +21,22 @@ export class DefaultGeneratorService<Options, Policy> implements GeneratorServic private _evaluators$ = new Map<UserId, Observable<PolicyEvaluator<Policy, Options>>>(); - /** {@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<void> { await this.strategy.durableState(userId).update(() => options); } - /** {@link GeneratorService.evaluator$()} */ + /** {@link GeneratorService.evaluator$} */ evaluator$(userId: UserId) { let evaluator$ = this._evaluators$.get(userId); @@ -59,7 +64,7 @@ export class DefaultGeneratorService<Options, Policy> implements GeneratorServic return evaluator$; } - /** {@link GeneratorService.enforcePolicy()} */ + /** {@link GeneratorService.enforcePolicy} */ async enforcePolicy(userId: UserId, options: Options): Promise<Options> { 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/key-definition.spec.ts b/libs/common/src/tools/generator/key-definition.spec.ts index f21767e77e..9cbbc44e14 100644 --- a/libs/common/src/tools/generator/key-definition.spec.ts +++ b/libs/common/src/tools/generator/key-definition.spec.ts @@ -10,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 = {}; @@ -31,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); }); @@ -39,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); }); @@ -47,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); }); diff --git a/libs/common/src/tools/generator/key-definitions.ts b/libs/common/src/tools/generator/key-definitions.ts index d51af70f2e..2f35169612 100644 --- a/libs/common/src/tools/generator/key-definitions.ts +++ b/libs/common/src/tools/generator/key-definitions.ts @@ -1,6 +1,7 @@ -import { GENERATOR_DISK, KeyDefinition } from "../../platform/state"; +import { GENERATOR_DISK, GENERATOR_MEMORY, KeyDefinition } from "../../platform/state"; import { GeneratedCredential } from "./history/generated-credential"; +import { GeneratorNavigation } from "./navigation/generator-navigation"; import { PassphraseGenerationOptions } from "./passphrase/passphrase-generation-options"; import { PasswordGenerationOptions } from "./password/password-generation-options"; import { SecretClassifier } from "./state/secret-classifier"; @@ -15,6 +16,15 @@ import { } from "./username/options/forwarder-options"; import { SubaddressGenerationOptions } from "./username/subaddress-generator-options"; +/** plaintext password generation options */ +export const GENERATOR_SETTINGS = new KeyDefinition<GeneratorNavigation>( + GENERATOR_MEMORY, + "generatorSettings", + { + deserializer: (value) => value, + }, +); + /** plaintext password generation options */ export const PASSWORD_SETTINGS = new KeyDefinition<PasswordGenerationOptions>( GENERATOR_DISK, @@ -42,7 +52,7 @@ export const EFF_USERNAME_SETTINGS = new KeyDefinition<EffUsernameGenerationOpti }, ); -/** catchall email generation options */ +/** plaintext configuration for a domain catch-all address. */ export const CATCHALL_SETTINGS = new KeyDefinition<CatchallGenerationOptions>( GENERATOR_DISK, "catchallGeneratorSettings", @@ -51,7 +61,7 @@ export const CATCHALL_SETTINGS = new KeyDefinition<CatchallGenerationOptions>( }, ); -/** email subaddress generation options */ +/** plaintext configuration for an email subaddress. */ export const SUBADDRESS_SETTINGS = new KeyDefinition<SubaddressGenerationOptions>( GENERATOR_DISK, "subaddressGeneratorSettings", @@ -60,6 +70,7 @@ export const SUBADDRESS_SETTINGS = new KeyDefinition<SubaddressGenerationOptions }, ); +/** backing store configuration for {@link Forwarders.AddyIo} */ export const ADDY_IO_FORWARDER = new KeyDefinition<SelfHostedApiOptions & EmailDomainOptions>( GENERATOR_DISK, "addyIoForwarder", @@ -68,6 +79,7 @@ export const ADDY_IO_FORWARDER = new KeyDefinition<SelfHostedApiOptions & EmailD }, ); +/** backing store configuration for {@link Forwarders.DuckDuckGo} */ export const DUCK_DUCK_GO_FORWARDER = new KeyDefinition<ApiOptions>( GENERATOR_DISK, "duckDuckGoForwarder", @@ -76,6 +88,7 @@ export const DUCK_DUCK_GO_FORWARDER = new KeyDefinition<ApiOptions>( }, ); +/** backing store configuration for {@link Forwarders.FastMail} */ export const FASTMAIL_FORWARDER = new KeyDefinition<ApiOptions & EmailPrefixOptions>( GENERATOR_DISK, "fastmailForwarder", @@ -84,6 +97,7 @@ export const FASTMAIL_FORWARDER = new KeyDefinition<ApiOptions & EmailPrefixOpti }, ); +/** backing store configuration for {@link Forwarders.FireFoxRelay} */ export const FIREFOX_RELAY_FORWARDER = new KeyDefinition<ApiOptions>( GENERATOR_DISK, "firefoxRelayForwarder", @@ -92,6 +106,7 @@ export const FIREFOX_RELAY_FORWARDER = new KeyDefinition<ApiOptions>( }, ); +/** backing store configuration for {@link Forwarders.ForwardEmail} */ export const FORWARD_EMAIL_FORWARDER = new KeyDefinition<ApiOptions & EmailDomainOptions>( GENERATOR_DISK, "forwardEmailForwarder", @@ -100,6 +115,7 @@ export const FORWARD_EMAIL_FORWARDER = new KeyDefinition<ApiOptions & EmailDomai }, ); +/** backing store configuration for {@link forwarders.SimpleLogin} */ export const SIMPLE_LOGIN_FORWARDER = new KeyDefinition<SelfHostedApiOptions>( GENERATOR_DISK, "simpleLoginForwarder", 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<GeneratorService<PassphraseGenerationOptions, PassphraseGeneratorPolicy>>({ + 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<GeneratorService<PasswordGenerationOptions, PasswordGeneratorPolicy>>({ + 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<GeneratorNavigationService>({ + 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<any[]>; + addHistory: (password: string) => Promise<void>; + clear: (userId?: string) => Promise<void>; +} 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: Options, defaults: Options) { + let savedOptions = options; + const generator = mock<GeneratorService<Options, NoPolicy>>({ + evaluator$(id: UserId) { + const evaluator = new DefaultPolicyEvaluator<Options>(); + 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<GeneratorNavigationService>({ + 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<CatchallGenerationOptions>(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<EffUsernameGenerationOptions>(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<SubaddressGenerationOptions>(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<SelfHostedApiOptions & EmailDomainOptions>(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<CatchallGenerationOptions>(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<SubaddressGenerationOptions>(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<SelfHostedApiOptions & EmailDomainOptions>(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<ApiOptions>(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<ApiOptions & EmailPrefixOptions>(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<ApiOptions>(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<ApiOptions & EmailDomainOptions>(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<SelfHostedApiOptions>(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<CatchallGenerationOptions>( + { + catchallDomain: "example.com", + catchallType: "random", + website: null, + }, + null, + ); + + const effUsername = createGenerator<EffUsernameGenerationOptions>( + { + wordCapitalize: true, + wordIncludeNumber: false, + website: null, + }, + null, + ); + + const subaddress = createGenerator<SubaddressGenerationOptions>( + { + subaddressType: "random", + subaddressEmail: "foo@example.com", + website: null, + }, + null, + ); + + const addyIo = createGenerator<SelfHostedApiOptions & EmailDomainOptions>( + { + token: "addyIoToken", + domain: "addyio.example.com", + baseUrl: "https://addyio.api.example.com", + website: null, + }, + null, + ); + + const duckDuckGo = createGenerator<ApiOptions>( + { + token: "ddgToken", + website: null, + }, + null, + ); + + const fastmail = createGenerator<ApiOptions & EmailPrefixOptions>( + { + token: "fastmailToken", + domain: "fastmail.example.com", + prefix: "foo", + website: null, + }, + null, + ); + + const firefoxRelay = createGenerator<ApiOptions>( + { + token: "firefoxToken", + website: null, + }, + null, + ); + + const forwardEmail = createGenerator<ApiOptions & EmailDomainOptions>( + { + token: "forwardEmailToken", + domain: "example.com", + website: null, + }, + null, + ); + + const simpleLogin = createGenerator<SelfHostedApiOptions>( + { + 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<CatchallGenerationOptions>(null, DefaultCatchallOptions); + const effUsername = createGenerator<EffUsernameGenerationOptions>( + null, + DefaultEffUsernameOptions, + ); + const subaddress = createGenerator<SubaddressGenerationOptions>( + null, + DefaultSubaddressOptions, + ); + const addyIo = createGenerator<SelfHostedApiOptions & EmailDomainOptions>( + null, + DefaultAddyIoOptions, + ); + const duckDuckGo = createGenerator<ApiOptions>(null, DefaultDuckDuckGoOptions); + const fastmail = createGenerator<ApiOptions & EmailPrefixOptions>( + null, + DefaultFastmailOptions, + ); + const firefoxRelay = createGenerator<ApiOptions>(null, DefaultFirefoxRelayOptions); + const forwardEmail = createGenerator<ApiOptions & EmailDomainOptions>( + null, + DefaultForwardEmailOptions, + ); + const simpleLogin = createGenerator<SelfHostedApiOptions>(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<CatchallGenerationOptions>(null, null); + const effUsername = createGenerator<EffUsernameGenerationOptions>(null, null); + const subaddress = createGenerator<SubaddressGenerationOptions>(null, null); + const addyIo = createGenerator<SelfHostedApiOptions & EmailDomainOptions>(null, null); + const duckDuckGo = createGenerator<ApiOptions>(null, null); + const fastmail = createGenerator<ApiOptions & EmailPrefixOptions>(null, null); + const firefoxRelay = createGenerator<ApiOptions>(null, null); + const forwardEmail = createGenerator<ApiOptions & EmailDomainOptions>(null, null); + const simpleLogin = createGenerator<SelfHostedApiOptions>(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<CatchallGenerationOptions, NoPolicy>, + private readonly effUsername: GeneratorService<EffUsernameGenerationOptions, NoPolicy>, + private readonly subaddress: GeneratorService<SubaddressGenerationOptions, NoPolicy>, + private readonly addyIo: GeneratorService<SelfHostedApiOptions & EmailDomainOptions, NoPolicy>, + private readonly duckDuckGo: GeneratorService<ApiOptions, NoPolicy>, + private readonly fastmail: GeneratorService<ApiOptions & EmailPrefixOptions, NoPolicy>, + private readonly firefoxRelay: GeneratorService<ApiOptions, NoPolicy>, + private readonly forwardEmail: GeneratorService<ApiOptions & EmailDomainOptions, NoPolicy>, + private readonly simpleLogin: GeneratorService<SelfHostedApiOptions, NoPolicy>, + ) {} + + 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<PolicyService>({ + 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<PolicyService>({ + 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<GeneratorNavigation> { + return this.stateProvider.getUserState$(GENERATOR_SETTINGS, userId); + } + + /** Gets the default options. */ + defaults$(userId: UserId): Observable<GeneratorNavigation> { + 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<void> { + 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<GeneratorNavigationPolicy, GeneratorNavigation> +{ + /** 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<GeneratorNavigation> = 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-strategy.spec.ts b/libs/common/src/tools/generator/passphrase/passphrase-generator-strategy.spec.ts index b7f09bd717..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,7 +2,6 @@ * include structuredClone in test environment. * @jest-environment ../../../../shared/test.environment.ts */ - import { mock } from "jest-mock-extended"; import { of, firstValueFrom } from "rxjs"; @@ -12,12 +11,16 @@ 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; @@ -71,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<PasswordGenerationServiceAbstraction>(); 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 f193b2b326..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,14 +1,17 @@ -import { map, pipe } from "rxjs"; +import { BehaviorSubject, map, pipe } from "rxjs"; import { GeneratorStrategy } from ".."; import { PolicyType } from "../../../admin-console/enums"; 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, @@ -36,6 +39,11 @@ 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; 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-strategy.spec.ts b/libs/common/src/tools/generator/password/password-generator-strategy.spec.ts index 9bfa5b5f35..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 @@ -17,6 +17,7 @@ import { PASSWORD_SETTINGS } from "../key-definitions"; import { DisabledPasswordGeneratorPolicy } from "./password-generator-policy"; import { + DefaultPasswordGenerationOptions, PasswordGenerationServiceAbstraction, PasswordGeneratorOptionsEvaluator, PasswordGeneratorStrategy, @@ -82,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<PasswordGenerationServiceAbstraction>(); 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 f8d618128b..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,14 +1,17 @@ -import { map, pipe } from "rxjs"; +import { BehaviorSubject, map, pipe } from "rxjs"; import { GeneratorStrategy } from ".."; import { PolicyType } from "../../../admin-console/enums"; 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, @@ -35,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; 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 339e4b2720..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 @@ -10,6 +10,8 @@ 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; @@ -47,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>(); @@ -70,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 6b36ebd50b..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,15 +1,15 @@ -import { map, pipe } from "rxjs"; +import { BehaviorSubject, map, pipe } from "rxjs"; import { PolicyType } from "../../../admin-console/enums"; 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; @@ -30,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 @@ -49,9 +54,6 @@ export class CatchallGeneratorStrategy /** {@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 821b4bb7dc..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 @@ -10,6 +10,8 @@ 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; @@ -47,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>(); @@ -72,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 133b4e7777..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,15 +1,18 @@ -import { map, pipe } from "rxjs"; +import { BehaviorSubject, map, pipe } from "rxjs"; import { PolicyType } from "../../../admin-console/enums"; 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; @@ -30,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 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 30dd620484..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 @@ -15,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> { @@ -30,6 +31,10 @@ class TestForwarder extends ForwarderGeneratorStrategy<ApiOptions> { // arbitrary. return DUCK_DUCK_GO_FORWARDER; } + + defaults$ = (userId: UserId) => { + return of(DefaultDuckDuckGoOptions); + }; } const SomeUser = "some user" as UserId; 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 8b78f22634..086e347669 100644 --- a/libs/common/src/tools/generator/username/forwarder-generator-strategy.ts +++ b/libs/common/src/tools/generator/username/forwarder-generator-strategy.ts @@ -1,4 +1,4 @@ -import { map, pipe } from "rxjs"; +import { Observable, map, pipe } from "rxjs"; import { PolicyType } from "../../../admin-console/enums"; import { CryptoService } from "../../../platform/abstractions/crypto.service"; @@ -79,6 +79,9 @@ export abstract class ForwarderGeneratorStrategy< return new UserKeyEncryptor(this.encryptService, this.keyService, packer); } + /** Gets the default options. */ + abstract defaults$: (userId: UserId) => Observable<Options>; + /** Determine where forwarder configuration is stored */ protected abstract readonly key: KeyDefinition<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 59a2b56172..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 @@ -10,6 +10,11 @@ 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; @@ -47,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>(); @@ -70,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 1ae0cb9142..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,19 +1,26 @@ -import { map, pipe } from "rxjs"; +import { BehaviorSubject, map, pipe } from "rxjs"; import { PolicyType } from "../../../admin-console/enums"; 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> { @@ -30,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 @@ -49,9 +61,6 @@ export class SubaddressGeneratorStrategy /** {@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", From daa9e742e7451ba6b3a2f2d9b2f4a2ea8a11f5b4 Mon Sep 17 00:00:00 2001 From: Cesar Gonzalez <cesar.a.gonzalezcs@gmail.com> Date: Wed, 3 Apr 2024 14:03:10 -0500 Subject: [PATCH 103/351] [PM-7247] Update AutofillService reference within Angular DI to remove the getBgService call (#8591) * [PM-7247] Update AutofillService dependency reference within Angular to remove getBgService call * [PM-7247] Update AutofillService reference within Angular DI to remove the getBgService call --- .../autofill-service.factory.ts | 6 ------ .../services/autofill.service.spec.ts | 3 --- .../src/autofill/services/autofill.service.ts | 2 -- .../browser/src/background/main.background.ts | 1 - .../src/popup/services/services.module.ts | 21 ++++++++++++++++--- 5 files changed, 18 insertions(+), 15 deletions(-) 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/services/autofill.service.spec.ts b/apps/browser/src/autofill/services/autofill.service.spec.ts index f6c1fa9067..4db64f417d 100644 --- a/apps/browser/src/autofill/services/autofill.service.spec.ts +++ b/apps/browser/src/autofill/services/autofill.service.spec.ts @@ -32,7 +32,6 @@ import { CipherService } from "@bitwarden/common/vault/services/cipher.service"; import { TotpService } from "@bitwarden/common/vault/services/totp.service"; import { BrowserApi } from "../../platform/browser/browser-api"; -import { BrowserStateService } from "../../platform/services/browser-state.service"; import { AutofillPort } from "../enums/autofill-port.enums"; import AutofillField from "../models/autofill-field"; import AutofillPageDetails from "../models/autofill-page-details"; @@ -63,7 +62,6 @@ const mockEquivalentDomains = [ describe("AutofillService", () => { let autofillService: AutofillService; const cipherService = mock<CipherService>(); - const stateService = mock<BrowserStateService>(); const autofillSettingsService = mock<AutofillSettingsService>(); const mockUserId = Utils.newGuid() as UserId; const accountService: FakeAccountService = mockAccountServiceWith(mockUserId); @@ -78,7 +76,6 @@ describe("AutofillService", () => { beforeEach(() => { autofillService = new AutofillService( cipherService, - stateService, autofillSettingsService, totpService, eventCollectionService, diff --git a/apps/browser/src/autofill/services/autofill.service.ts b/apps/browser/src/autofill/services/autofill.service.ts index dae29e61e4..8b33d03419 100644 --- a/apps/browser/src/autofill/services/autofill.service.ts +++ b/apps/browser/src/autofill/services/autofill.service.ts @@ -20,7 +20,6 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FieldView } from "@bitwarden/common/vault/models/view/field.view"; import { BrowserApi } from "../../platform/browser/browser-api"; -import { BrowserStateService } from "../../platform/services/abstractions/browser-state.service"; import { openVaultItemPasswordRepromptPopout } from "../../vault/popup/utils/vault-popout-window"; import { AutofillPort } from "../enums/autofill-port.enums"; import AutofillField from "../models/autofill-field"; @@ -49,7 +48,6 @@ export default class AutofillService implements AutofillServiceInterface { constructor( private cipherService: CipherService, - private stateService: BrowserStateService, private autofillSettingsService: AutofillSettingsServiceAbstraction, private totpService: TotpService, private eventCollectionService: EventCollectionService, diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index ea43aecff9..255538de52 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -767,7 +767,6 @@ export default class MainBackground { this.autofillService = new AutofillService( this.cipherService, - this.stateService, this.autofillSettingsService, this.totpService, this.eventCollectionService, diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 6d0f73f206..e68873e31c 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -19,6 +19,7 @@ import { AuthRequestServiceAbstraction, LoginStrategyServiceAbstraction, } from "@bitwarden/auth/common"; +import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service"; import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service"; import { SearchService as SearchServiceAbstraction } from "@bitwarden/common/abstractions/search.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; @@ -47,6 +48,7 @@ import { UserNotificationSettingsService, UserNotificationSettingsServiceAbstraction, } from "@bitwarden/common/autofill/services/user-notification-settings.service"; +import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; @@ -85,7 +87,8 @@ import { DialogService } from "@bitwarden/components"; import { VaultExportServiceAbstraction } from "@bitwarden/vault-export-core"; import { UnauthGuardService } from "../../auth/popup/services"; -import { AutofillService } from "../../autofill/services/abstractions/autofill.service"; +import { AutofillService as AutofillServiceAbstraction } from "../../autofill/services/abstractions/autofill.service"; +import AutofillService from "../../autofill/services/autofill.service"; import MainBackground from "../../background/main.background"; import { Account } from "../../models/account"; import { BrowserApi } from "../../platform/browser/browser-api"; @@ -312,10 +315,22 @@ const safeProviders: SafeProvider[] = [ useClass: BrowserLocalStorageService, deps: [], }), + safeProvider({ + provide: AutofillServiceAbstraction, + useExisting: AutofillService, + }), safeProvider({ provide: AutofillService, - useFactory: getBgService<AutofillService>("autofillService"), - deps: [], + deps: [ + CipherService, + AutofillSettingsServiceAbstraction, + TotpService, + EventCollectionServiceAbstraction, + LogService, + DomainSettingsService, + UserVerificationService, + BillingAccountProfileStateService, + ], }), safeProvider({ provide: VaultExportServiceAbstraction, From 23c89bda74e365699e9ed2b37527d3c58a5c831c Mon Sep 17 00:00:00 2001 From: Oscar Hinton <Hinton@users.noreply.github.com> Date: Wed, 3 Apr 2024 22:51:55 +0200 Subject: [PATCH 104/351] [PM-6975] Replace purchasedPremium broadcast message with observables (#8421) In https://github.com/bitwarden/clients/pull/8133 the premium state changed to be derived from observables, which means we can get rid of the `purchasePremium` messages that are sent and instead rely directly on the observable to distribute the state. --- .../billing/individual/premium.component.ts | 5 +- .../app/layouts/user-layout.component.html | 4 +- .../src/app/layouts/user-layout.component.ts | 64 ++++++----------- .../src/app/settings/settings.component.ts | 68 ------------------- .../organization.service.abstraction.ts | 2 +- .../organization/organization.service.spec.ts | 6 +- .../organization/organization.service.ts | 12 ++-- 7 files changed, 33 insertions(+), 128 deletions(-) delete mode 100644 apps/web/src/app/settings/settings.component.ts 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/layouts/user-layout.component.html b/apps/web/src/app/layouts/user-layout.component.html index 397e95d485..c70b2f9ff7 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 @@ <bit-nav-item [text]="'subscription' | i18n" route="settings/subscription" - *ngIf="!hideSubscription" + *ngIf="showSubscription$ | async" ></bit-nav-item> <bit-nav-item [text]="'domainRules' | i18n" route="settings/domain-rules"></bit-nav-item> <bit-nav-item @@ -29,7 +29,7 @@ <bit-nav-item [text]="'sponsoredFamilies' | i18n" route="settings/sponsored-families" - *ngIf="hasFamilySponsorshipAvailable" + *ngIf="hasFamilySponsorshipAvailable$ | async" ></bit-nav-item> </bit-nav-group> </nav> diff --git a/apps/web/src/app/layouts/user-layout.component.ts b/apps/web/src/app/layouts/user-layout.component.ts index ee30bed0d6..1a225e49c7 100644 --- a/apps/web/src/app/layouts/user-layout.component.ts +++ b/apps/web/src/app/layouts/user-layout.component.ts @@ -1,14 +1,13 @@ import { CommonModule } from "@angular/common"; -import { Component, NgZone, OnDestroy, OnInit } from "@angular/core"; +import { Component, OnInit } from "@angular/core"; import { RouterModule } from "@angular/router"; -import { firstValueFrom } from "rxjs"; +import { Observable, combineLatest, concatMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; 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 { 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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; @@ -18,8 +17,6 @@ import { PaymentMethodWarningsModule } from "../billing/shared"; import { PasswordManagerLogo } from "./password-manager-logo"; -const BroadcasterSubscriptionId = "UserLayoutComponent"; - @Component({ selector: "app-user-layout", templateUrl: "user-layout.component.html", @@ -34,10 +31,10 @@ const BroadcasterSubscriptionId = "UserLayoutComponent"; PaymentMethodWarningsModule, ], }) -export class UserLayoutComponent implements OnInit, OnDestroy { +export class UserLayoutComponent implements OnInit { protected readonly logo = PasswordManagerLogo; - hasFamilySponsorshipAvailable: boolean; - hideSubscription: boolean; + protected hasFamilySponsorshipAvailable$: Observable<boolean>; + protected showSubscription$: Observable<boolean>; protected showPaymentMethodWarningBanners$ = this.configService.getFeatureFlag$( FeatureFlag.ShowPaymentMethodWarningBanners, @@ -45,8 +42,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 +53,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/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/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..a1ae64a885 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,7 +116,7 @@ export abstract class OrganizationService { * https://bitwarden.atlassian.net/browse/AC-2252. */ getFromState: (id: string) => Promise<Organization>; - canManageSponsorships: () => Promise<boolean>; + canManageSponsorships$: Observable<boolean>; hasOrganizations: () => Promise<boolean>; get$: (id: string) => Observable<Organization | undefined>; get: (id: string) => Promise<Organization>; 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..411850fe30 100644 --- a/libs/common/src/admin-console/services/organization/organization.service.ts +++ b/libs/common/src/admin-console/services/organization/organization.service.ts @@ -77,14 +77,10 @@ export class OrganizationService implements InternalOrganizationServiceAbstracti return await firstValueFrom(this.getOrganizationsFromState$(userId as UserId)); } - async canManageSponsorships(): Promise<boolean> { - return await firstValueFrom( - this.organizations$.pipe( - mapToExcludeOrganizationsWithoutFamilySponsorshipSupport(), - mapToBooleanHasAnyOrganizations(), - ), - ); - } + canManageSponsorships$ = this.organizations$.pipe( + mapToExcludeOrganizationsWithoutFamilySponsorshipSupport(), + mapToBooleanHasAnyOrganizations(), + ); async hasOrganizations(): Promise<boolean> { return await firstValueFrom(this.organizations$.pipe(mapToBooleanHasAnyOrganizations())); From 8cdcb51e3c9703fa3386f7713520dc534e3bff63 Mon Sep 17 00:00:00 2001 From: Cesar Gonzalez <cesar.a.gonzalezcs@gmail.com> Date: Wed, 3 Apr 2024 16:32:58 -0500 Subject: [PATCH 105/351] [PM-7256] Input element loses focus when inline menu is opened in Safari (#8600) --- .../autofill-overlay-list.spec.ts.snap | 6 ------ .../pages/list/autofill-overlay-list.spec.ts | 18 ++++++++++++++++++ .../pages/list/autofill-overlay-list.ts | 5 +++-- 3 files changed, 21 insertions(+), 8 deletions(-) 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`] = ` <div - aria-modal="true" class="overlay-list-container theme_light" - role="dialog" > <ul class="overlay-actions-list" @@ -436,9 +434,7 @@ exports[`AutofillOverlayList initAutofillOverlayList the list of ciphers for an exports[`AutofillOverlayList initAutofillOverlayList the locked overlay for an unauthenticated user creates the views for the locked overlay 1`] = ` <div - aria-modal="true" class="overlay-list-container theme_light" - role="dialog" > <div class="locked-overlay overlay-list-message" @@ -490,9 +486,7 @@ exports[`AutofillOverlayList initAutofillOverlayList the locked overlay for an u exports[`AutofillOverlayList initAutofillOverlayList the overlay with an empty list of ciphers creates the views for the no results overlay 1`] = ` <div - aria-modal="true" class="overlay-list-container theme_light" - role="dialog" > <div class="no-items overlay-list-message" diff --git a/apps/browser/src/autofill/overlay/pages/list/autofill-overlay-list.spec.ts b/apps/browser/src/autofill/overlay/pages/list/autofill-overlay-list.spec.ts index ce31b61fa5..e0ad15444f 100644 --- a/apps/browser/src/autofill/overlay/pages/list/autofill-overlay-list.spec.ts +++ b/apps/browser/src/autofill/overlay/pages/list/autofill-overlay-list.spec.ts @@ -312,6 +312,24 @@ describe("AutofillOverlayList", () => { }); describe("directing user focus into the overlay list", () => { + it("sets ARIA attributes that define the list as a `dialog` to screen reader users", () => { + postWindowMessage( + createInitAutofillOverlayListMessageMock({ + authStatus: AuthenticationStatus.Locked, + cipherList: [], + }), + ); + const overlayContainerSetAttributeSpy = jest.spyOn( + autofillOverlayList["overlayListContainer"], + "setAttribute", + ); + + postWindowMessage({ command: "focusOverlayList" }); + + expect(overlayContainerSetAttributeSpy).toHaveBeenCalledWith("role", "dialog"); + expect(overlayContainerSetAttributeSpy).toHaveBeenCalledWith("aria-modal", "true"); + }); + it("focuses the unlock button element if the user is not authenticated", () => { postWindowMessage( createInitAutofillOverlayListMessageMock({ diff --git a/apps/browser/src/autofill/overlay/pages/list/autofill-overlay-list.ts b/apps/browser/src/autofill/overlay/pages/list/autofill-overlay-list.ts index 8d4fa724af..4892ec4abf 100644 --- a/apps/browser/src/autofill/overlay/pages/list/autofill-overlay-list.ts +++ b/apps/browser/src/autofill/overlay/pages/list/autofill-overlay-list.ts @@ -59,8 +59,6 @@ class AutofillOverlayList extends AutofillOverlayPageElement { this.overlayListContainer = globalThis.document.createElement("div"); this.overlayListContainer.classList.add("overlay-list-container", themeClass); - this.overlayListContainer.setAttribute("role", "dialog"); - this.overlayListContainer.setAttribute("aria-modal", "true"); this.resizeObserver.observe(this.overlayListContainer); this.shadowDom.append(linkElement, this.overlayListContainer); @@ -487,6 +485,9 @@ class AutofillOverlayList extends AutofillOverlayPageElement { * the first cipher button. */ private focusOverlayList() { + this.overlayListContainer.setAttribute("role", "dialog"); + this.overlayListContainer.setAttribute("aria-modal", "true"); + const unlockButtonElement = this.overlayListContainer.querySelector( "#unlock-button", ) as HTMLElement; From 678ba04781a649515c5df18bbfa899f2ebb01f62 Mon Sep 17 00:00:00 2001 From: SmithThe4th <gsmith@bitwarden.com> Date: Wed, 3 Apr 2024 18:58:19 -0400 Subject: [PATCH 106/351] [PM-6532] Make Admin Console Single Sign on Settings Page Consistent With Others (#8550) * added class to reduce width of fields * moved class to form * updated sso template to use bit container which already handles width --- .../src/app/auth/sso/sso.component.html | 1010 +++++++++-------- 1 file changed, 508 insertions(+), 502 deletions(-) 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 72a073e0c0..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 @@ <app-header></app-header> -<ng-container *ngIf="loading"> - <i - class="bwi bwi-spinner bwi-spin text-muted" - title="{{ 'loading' | i18n }}" - aria-hidden="true" - ></i> - <span class="tw-sr-only">{{ "loading" | i18n }}</span> -</ng-container> +<bit-container> + <ng-container *ngIf="loading"> + <i + class="bwi bwi-spinner bwi-spin text-muted" + title="{{ 'loading' | i18n }}" + aria-hidden="true" + ></i> + <span class="tw-sr-only">{{ "loading" | i18n }}</span> + </ng-container> -<form [formGroup]="ssoConfigForm" [bitSubmit]="submit" *ngIf="!loading" class="tw-w-2/3"> - <p> - {{ "ssoPolicyHelpStart" | i18n }} - <a routerLink="../policies">{{ "ssoPolicyHelpAnchor" | i18n }}</a> - {{ "ssoPolicyHelpEnd" | i18n }} - <br /> - </p> + <form [formGroup]="ssoConfigForm" [bitSubmit]="submit" *ngIf="!loading"> + <p> + {{ "ssoPolicyHelpStart" | i18n }} + <a routerLink="../policies">{{ "ssoPolicyHelpAnchor" | i18n }}</a> + {{ "ssoPolicyHelpEnd" | i18n }} + <br /> + </p> - <!-- Root form --> - <ng-container> - <bit-form-control> - <bit-label>{{ "allowSso" | i18n }}</bit-label> - <input bitCheckbox type="checkbox" formControlName="enabled" id="enabled" /> - <bit-hint>{{ "allowSsoDesc" | i18n }}</bit-hint> - </bit-form-control> - - <bit-form-field> - <bit-label>{{ "ssoIdentifier" | i18n }}</bit-label> - <input bitInput type="text" formControlName="ssoIdentifier" /> - <bit-hint> - {{ "ssoIdentifierHintPartOne" | i18n }} - <a routerLink="../domain-verification">{{ "domainVerification" | i18n }}</a> - </bit-hint> - </bit-form-field> - - <hr /> - - <bit-radio-group formControlName="memberDecryptionType"> - <bit-label>{{ "memberDecryptionOption" | i18n }}</bit-label> - - <bit-radio-button - class="tw-block" - id="memberDecryptionPass" - [value]="memberDecryptionType.MasterPassword" - > - <bit-label>{{ "masterPass" | i18n }}</bit-label> - </bit-radio-button> - - <bit-radio-button - class="tw-block" - id="memberDecryptionKey" - [value]="memberDecryptionType.KeyConnector" - [disabled]="!organization.useKeyConnector || null" - *ngIf="showKeyConnectorOptions" - > - <bit-label> - {{ "keyConnector" | i18n }} - <a - target="_blank" - rel="noreferrer" - appA11yTitle="{{ 'learnMore' | i18n }}" - href="https://bitwarden.com/help/about-key-connector/" - > - <i class="bwi bwi-question-circle" aria-hidden="true"></i> - </a> - </bit-label> - <bit-hint> - {{ "memberDecryptionKeyConnectorDescStart" | i18n }} - <a routerLink="../policies">{{ "memberDecryptionKeyConnectorDescLink" | i18n }}</a> - {{ "memberDecryptionKeyConnectorDescEnd" | i18n }} - </bit-hint> - </bit-radio-button> - - <bit-radio-button - class="tw-block" - id="memberDecryptionTde" - [value]="memberDecryptionType.TrustedDeviceEncryption" - > - <bit-label> - {{ "trustedDevices" | i18n }} - </bit-label> - <bit-hint> - {{ "memberDecryptionOptionTdeDescriptionPartOne" | i18n }} - <a routerLink="../policies">{{ "memberDecryptionOptionTdeDescriptionLinkOne" | i18n }}</a> - {{ "memberDecryptionOptionTdeDescriptionPartTwo" | i18n }} - <a routerLink="../policies">{{ "memberDecryptionOptionTdeDescriptionLinkTwo" | i18n }}</a> - {{ "memberDecryptionOptionTdeDescriptionPartThree" | i18n }} - <a routerLink="../policies">{{ - "memberDecryptionOptionTdeDescriptionLinkThree" | i18n - }}</a> - {{ "memberDecryptionOptionTdeDescriptionPartFour" | i18n }} - </bit-hint> - </bit-radio-button> - </bit-radio-group> - - <!-- Key Connector --> - <ng-container - *ngIf=" - ssoConfigForm.value.memberDecryptionType === memberDecryptionType.KeyConnector && - showKeyConnectorOptions - " - > - <app-callout type="warning" [useAlertRole]="true"> - {{ "keyConnectorWarning" | i18n }} - </app-callout> + <!-- Root form --> + <ng-container> + <bit-form-control> + <bit-label>{{ "allowSso" | i18n }}</bit-label> + <input bitCheckbox type="checkbox" formControlName="enabled" id="enabled" /> + <bit-hint>{{ "allowSsoDesc" | i18n }}</bit-hint> + </bit-form-control> <bit-form-field> - <bit-label>{{ "keyConnectorUrl" | i18n }}</bit-label> - <input - bitInput - type="text" - required - formControlName="keyConnectorUrl" - appInputStripSpaces - (input)="haveTestedKeyConnector = false" - /> - <button - bitSuffix - bitButton - [disabled]="!enableTestKeyConnector" - type="button" - (click)="validateKeyConnectorUrl()" - > - <i - class="bwi bwi-spinner bwi-spin" - title="{{ 'loading' | i18n }}" - aria-hidden="true" - *ngIf="keyConnectorUrl.pending" - ></i> - <span *ngIf="!keyConnectorUrl.pending"> - {{ "keyConnectorTest" | i18n }} - </span> - </button> - <bit-hint - aria-live="polite" - *ngIf="haveTestedKeyConnector && !keyConnectorUrl.hasError('invalidUrl')" - > - <small class="text-success"> - <i class="bwi bwi-check-circle" aria-hidden="true"></i> - {{ "keyConnectorTestSuccess" | i18n }} - </small> + <bit-label>{{ "ssoIdentifier" | i18n }}</bit-label> + <input bitInput type="text" formControlName="ssoIdentifier" /> + <bit-hint> + {{ "ssoIdentifierHintPartOne" | i18n }} + <a routerLink="../domain-verification">{{ "domainVerification" | i18n }}</a> </bit-hint> </bit-form-field> + + <hr /> + + <bit-radio-group formControlName="memberDecryptionType"> + <bit-label>{{ "memberDecryptionOption" | i18n }}</bit-label> + + <bit-radio-button + class="tw-block" + id="memberDecryptionPass" + [value]="memberDecryptionType.MasterPassword" + > + <bit-label>{{ "masterPass" | i18n }}</bit-label> + </bit-radio-button> + + <bit-radio-button + class="tw-block" + id="memberDecryptionKey" + [value]="memberDecryptionType.KeyConnector" + [disabled]="!organization.useKeyConnector || null" + *ngIf="showKeyConnectorOptions" + > + <bit-label> + {{ "keyConnector" | i18n }} + <a + target="_blank" + rel="noreferrer" + appA11yTitle="{{ 'learnMore' | i18n }}" + href="https://bitwarden.com/help/about-key-connector/" + > + <i class="bwi bwi-question-circle" aria-hidden="true"></i> + </a> + </bit-label> + <bit-hint> + {{ "memberDecryptionKeyConnectorDescStart" | i18n }} + <a routerLink="../policies">{{ "memberDecryptionKeyConnectorDescLink" | i18n }}</a> + {{ "memberDecryptionKeyConnectorDescEnd" | i18n }} + </bit-hint> + </bit-radio-button> + + <bit-radio-button + class="tw-block" + id="memberDecryptionTde" + [value]="memberDecryptionType.TrustedDeviceEncryption" + > + <bit-label> + {{ "trustedDevices" | i18n }} + </bit-label> + <bit-hint> + {{ "memberDecryptionOptionTdeDescriptionPartOne" | i18n }} + <a routerLink="../policies">{{ + "memberDecryptionOptionTdeDescriptionLinkOne" | i18n + }}</a> + {{ "memberDecryptionOptionTdeDescriptionPartTwo" | i18n }} + <a routerLink="../policies">{{ + "memberDecryptionOptionTdeDescriptionLinkTwo" | i18n + }}</a> + {{ "memberDecryptionOptionTdeDescriptionPartThree" | i18n }} + <a routerLink="../policies">{{ + "memberDecryptionOptionTdeDescriptionLinkThree" | i18n + }}</a> + {{ "memberDecryptionOptionTdeDescriptionPartFour" | i18n }} + </bit-hint> + </bit-radio-button> + </bit-radio-group> + + <!-- Key Connector --> + <ng-container + *ngIf=" + ssoConfigForm.value.memberDecryptionType === memberDecryptionType.KeyConnector && + showKeyConnectorOptions + " + > + <app-callout type="warning" [useAlertRole]="true"> + {{ "keyConnectorWarning" | i18n }} + </app-callout> + + <bit-form-field> + <bit-label>{{ "keyConnectorUrl" | i18n }}</bit-label> + <input + bitInput + type="text" + required + formControlName="keyConnectorUrl" + appInputStripSpaces + (input)="haveTestedKeyConnector = false" + /> + <button + bitSuffix + bitButton + [disabled]="!enableTestKeyConnector" + type="button" + (click)="validateKeyConnectorUrl()" + > + <i + class="bwi bwi-spinner bwi-spin" + title="{{ 'loading' | i18n }}" + aria-hidden="true" + *ngIf="keyConnectorUrl.pending" + ></i> + <span *ngIf="!keyConnectorUrl.pending"> + {{ "keyConnectorTest" | i18n }} + </span> + </button> + <bit-hint + aria-live="polite" + *ngIf="haveTestedKeyConnector && !keyConnectorUrl.hasError('invalidUrl')" + > + <small class="text-success"> + <i class="bwi bwi-check-circle" aria-hidden="true"></i> + {{ "keyConnectorTestSuccess" | i18n }} + </small> + </bit-hint> + </bit-form-field> + </ng-container> + + <hr /> + + <bit-form-field> + <bit-label>{{ "type" | i18n }}</bit-label> + <select bitInput formControlName="configType"> + <option *ngFor="let o of ssoTypeOptions" [ngValue]="o.value" disabled="{{ o.disabled }}"> + {{ o.name }} + </option> + </select> + </bit-form-field> </ng-container> - <hr /> + <!-- OIDC --> + <div + *ngIf="ssoConfigForm.get('configType').value === ssoType.OpenIdConnect" + [formGroup]="openIdForm" + > + <div class="config-section"> + <h2 class="secondary-header">{{ "openIdConnectConfig" | i18n }}</h2> - <bit-form-field> - <bit-label>{{ "type" | i18n }}</bit-label> - <select bitInput formControlName="configType"> - <option *ngFor="let o of ssoTypeOptions" [ngValue]="o.value" disabled="{{ o.disabled }}"> - {{ o.name }} - </option> - </select> - </bit-form-field> - </ng-container> + <bit-form-field> + <bit-label>{{ "callbackPath" | i18n }}</bit-label> + <input bitInput disabled [value]="callbackPath" /> + <button + bitIconButton="bwi-clone" + bitSuffix + type="button" + [appCopyClick]="callbackPath" + [appA11yTitle]="'copyValue' | i18n" + ></button> + </bit-form-field> - <!-- OIDC --> - <div - *ngIf="ssoConfigForm.get('configType').value === ssoType.OpenIdConnect" - [formGroup]="openIdForm" - > - <div class="config-section"> - <h2 class="secondary-header">{{ "openIdConnectConfig" | i18n }}</h2> + <bit-form-field> + <bit-label>{{ "signedOutCallbackPath" | i18n }}</bit-label> + <input bitInput disabled [value]="signedOutCallbackPath" /> + <button + bitIconButton="bwi-clone" + bitSuffix + type="button" + [appCopyClick]="signedOutCallbackPath" + [appA11yTitle]="'copyValue' | i18n" + ></button> + </bit-form-field> - <bit-form-field> - <bit-label>{{ "callbackPath" | i18n }}</bit-label> - <input bitInput disabled [value]="callbackPath" /> - <button - bitIconButton="bwi-clone" - bitSuffix - type="button" - [appCopyClick]="callbackPath" - [appA11yTitle]="'copyValue' | i18n" - ></button> - </bit-form-field> + <bit-form-field> + <bit-label>{{ "authority" | i18n }}</bit-label> + <input bitInput type="text" formControlName="authority" appInputStripSpaces /> + </bit-form-field> - <bit-form-field> - <bit-label>{{ "signedOutCallbackPath" | i18n }}</bit-label> - <input bitInput disabled [value]="signedOutCallbackPath" /> - <button - bitIconButton="bwi-clone" - bitSuffix - type="button" - [appCopyClick]="signedOutCallbackPath" - [appA11yTitle]="'copyValue' | i18n" - ></button> - </bit-form-field> + <bit-form-field> + <bit-label>{{ "clientId" | i18n }}</bit-label> + <input bitInput type="text" formControlName="clientId" appInputStripSpaces /> + </bit-form-field> - <bit-form-field> - <bit-label>{{ "authority" | i18n }}</bit-label> - <input bitInput type="text" formControlName="authority" appInputStripSpaces /> - </bit-form-field> + <bit-form-field> + <bit-label>{{ "clientSecret" | i18n }}</bit-label> + <input bitInput type="text" formControlName="clientSecret" appInputStripSpaces /> + </bit-form-field> - <bit-form-field> - <bit-label>{{ "clientId" | i18n }}</bit-label> - <input bitInput type="text" formControlName="clientId" appInputStripSpaces /> - </bit-form-field> + <bit-form-field> + <bit-label>{{ "metadataAddress" | i18n }}</bit-label> + <input bitInput type="text" formControlName="metadataAddress" appInputStripSpaces /> + <bit-hint>{{ "openIdAuthorityRequired" | i18n }}</bit-hint> + </bit-form-field> - <bit-form-field> - <bit-label>{{ "clientSecret" | i18n }}</bit-label> - <input bitInput type="text" formControlName="clientSecret" appInputStripSpaces /> - </bit-form-field> + <bit-form-field> + <bit-label>{{ "oidcRedirectBehavior" | i18n }}</bit-label> + <select bitInput formControlName="redirectBehavior"> + <option + *ngFor="let o of connectRedirectOptions" + [ngValue]="o.value" + disabled="{{ o.disabled }}" + > + {{ o.name }} + </option> + </select> + </bit-form-field> - <bit-form-field> - <bit-label>{{ "metadataAddress" | i18n }}</bit-label> - <input bitInput type="text" formControlName="metadataAddress" appInputStripSpaces /> - <bit-hint>{{ "openIdAuthorityRequired" | i18n }}</bit-hint> - </bit-form-field> + <bit-form-control> + <bit-label>{{ "getClaimsFromUserInfoEndpoint" | i18n }}</bit-label> + <input + bitCheckbox + type="checkbox" + formControlName="getClaimsFromUserInfoEndpoint" + id="getClaimsFromUserInfoEndpoint" + /> + </bit-form-control> - <bit-form-field> - <bit-label>{{ "oidcRedirectBehavior" | i18n }}</bit-label> - <select bitInput formControlName="redirectBehavior"> - <option - *ngFor="let o of connectRedirectOptions" - [ngValue]="o.value" - disabled="{{ o.disabled }}" - > - {{ o.name }} - </option> - </select> - </bit-form-field> - - <bit-form-control> - <bit-label>{{ "getClaimsFromUserInfoEndpoint" | i18n }}</bit-label> - <input - bitCheckbox - type="checkbox" - formControlName="getClaimsFromUserInfoEndpoint" - id="getClaimsFromUserInfoEndpoint" - /> - </bit-form-control> - - <!-- Optional customizations --> - <div - class="section-header d-flex flex-row align-items-center mt-3 mb-3" - (click)="toggleOpenIdCustomizations()" - > - <h3 class="mb-0 mr-2" id="customizations-header"> - {{ "openIdOptionalCustomizations" | i18n }} - </h3> - <button - class="mb-1 btn btn-link" - type="button" - appStopClick - role="button" - aria-controls="customizations" - [attr.aria-expanded]="showOpenIdCustomizations" - aria-labelledby="customizations-header" + <!-- Optional customizations --> + <div + class="section-header d-flex flex-row align-items-center mt-3 mb-3" + (click)="toggleOpenIdCustomizations()" > - <i - class="bwi" - aria-hidden="true" - [ngClass]="{ - 'bwi-angle-right': !showOpenIdCustomizations, - 'bwi-angle-down': showOpenIdCustomizations - }" - ></i> - </button> - </div> - <div id="customizations" [hidden]="!showOpenIdCustomizations"> - <bit-form-field> - <bit-label>{{ "additionalScopes" | i18n }}</bit-label> - <input bitInput type="text" formControlName="additionalScopes" /> - <bit-hint>{{ "separateMultipleWithComma" | i18n }}</bit-hint> - </bit-form-field> + <h3 class="mb-0 mr-2" id="customizations-header"> + {{ "openIdOptionalCustomizations" | i18n }} + </h3> + <button + class="mb-1 btn btn-link" + type="button" + appStopClick + role="button" + aria-controls="customizations" + [attr.aria-expanded]="showOpenIdCustomizations" + aria-labelledby="customizations-header" + > + <i + class="bwi" + aria-hidden="true" + [ngClass]="{ + 'bwi-angle-right': !showOpenIdCustomizations, + 'bwi-angle-down': showOpenIdCustomizations + }" + ></i> + </button> + </div> + <div id="customizations" [hidden]="!showOpenIdCustomizations"> + <bit-form-field> + <bit-label>{{ "additionalScopes" | i18n }}</bit-label> + <input bitInput type="text" formControlName="additionalScopes" /> + <bit-hint>{{ "separateMultipleWithComma" | i18n }}</bit-hint> + </bit-form-field> - <bit-form-field> - <bit-label>{{ "additionalUserIdClaimTypes" | i18n }}</bit-label> - <input bitInput type="text" formControlName="additionalUserIdClaimTypes" /> - <bit-hint>{{ "separateMultipleWithComma" | i18n }}</bit-hint> - </bit-form-field> + <bit-form-field> + <bit-label>{{ "additionalUserIdClaimTypes" | i18n }}</bit-label> + <input bitInput type="text" formControlName="additionalUserIdClaimTypes" /> + <bit-hint>{{ "separateMultipleWithComma" | i18n }}</bit-hint> + </bit-form-field> - <bit-form-field> - <bit-label>{{ "additionalEmailClaimTypes" | i18n }}</bit-label> - <input bitInput type="text" formControlName="additionalEmailClaimTypes" /> - <bit-hint>{{ "separateMultipleWithComma" | i18n }}</bit-hint> - </bit-form-field> + <bit-form-field> + <bit-label>{{ "additionalEmailClaimTypes" | i18n }}</bit-label> + <input bitInput type="text" formControlName="additionalEmailClaimTypes" /> + <bit-hint>{{ "separateMultipleWithComma" | i18n }}</bit-hint> + </bit-form-field> - <bit-form-field> - <bit-label>{{ "additionalNameClaimTypes" | i18n }}</bit-label> - <input bitInput type="text" formControlName="additionalNameClaimTypes" /> - <bit-hint>{{ "separateMultipleWithComma" | i18n }}</bit-hint> - </bit-form-field> + <bit-form-field> + <bit-label>{{ "additionalNameClaimTypes" | i18n }}</bit-label> + <input bitInput type="text" formControlName="additionalNameClaimTypes" /> + <bit-hint>{{ "separateMultipleWithComma" | i18n }}</bit-hint> + </bit-form-field> - <bit-form-field> - <bit-label>{{ "acrValues" | i18n }}</bit-label> - <input bitInput type="text" formControlName="acrValues" /> - <bit-hint>acr_values</bit-hint> - </bit-form-field> + <bit-form-field> + <bit-label>{{ "acrValues" | i18n }}</bit-label> + <input bitInput type="text" formControlName="acrValues" /> + <bit-hint>acr_values</bit-hint> + </bit-form-field> - <bit-form-field> - <bit-label>{{ "expectedReturnAcrValue" | i18n }}</bit-label> - <input bitInput type="text" formControlName="expectedReturnAcrValue" /> - <bit-hint>acr_validaton</bit-hint> - </bit-form-field> + <bit-form-field> + <bit-label>{{ "expectedReturnAcrValue" | i18n }}</bit-label> + <input bitInput type="text" formControlName="expectedReturnAcrValue" /> + <bit-hint>acr_validaton</bit-hint> + </bit-form-field> + </div> </div> </div> - </div> - <!-- SAML2 SP --> - <div *ngIf="ssoConfigForm.get('configType').value === ssoType.Saml2" [formGroup]="samlForm"> <!-- SAML2 SP --> - <div class="config-section"> - <h2 class="secondary-header">{{ "samlSpConfig" | i18n }}</h2> + <div *ngIf="ssoConfigForm.get('configType').value === ssoType.Saml2" [formGroup]="samlForm"> + <!-- SAML2 SP --> + <div class="config-section"> + <h2 class="secondary-header">{{ "samlSpConfig" | i18n }}</h2> - <bit-form-control> - <bit-label>{{ "spUniqueEntityId" | i18n }}</bit-label> - <input - bitCheckbox - type="checkbox" - formControlName="spUniqueEntityId" - id="spUniqueEntityId" - /> - <bit-hint>{{ "spUniqueEntityIdDesc" | i18n }}</bit-hint> - </bit-form-control> + <bit-form-control> + <bit-label>{{ "spUniqueEntityId" | i18n }}</bit-label> + <input + bitCheckbox + type="checkbox" + formControlName="spUniqueEntityId" + id="spUniqueEntityId" + /> + <bit-hint>{{ "spUniqueEntityIdDesc" | i18n }}</bit-hint> + </bit-form-control> - <bit-form-field *ngIf="ssoConfigForm.value.saml.spUniqueEntityId"> - <bit-label>{{ "spEntityId" | i18n }}</bit-label> - <input bitInput disabled [value]="spEntityId" /> - <button - bitIconButton="bwi-clone" - bitSuffix - type="button" - [appCopyClick]="spEntityId" - [appA11yTitle]="'copyValue' | i18n" - ></button> - </bit-form-field> + <bit-form-field *ngIf="ssoConfigForm.value.saml.spUniqueEntityId"> + <bit-label>{{ "spEntityId" | i18n }}</bit-label> + <input bitInput disabled [value]="spEntityId" /> + <button + bitIconButton="bwi-clone" + bitSuffix + type="button" + [appCopyClick]="spEntityId" + [appA11yTitle]="'copyValue' | i18n" + ></button> + </bit-form-field> - <bit-form-field *ngIf="!ssoConfigForm.value.saml.spUniqueEntityId"> - <bit-label>{{ "spEntityId" | i18n }}</bit-label> - <input bitInput disabled [value]="spEntityIdStatic" /> - <button - bitIconButton="bwi-clone" - bitSuffix - type="button" - [appCopyClick]="spEntityIdStatic" - [appA11yTitle]="'copyValue' | i18n" - ></button> - </bit-form-field> + <bit-form-field *ngIf="!ssoConfigForm.value.saml.spUniqueEntityId"> + <bit-label>{{ "spEntityId" | i18n }}</bit-label> + <input bitInput disabled [value]="spEntityIdStatic" /> + <button + bitIconButton="bwi-clone" + bitSuffix + type="button" + [appCopyClick]="spEntityIdStatic" + [appA11yTitle]="'copyValue' | i18n" + ></button> + </bit-form-field> - <bit-form-field> - <bit-label>{{ "spMetadataUrl" | i18n }}</bit-label> - <input bitInput disabled [value]="spMetadataUrl" /> - <button - bitButton - bitSuffix - type="button" - [appLaunchClick]="spMetadataUrl" - [appA11yTitle]="'launch' | i18n" - > - <i class="bwi bwi-lg bwi-external-link" aria-hidden="true"></i> - </button> - <button - bitIconButton="bwi-clone" - bitSuffix - type="button" - [appCopyClick]="spMetadataUrl" - [appA11yTitle]="'copyValue' | i18n" - ></button> - </bit-form-field> - - <bit-form-field> - <bit-label>{{ "spAcsUrl" | i18n }}</bit-label> - <input bitInput disabled [value]="spAcsUrl" /> - <button - bitIconButton="bwi-clone" - bitSuffix - type="button" - [appCopyClick]="spAcsUrl" - [appA11yTitle]="'copyValue' | i18n" - ></button> - </bit-form-field> - - <bit-form-field> - <bit-label>{{ "spNameIdFormat" | i18n }}</bit-label> - <select bitInput formControlName="spNameIdFormat"> - <option - *ngFor="let o of saml2NameIdFormatOptions" - [ngValue]="o.value" - disabled="{{ o.disabled }}" + <bit-form-field> + <bit-label>{{ "spMetadataUrl" | i18n }}</bit-label> + <input bitInput disabled [value]="spMetadataUrl" /> + <button + bitButton + bitSuffix + type="button" + [appLaunchClick]="spMetadataUrl" + [appA11yTitle]="'launch' | i18n" > - {{ o.name }} - </option> - </select> - </bit-form-field> + <i class="bwi bwi-lg bwi-external-link" aria-hidden="true"></i> + </button> + <button + bitIconButton="bwi-clone" + bitSuffix + type="button" + [appCopyClick]="spMetadataUrl" + [appA11yTitle]="'copyValue' | i18n" + ></button> + </bit-form-field> - <bit-form-field> - <bit-label>{{ "spOutboundSigningAlgorithm" | i18n }}</bit-label> - <select bitInput formControlName="spOutboundSigningAlgorithm"> - <option - *ngFor="let o of samlSigningAlgorithmOptions" - [ngValue]="o.value" - disabled="{{ o.disabled }}" - > - {{ o.name }} - </option> - </select> - </bit-form-field> + <bit-form-field> + <bit-label>{{ "spAcsUrl" | i18n }}</bit-label> + <input bitInput disabled [value]="spAcsUrl" /> + <button + bitIconButton="bwi-clone" + bitSuffix + type="button" + [appCopyClick]="spAcsUrl" + [appA11yTitle]="'copyValue' | i18n" + ></button> + </bit-form-field> - <bit-form-field> - <bit-label>{{ "spSigningBehavior" | i18n }}</bit-label> - <select bitInput formControlName="spSigningBehavior"> - <option - *ngFor="let o of saml2SigningBehaviourOptions" - [ngValue]="o.value" - disabled="{{ o.disabled }}" - > - {{ o.name }} - </option> - </select> - </bit-form-field> + <bit-form-field> + <bit-label>{{ "spNameIdFormat" | i18n }}</bit-label> + <select bitInput formControlName="spNameIdFormat"> + <option + *ngFor="let o of saml2NameIdFormatOptions" + [ngValue]="o.value" + disabled="{{ o.disabled }}" + > + {{ o.name }} + </option> + </select> + </bit-form-field> - <bit-form-field> - <bit-label>{{ "spMinIncomingSigningAlgorithm" | i18n }}</bit-label> - <select bitInput formControlName="spMinIncomingSigningAlgorithm"> - <option - *ngFor="let o of samlSigningAlgorithmOptions" - [ngValue]="o.value" - disabled="{{ o.disabled }}" - > - {{ o.name }} - </option> - </select> - </bit-form-field> + <bit-form-field> + <bit-label>{{ "spOutboundSigningAlgorithm" | i18n }}</bit-label> + <select bitInput formControlName="spOutboundSigningAlgorithm"> + <option + *ngFor="let o of samlSigningAlgorithmOptions" + [ngValue]="o.value" + disabled="{{ o.disabled }}" + > + {{ o.name }} + </option> + </select> + </bit-form-field> - <bit-form-control> - <bit-label>{{ "spWantAssertionsSigned" | i18n }}</bit-label> - <input - bitCheckbox - type="checkbox" - formControlName="spWantAssertionsSigned" - id="spWantAssertionsSigned" - /> - </bit-form-control> + <bit-form-field> + <bit-label>{{ "spSigningBehavior" | i18n }}</bit-label> + <select bitInput formControlName="spSigningBehavior"> + <option + *ngFor="let o of saml2SigningBehaviourOptions" + [ngValue]="o.value" + disabled="{{ o.disabled }}" + > + {{ o.name }} + </option> + </select> + </bit-form-field> - <bit-form-control> - <bit-label>{{ "spValidateCertificates" | i18n }}</bit-label> - <input - bitCheckbox - type="checkbox" - formControlName="spValidateCertificates" - id="spValidateCertificates" - /> - </bit-form-control> + <bit-form-field> + <bit-label>{{ "spMinIncomingSigningAlgorithm" | i18n }}</bit-label> + <select bitInput formControlName="spMinIncomingSigningAlgorithm"> + <option + *ngFor="let o of samlSigningAlgorithmOptions" + [ngValue]="o.value" + disabled="{{ o.disabled }}" + > + {{ o.name }} + </option> + </select> + </bit-form-field> + + <bit-form-control> + <bit-label>{{ "spWantAssertionsSigned" | i18n }}</bit-label> + <input + bitCheckbox + type="checkbox" + formControlName="spWantAssertionsSigned" + id="spWantAssertionsSigned" + /> + </bit-form-control> + + <bit-form-control> + <bit-label>{{ "spValidateCertificates" | i18n }}</bit-label> + <input + bitCheckbox + type="checkbox" + formControlName="spValidateCertificates" + id="spValidateCertificates" + /> + </bit-form-control> + </div> + + <!-- SAML2 IDP --> + <div class="config-section"> + <h2 class="secondary-header">{{ "samlIdpConfig" | i18n }}</h2> + + <bit-form-field> + <bit-label>{{ "idpEntityId" | i18n }}</bit-label> + <input bitInput type="text" formControlName="idpEntityId" /> + </bit-form-field> + + <bit-form-field> + <bit-label>{{ "idpBindingType" | i18n }}</bit-label> + <select bitInput formControlName="idpBindingType"> + <option + *ngFor="let o of saml2BindingTypeOptions" + [ngValue]="o.value" + disabled="{{ o.disabled }}" + > + {{ o.name }} + </option> + </select> + </bit-form-field> + + <bit-form-field> + <bit-label>{{ "idpSingleSignOnServiceUrl" | i18n }}</bit-label> + <input + bitInput + type="text" + formControlName="idpSingleSignOnServiceUrl" + appInputStripSpaces + /> + <bit-hint>{{ "idpSingleSignOnServiceUrlRequired" | i18n }}</bit-hint> + </bit-form-field> + + <bit-form-field> + <bit-label>{{ "idpSingleLogoutServiceUrl" | i18n }}</bit-label> + <input + bitInput + type="text" + formControlName="idpSingleLogoutServiceUrl" + appInputStripSpaces + /> + </bit-form-field> + + <bit-form-field> + <bit-label>{{ "idpX509PublicCert" | i18n }}</bit-label> + <textarea bitInput rows="6" formControlName="idpX509PublicCert"></textarea> + </bit-form-field> + + <bit-form-field> + <bit-label>{{ "idpOutboundSigningAlgorithm" | i18n }}</bit-label> + <select bitInput formControlName="idpOutboundSigningAlgorithm"> + <option + *ngFor="let o of samlSigningAlgorithmOptions" + [ngValue]="o.value" + disabled="{{ o.disabled }}" + > + {{ o.name }} + </option> + </select> + </bit-form-field> + + <!--TODO: Uncomment once Unsolicited IdP Response is supported--> + <!-- <app-input-checkbox + controlId="idpAllowUnsolicitedAuthnResponse" + formControlName="idpAllowUnsolicitedAuthnResponse" + [label]="'idpAllowUnsolicitedAuthnResponse' | i18n" + ></app-input-checkbox> --> + + <bit-form-control> + <bit-label>{{ "idpAllowOutboundLogoutRequests" | i18n }}</bit-label> + <input + bitCheckbox + type="checkbox" + formControlName="idpAllowOutboundLogoutRequests" + id="idpAllowOutboundLogoutRequests" + /> + </bit-form-control> + + <bit-form-control> + <bit-label>{{ "idpSignAuthenticationRequests" | i18n }}</bit-label> + <input + bitCheckbox + type="checkbox" + formControlName="idpWantAuthnRequestsSigned" + id="idpWantAuthnRequestsSigned" + /> + </bit-form-control> + </div> </div> - <!-- SAML2 IDP --> - <div class="config-section"> - <h2 class="secondary-header">{{ "samlIdpConfig" | i18n }}</h2> - - <bit-form-field> - <bit-label>{{ "idpEntityId" | i18n }}</bit-label> - <input bitInput type="text" formControlName="idpEntityId" /> - </bit-form-field> - - <bit-form-field> - <bit-label>{{ "idpBindingType" | i18n }}</bit-label> - <select bitInput formControlName="idpBindingType"> - <option - *ngFor="let o of saml2BindingTypeOptions" - [ngValue]="o.value" - disabled="{{ o.disabled }}" - > - {{ o.name }} - </option> - </select> - </bit-form-field> - - <bit-form-field> - <bit-label>{{ "idpSingleSignOnServiceUrl" | i18n }}</bit-label> - <input - bitInput - type="text" - formControlName="idpSingleSignOnServiceUrl" - appInputStripSpaces - /> - <bit-hint>{{ "idpSingleSignOnServiceUrlRequired" | i18n }}</bit-hint> - </bit-form-field> - - <bit-form-field> - <bit-label>{{ "idpSingleLogoutServiceUrl" | i18n }}</bit-label> - <input - bitInput - type="text" - formControlName="idpSingleLogoutServiceUrl" - appInputStripSpaces - /> - </bit-form-field> - - <bit-form-field> - <bit-label>{{ "idpX509PublicCert" | i18n }}</bit-label> - <textarea bitInput rows="6" formControlName="idpX509PublicCert"></textarea> - </bit-form-field> - - <bit-form-field> - <bit-label>{{ "idpOutboundSigningAlgorithm" | i18n }}</bit-label> - <select bitInput formControlName="idpOutboundSigningAlgorithm"> - <option - *ngFor="let o of samlSigningAlgorithmOptions" - [ngValue]="o.value" - disabled="{{ o.disabled }}" - > - {{ o.name }} - </option> - </select> - </bit-form-field> - - <!--TODO: Uncomment once Unsolicited IdP Response is supported--> - <!-- <app-input-checkbox - controlId="idpAllowUnsolicitedAuthnResponse" - formControlName="idpAllowUnsolicitedAuthnResponse" - [label]="'idpAllowUnsolicitedAuthnResponse' | i18n" - ></app-input-checkbox> --> - - <bit-form-control> - <bit-label>{{ "idpAllowOutboundLogoutRequests" | i18n }}</bit-label> - <input - bitCheckbox - type="checkbox" - formControlName="idpAllowOutboundLogoutRequests" - id="idpAllowOutboundLogoutRequests" - /> - </bit-form-control> - - <bit-form-control> - <bit-label>{{ "idpSignAuthenticationRequests" | i18n }}</bit-label> - <input - bitCheckbox - type="checkbox" - formControlName="idpWantAuthnRequestsSigned" - id="idpWantAuthnRequestsSigned" - /> - </bit-form-control> - </div> - </div> - - <button type="submit" buttonType="primary" bitButton bitFormButton> - {{ "save" | i18n }} - </button> - <bit-error-summary [formGroup]="ssoConfigForm"></bit-error-summary> -</form> + <button type="submit" buttonType="primary" bitButton bitFormButton> + {{ "save" | i18n }} + </button> + <bit-error-summary [formGroup]="ssoConfigForm"></bit-error-summary> + </form> +</bit-container> From 32981ce30d984ba47e26c39d3e85d7af665f9e58 Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Thu, 4 Apr 2024 13:48:41 +1000 Subject: [PATCH 107/351] [AC-2320] Update canEditAnyCollection logic for Flexible Collections v1 (#8394) * also update calling locations to use canEditAllCiphers where applicable --- .../vault-items/vault-items.component.ts | 3 +- .../vault/core/views/collection-admin.view.ts | 7 ++-- .../bulk-delete-dialog.component.ts | 19 +++++++++-- .../vault-header/vault-header.component.ts | 29 +++++++++++++--- .../individual-vault/vault.component.html | 1 + .../vault/individual-vault/vault.component.ts | 5 +++ .../vault/org-vault/attachments.component.ts | 33 +++++++++++++++---- .../vault-header/vault-header.component.ts | 17 ++++++++-- .../app/vault/org-vault/vault.component.html | 6 +++- .../app/vault/org-vault/vault.component.ts | 18 +++++----- .../vault/components/add-edit.component.ts | 6 ++-- .../models/domain/organization.ts | 32 +++++++++++++----- .../src/vault/models/view/collection.view.ts | 8 ++--- 13 files changed, 140 insertions(+), 44 deletions(-) diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts index 7a8e858ba5..8b6ead33be 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts @@ -45,6 +45,7 @@ export class VaultItemsComponent { @Input() showBulkAddToCollections = false; @Input() showPermissionsColumn = false; @Input() viewingOrgVault: boolean; + @Input({ required: true }) flexibleCollectionsV1Enabled = false; private _ciphers?: CipherView[] = []; @Input() get ciphers(): CipherView[] { @@ -101,7 +102,7 @@ export class VaultItemsComponent { } const organization = this.allOrganizations.find((o) => o.id === collection.organizationId); - return collection.canEdit(organization); + return collection.canEdit(organization, this.flexibleCollectionsV1Enabled); } protected canDeleteCollection(collection: CollectionView): boolean { diff --git a/apps/web/src/app/vault/core/views/collection-admin.view.ts b/apps/web/src/app/vault/core/views/collection-admin.view.ts index 160228576a..d942d42fb8 100644 --- a/apps/web/src/app/vault/core/views/collection-admin.view.ts +++ b/apps/web/src/app/vault/core/views/collection-admin.view.ts @@ -31,10 +31,11 @@ export class CollectionAdminView extends CollectionView { this.assigned = response.assigned; } - override canEdit(org: Organization): boolean { + override canEdit(org: Organization, flexibleCollectionsV1Enabled: boolean): boolean { return org?.flexibleCollections - ? org?.canEditAnyCollection || this.manage - : org?.canEditAnyCollection || (org?.canEditAssignedCollections && this.assigned); + ? org?.canEditAnyCollection(flexibleCollectionsV1Enabled) || this.manage + : org?.canEditAnyCollection(flexibleCollectionsV1Enabled) || + (org?.canEditAssignedCollections && this.assigned); } override canDelete(org: Organization): boolean { diff --git a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts index 70f7a555f3..4050823a6d 100644 --- a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts +++ b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts @@ -1,8 +1,11 @@ import { DialogConfig, DialogRef, DIALOG_DATA } from "@angular/cdk/dialog"; import { Component, Inject } from "@angular/core"; +import { firstValueFrom } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -49,6 +52,11 @@ export class BulkDeleteDialogComponent { organizations: Organization[]; collections: CollectionView[]; + private flexibleCollectionsV1Enabled$ = this.configService.getFeatureFlag$( + FeatureFlag.FlexibleCollectionsV1, + false, + ); + constructor( @Inject(DIALOG_DATA) params: BulkDeleteDialogParams, private dialogRef: DialogRef<BulkDeleteDialogResult>, @@ -57,6 +65,7 @@ export class BulkDeleteDialogComponent { private i18nService: I18nService, private apiService: ApiService, private collectionService: CollectionService, + private configService: ConfigService, ) { this.cipherIds = params.cipherIds ?? []; this.permanent = params.permanent; @@ -72,7 +81,12 @@ export class BulkDeleteDialogComponent { protected submit = async () => { const deletePromises: Promise<void>[] = []; if (this.cipherIds.length) { - if (!this.organization || !this.organization.canEditAnyCollection) { + const flexibleCollectionsV1Enabled = await firstValueFrom(this.flexibleCollectionsV1Enabled$); + + if ( + !this.organization || + !this.organization.canEditAllCiphers(flexibleCollectionsV1Enabled) + ) { deletePromises.push(this.deleteCiphers()); } else { deletePromises.push(this.deleteCiphersAdmin()); @@ -104,7 +118,8 @@ export class BulkDeleteDialogComponent { }; private async deleteCiphers(): Promise<any> { - const asAdmin = this.organization?.canEditAnyCollection; + const flexibleCollectionsV1Enabled = await firstValueFrom(this.flexibleCollectionsV1Enabled$); + const asAdmin = this.organization?.canEditAllCiphers(flexibleCollectionsV1Enabled); if (this.permanent) { await this.cipherService.deleteManyWithServer(this.cipherIds, asAdmin); } else { diff --git a/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts b/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts index b88b3af787..08afd09982 100644 --- a/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts @@ -1,6 +1,16 @@ -import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from "@angular/core"; +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + OnInit, + Output, +} from "@angular/core"; +import { firstValueFrom } from "rxjs"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +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 { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; @@ -17,7 +27,7 @@ import { templateUrl: "./vault-header.component.html", changeDetection: ChangeDetectionStrategy.OnPush, }) -export class VaultHeaderComponent { +export class VaultHeaderComponent implements OnInit { protected Unassigned = Unassigned; protected All = All; protected CollectionDialogTabType = CollectionDialogTabType; @@ -55,7 +65,18 @@ export class VaultHeaderComponent { /** Emits an event when the delete collection button is clicked in the header */ @Output() onDeleteCollection = new EventEmitter<void>(); - constructor(private i18nService: I18nService) {} + private flexibleCollectionsV1Enabled = false; + + constructor( + private i18nService: I18nService, + private configService: ConfigService, + ) {} + + async ngOnInit() { + this.flexibleCollectionsV1Enabled = await firstValueFrom( + this.configService.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1), + ); + } /** * The id of the organization that is currently being filtered on. @@ -137,7 +158,7 @@ export class VaultHeaderComponent { const organization = this.organizations.find( (o) => o.id === this.collection?.node.organizationId, ); - return this.collection.node.canEdit(organization); + return this.collection.node.canEdit(organization, this.flexibleCollectionsV1Enabled); } async editCollection(tab: CollectionDialogTabType): Promise<void> { diff --git a/apps/web/src/app/vault/individual-vault/vault.component.html b/apps/web/src/app/vault/individual-vault/vault.component.html index 5f90f8d440..003066dadd 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.html +++ b/apps/web/src/app/vault/individual-vault/vault.component.html @@ -50,6 +50,7 @@ [cloneableOrganizationCiphers]="false" [showAdminActions]="false" (onEvent)="onVaultItemsEvent($event)" + [flexibleCollectionsV1Enabled]="flexibleCollectionsV1Enabled$ | async" > </app-vault-items> <div diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index b9e1c553a7..2027b0102b 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -39,6 +39,7 @@ import { TokenService } from "@bitwarden/common/auth/abstractions/token.service" import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EventType } from "@bitwarden/common/enums"; +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"; @@ -144,6 +145,10 @@ export class VaultComponent implements OnInit, OnDestroy { protected selectedCollection: TreeNode<CollectionView> | undefined; protected canCreateCollections = false; protected currentSearchText$: Observable<string>; + protected flexibleCollectionsV1Enabled$ = this.configService.getFeatureFlag$( + FeatureFlag.FlexibleCollectionsV1, + false, + ); private searchText$ = new Subject<string>(); private refresh$ = new BehaviorSubject<void>(null); diff --git a/apps/web/src/app/vault/org-vault/attachments.component.ts b/apps/web/src/app/vault/org-vault/attachments.component.ts index f7ef372a2e..cf1f0796ec 100644 --- a/apps/web/src/app/vault/org-vault/attachments.component.ts +++ b/apps/web/src/app/vault/org-vault/attachments.component.ts @@ -1,8 +1,11 @@ -import { Component } from "@angular/core"; +import { Component, OnInit } from "@angular/core"; +import { firstValueFrom } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -21,10 +24,12 @@ import { AttachmentsComponent as BaseAttachmentsComponent } from "../individual- selector: "app-org-vault-attachments", templateUrl: "../individual-vault/attachments.component.html", }) -export class AttachmentsComponent extends BaseAttachmentsComponent { +export class AttachmentsComponent extends BaseAttachmentsComponent implements OnInit { viewOnly = false; organization: Organization; + private flexibleCollectionsV1Enabled = false; + constructor( cipherService: CipherService, i18nService: I18nService, @@ -36,6 +41,7 @@ export class AttachmentsComponent extends BaseAttachmentsComponent { fileDownloadService: FileDownloadService, dialogService: DialogService, billingAccountProfileStateService: BillingAccountProfileStateService, + private configService: ConfigService, ) { super( cipherService, @@ -51,14 +57,24 @@ export class AttachmentsComponent extends BaseAttachmentsComponent { ); } + async ngOnInit() { + await super.ngOnInit(); + this.flexibleCollectionsV1Enabled = await firstValueFrom( + this.configService.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1, false), + ); + } + protected async reupload(attachment: AttachmentView) { - if (this.organization.canEditAnyCollection && this.showFixOldAttachments(attachment)) { + if ( + this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled) && + this.showFixOldAttachments(attachment) + ) { await super.reuploadCipherAttachment(attachment, true); } } protected async loadCipher() { - if (!this.organization.canEditAnyCollection) { + if (!this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) { return await super.loadCipher(); } const response = await this.apiService.getCipherAdmin(this.cipherId); @@ -69,18 +85,21 @@ export class AttachmentsComponent extends BaseAttachmentsComponent { return this.cipherService.saveAttachmentWithServer( this.cipherDomain, file, - this.organization.canEditAnyCollection, + this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled), ); } protected deleteCipherAttachment(attachmentId: string) { - if (!this.organization.canEditAnyCollection) { + if (!this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) { return super.deleteCipherAttachment(attachmentId); } return this.apiService.deleteCipherAttachmentAdmin(this.cipherId, attachmentId); } protected showFixOldAttachments(attachment: AttachmentView) { - return attachment.key == null && this.organization.canEditAnyCollection; + return ( + attachment.key == null && + this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled) + ); } } diff --git a/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts b/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts index 45e56d062c..c4c67759c7 100644 --- a/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts +++ b/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts @@ -1,10 +1,12 @@ -import { Component, EventEmitter, Input, Output } from "@angular/core"; +import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core"; import { Router } from "@angular/router"; import { firstValueFrom } 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 { 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 { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; import { DialogService, SimpleDialogOptions } from "@bitwarden/components"; @@ -22,7 +24,7 @@ import { selector: "app-org-vault-header", templateUrl: "./vault-header.component.html", }) -export class VaultHeaderComponent { +export class VaultHeaderComponent implements OnInit { protected All = All; protected Unassigned = Unassigned; @@ -56,14 +58,23 @@ export class VaultHeaderComponent { protected CollectionDialogTabType = CollectionDialogTabType; protected organizations$ = this.organizationService.organizations$; + private flexibleCollectionsV1Enabled = false; + constructor( private organizationService: OrganizationService, private i18nService: I18nService, private dialogService: DialogService, private collectionAdminService: CollectionAdminService, private router: Router, + private configService: ConfigService, ) {} + async ngOnInit() { + this.flexibleCollectionsV1Enabled = await firstValueFrom( + this.configService.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1), + ); + } + get title() { const headerType = this.organization?.flexibleCollections ? this.i18nService.t("collections").toLowerCase() @@ -153,7 +164,7 @@ export class VaultHeaderComponent { } // Otherwise, check if we can edit the specified collection - return this.collection.node.canEdit(this.organization); + return this.collection.node.canEdit(this.organization, this.flexibleCollectionsV1Enabled); } addCipher() { diff --git a/apps/web/src/app/vault/org-vault/vault.component.html b/apps/web/src/app/vault/org-vault/vault.component.html index 4bec92b5db..391f412f1e 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.html +++ b/apps/web/src/app/vault/org-vault/vault.component.html @@ -54,6 +54,7 @@ [showBulkEditCollectionAccess]="organization?.flexibleCollections" [showBulkAddToCollections]="organization?.flexibleCollections" [viewingOrgVault]="true" + [flexibleCollectionsV1Enabled]="flexibleCollectionsV1Enabled" > </app-vault-items> <ng-container *ngIf="!flexibleCollectionsV1Enabled"> @@ -98,7 +99,10 @@ </bit-no-items> <collection-access-restricted *ngIf="showCollectionAccessRestricted" - [canEdit]="selectedCollection != null && selectedCollection.node.canEdit(organization)" + [canEdit]=" + selectedCollection != null && + selectedCollection.node.canEdit(organization, flexibleCollectionsV1Enabled) + " (editInfoClicked)="editCollection(selectedCollection.node, CollectionDialogTabType.Info)" > </collection-access-restricted> diff --git a/apps/web/src/app/vault/org-vault/vault.component.ts b/apps/web/src/app/vault/org-vault/vault.component.ts index a267612bd6..e4860f2dbc 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.ts +++ b/apps/web/src/app/vault/org-vault/vault.component.ts @@ -213,7 +213,7 @@ export class VaultComponent implements OnInit, OnDestroy { switchMap(async ([organization]) => { this.organization = organization; - if (!organization.canUseAdminCollections) { + if (!organization.canUseAdminCollections(this.flexibleCollectionsV1Enabled)) { await this.syncService.fullSync(false); } @@ -322,7 +322,7 @@ export class VaultComponent implements OnInit, OnDestroy { } } else { // Pre-flexible collections logic, to be removed after flexible collections is fully released - if (organization.canEditAnyCollection) { + if (organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) { ciphers = await this.cipherService.getAllFromApiForOrganization(organization.id); } else { ciphers = (await this.cipherService.getAllDecrypted()).filter( @@ -407,7 +407,8 @@ export class VaultComponent implements OnInit, OnDestroy { ]).pipe( map(([filter, collection, organization]) => { return ( - (filter.collectionId === Unassigned && !organization.canUseAdminCollections) || + (filter.collectionId === Unassigned && + !organization.canUseAdminCollections(this.flexibleCollectionsV1Enabled)) || (!organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled) && collection != undefined && !collection.node.assigned) @@ -453,11 +454,12 @@ export class VaultComponent implements OnInit, OnDestroy { map(([filter, collection, organization]) => { return ( // Filtering by unassigned, show message if not admin - (filter.collectionId === Unassigned && !organization.canUseAdminCollections) || + (filter.collectionId === Unassigned && + !organization.canUseAdminCollections(this.flexibleCollectionsV1Enabled)) || // Filtering by a collection, so show message if user is not assigned (collection != undefined && !collection.node.assigned && - !organization.canUseAdminCollections) + !organization.canUseAdminCollections(this.flexibleCollectionsV1Enabled)) ); }), shareReplay({ refCount: true, bufferSize: 1 }), @@ -480,7 +482,7 @@ export class VaultComponent implements OnInit, OnDestroy { (await firstValueFrom(allCipherMap$))[cipherId] != undefined; } else { canEditCipher = - organization.canUseAdminCollections || + organization.canUseAdminCollections(this.flexibleCollectionsV1Enabled) || (await this.cipherService.get(cipherId)) != null; } @@ -856,7 +858,7 @@ export class VaultComponent implements OnInit, OnDestroy { } try { - const asAdmin = this.organization?.canEditAnyCollection; + const asAdmin = this.organization?.canEditAnyCollection(this.flexibleCollectionsV1Enabled); await this.cipherService.restoreWithServer(c.id, asAdmin); this.platformUtilsService.showToast("success", null, this.i18nService.t("restoredItem")); this.refresh(); @@ -1143,7 +1145,7 @@ export class VaultComponent implements OnInit, OnDestroy { } protected deleteCipherWithServer(id: string, permanent: boolean) { - const asAdmin = this.organization?.canEditAnyCollection; + const asAdmin = this.organization?.canEditAllCiphers(this.flexibleCollectionsV1Enabled); return permanent ? this.cipherService.deleteWithServer(id, asAdmin) : this.cipherService.softDeleteWithServer(id, asAdmin); diff --git a/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts index 4c177a77f2..36182ed9cf 100644 --- a/libs/angular/src/vault/components/add-edit.component.ts +++ b/libs/angular/src/vault/components/add-edit.component.ts @@ -662,7 +662,7 @@ export class AddEditComponent implements OnInit, OnDestroy { // 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?.canEditAnyCollection; + orgAdmin = this.organization?.canEditAllCiphers(this.flexibleCollectionsV1Enabled); } return this.cipher.id == null @@ -671,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); } diff --git a/libs/common/src/admin-console/models/domain/organization.ts b/libs/common/src/admin-console/models/domain/organization.ts index 18b762207a..5850f4582e 100644 --- a/libs/common/src/admin-console/models/domain/organization.ts +++ b/libs/common/src/admin-console/models/domain/organization.ts @@ -188,18 +188,29 @@ export class Organization { return this.isManager || this.permissions.createNewCollections; } - get canEditAnyCollection() { - return this.isAdmin || this.permissions.editAnyCollection; + 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) + ); } - get canUseAdminCollections() { - return this.canEditAnyCollection; + canUseAdminCollections(flexibleCollectionsV1Enabled: boolean) { + return this.canEditAnyCollection(flexibleCollectionsV1Enabled); } 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 +225,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/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. From d12953ff72601737f3753f5bfda56f84ddbdc033 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 4 Apr 2024 09:49:15 +0200 Subject: [PATCH 108/351] [deps] Platform: Update @types/node to v18.19.29 (#8262) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .../native-messaging-test-runner/package-lock.json | 8 ++++---- apps/desktop/native-messaging-test-runner/package.json | 2 +- package-lock.json | 8 ++++---- package.json | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/desktop/native-messaging-test-runner/package-lock.json b/apps/desktop/native-messaging-test-runner/package-lock.json index fbf27703d2..e2961eb9ee 100644 --- a/apps/desktop/native-messaging-test-runner/package-lock.json +++ b/apps/desktop/native-messaging-test-runner/package-lock.json @@ -19,7 +19,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" } @@ -97,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" } 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/package-lock.json b/package-lock.json index 0666ae8d21..4ec09e2b7c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", @@ -10502,9 +10502,9 @@ "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" diff --git a/package.json b/package.json index 622f92f6eb..1c36865cd6 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", From 7375dc9aab2d6d6e70a5b2cdfd0b6e56132bcf19 Mon Sep 17 00:00:00 2001 From: Matt Bishop <mbishop@bitwarden.com> Date: Thu, 4 Apr 2024 08:41:39 -0400 Subject: [PATCH 109/351] Exclude not exploitable state from scanner SARIF results (#8603) --- .github/workflows/scan.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml index 878171cd17..9aa6745faa 100644 --- a/.github/workflows/scan.yml +++ b/.github/workflows/scan.yml @@ -40,7 +40,10 @@ 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@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9 From bbf19b2c5bf5d8b163e1cc18c3a4e16d98d32df7 Mon Sep 17 00:00:00 2001 From: Cesar Gonzalez <cesar.a.gonzalezcs@gmail.com> Date: Thu, 4 Apr 2024 09:08:10 -0500 Subject: [PATCH 110/351] [PM-6507] Disregard collection of form input elements when they are children of a submit button (#8123) * [PM-6507] Disregard collection of form input elements when they are children of a submit button * [PM-6507] Disregard collection of form input elements when they are children of a submit button * [PM-6507] Disregard collection of form input elements when they are children of a submit button --- .../src/autofill/content/autofill-init.ts | 4 +- .../collect-autofill-content.service.spec.ts | 51 +++++++++++-------- .../collect-autofill-content.service.ts | 19 ++++--- 3 files changed, 42 insertions(+), 32 deletions(-) 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/services/collect-autofill-content.service.spec.ts b/apps/browser/src/autofill/services/collect-autofill-content.service.spec.ts index 79cb41b9a1..22a856c25d 100644 --- a/apps/browser/src/autofill/services/collect-autofill-content.service.spec.ts +++ b/apps/browser/src/autofill/services/collect-autofill-content.service.spec.ts @@ -807,6 +807,36 @@ describe("CollectAutofillContentService", () => { }); describe("buildAutofillFieldItem", () => { + it("returns a `null` value if the field is a child of a `button[type='submit']`", async () => { + const usernameField = { + labelText: "Username", + id: "username-id", + type: "text", + }; + document.body.innerHTML = ` + <form> + <div> + <div> + <label for="${usernameField.id}">${usernameField.labelText}</label> + <button type="submit"> + <input id="${usernameField.id}" type="${usernameField.type}" /> + </button> + </div> + </div> + </form> + `; + const usernameInput = document.getElementById( + usernameField.id, + ) as ElementWithOpId<FillableFormFieldElement>; + + const autofillFieldItem = await collectAutofillContentService["buildAutofillFieldItem"]( + usernameInput, + 0, + ); + + expect(autofillFieldItem).toBeNull(); + }); + it("returns an existing autofill field item if it exists", async () => { const index = 0; const usernameField = { @@ -847,27 +877,6 @@ describe("CollectAutofillContentService", () => { /> </form> `; - document.body.innerHTML = ` - <form> - <label for="${usernameField.id}">${usernameField.labelText}</label> - <input - id="${usernameField.id}" - class="${usernameField.classes}" - name="${usernameField.name}" - type="${usernameField.type}" - maxlength="${usernameField.maxLength}" - tabindex="${usernameField.tabIndex}" - title="${usernameField.title}" - autocomplete="${usernameField.autocomplete}" - data-label="${usernameField.dataLabel}" - aria-label="${usernameField.ariaLabel}" - placeholder="${usernameField.placeholder}" - rel="${usernameField.rel}" - value="${usernameField.value}" - data-stripe="${usernameField.dataStripe}" - /> - </form> - `; const existingFieldData: AutofillField = { elementNumber: index, htmlClass: usernameField.classes, diff --git a/apps/browser/src/autofill/services/collect-autofill-content.service.ts b/apps/browser/src/autofill/services/collect-autofill-content.service.ts index 63dee7f3b1..7c49a3d988 100644 --- a/apps/browser/src/autofill/services/collect-autofill-content.service.ts +++ b/apps/browser/src/autofill/services/collect-autofill-content.service.ts @@ -92,9 +92,9 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte const { formElements, formFieldElements } = this.queryAutofillFormAndFieldElements(); const autofillFormsData: Record<string, AutofillForm> = this.buildAutofillFormsData(formElements); - const autofillFieldsData: AutofillField[] = await this.buildAutofillFieldsData( - formFieldElements as FormFieldElement[], - ); + const autofillFieldsData: AutofillField[] = ( + await this.buildAutofillFieldsData(formFieldElements as FormFieldElement[]) + ).filter((field) => !!field); this.sortAutofillFieldElementsMap(); if (!autofillFieldsData.length) { @@ -333,15 +333,18 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte * Builds an AutofillField object from the given form element. Will only return * shared field values if the element is a span element. Will not return any label * values if the element is a hidden input element. - * @param {ElementWithOpId<FormFieldElement>} element - * @param {number} index - * @returns {Promise<AutofillField>} - * @private + * + * @param element - The form field element to build the AutofillField object from + * @param index - The index of the form field element */ private buildAutofillFieldItem = async ( element: ElementWithOpId<FormFieldElement>, index: number, - ): Promise<AutofillField> => { + ): Promise<AutofillField | null> => { + if (element.closest("button[type='submit']")) { + return null; + } + element.opid = `__${index}`; const existingAutofillField = this.autofillFieldElements.get(element); From df25074bdfaafb51add323c9db7e22ecd7de52f9 Mon Sep 17 00:00:00 2001 From: Cesar Gonzalez <cesar.a.gonzalezcs@gmail.com> Date: Thu, 4 Apr 2024 09:11:31 -0500 Subject: [PATCH 111/351] [PM-7217] Clicking the "New Item" button on the inline menu breaks in Safari (#8601) --- apps/browser/src/autofill/background/overlay.background.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/browser/src/autofill/background/overlay.background.ts b/apps/browser/src/autofill/background/overlay.background.ts index 7b43756553..50fb80ef1b 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" }); } /** @@ -643,8 +641,8 @@ class OverlayBackground implements OverlayBackgroundInterface { collectionIds: cipherView.collectionIds, }); - await BrowserApi.sendMessage("inlineAutofillMenuRefreshAddEditCipher"); await this.openAddEditVaultItemPopout(sender.tab, { cipherId: cipherView.id }); + await BrowserApi.sendMessage("inlineAutofillMenuRefreshAddEditCipher"); } /** From b1abfb0a5cd48c23affb1bd52e7d17e361037ff6 Mon Sep 17 00:00:00 2001 From: Jake Fink <jfink@bitwarden.com> Date: Thu, 4 Apr 2024 10:22:41 -0400 Subject: [PATCH 112/351] [PM-5362]Create MP Service for state provider migration (#7623) * create mp and kdf service * update mp service interface to not rely on active user * rename observable methods * update crypto service with new MP service * add master password service to login strategies - make fake service for easier testing - fix crypto service tests * update auth service and finish strategies * auth request refactors * more service refactors and constructor updates * setMasterKey refactors * remove master key methods from crypto service * remove master key and hash from state service * missed fixes * create migrations and fix references * fix master key imports * default force set password reason to none * add password reset reason observable factory to service * remove kdf changes and migrate only disk data * update migration number * fix sync service deps * use disk for force set password state * fix desktop migration * fix sso test * fix tests * fix more tests * fix even more tests * fix even more tests * fix cli * remove kdf service abstraction * add missing deps for browser * fix merge conflicts * clear reset password reason on lock or logout * fix tests * fix other tests * add jsdocs to abstraction * use state provider in crypto service * inverse master password service factory * add clearOn to master password service * add parameter validation to master password service * add component level userId * add missed userId * migrate key hash * fix login strategy service * delete crypto master key from account * migrate master key encrypted user key * rename key hash to master key hash * use mp service for getMasterKeyEncryptedUserKey * fix tests --- .../auth-request-service.factory.ts | 16 +- .../key-connector-service.factory.ts | 9 + .../login-strategy-service.factory.ts | 9 + .../master-password-service.factory.ts | 42 ++++ .../user-verification-service.factory.ts | 9 + apps/browser/src/auth/popup/lock.component.ts | 3 + .../src/auth/popup/set-password.component.ts | 58 +---- apps/browser/src/auth/popup/sso.component.ts | 8 +- .../src/auth/popup/two-factor.component.ts | 6 + .../browser/src/background/main.background.ts | 23 +- .../background/nativeMessaging.background.ts | 16 +- .../vault-timeout-service.factory.ts | 12 + .../crypto-service.factory.ts | 6 + .../services/browser-crypto.service.ts | 3 + apps/cli/src/auth/commands/unlock.command.ts | 18 +- apps/cli/src/bw.ts | 16 +- apps/cli/src/commands/serve.command.ts | 2 + apps/cli/src/program.ts | 4 + apps/desktop/src/app/app.component.ts | 7 +- .../src/app/services/services.module.ts | 2 + apps/desktop/src/auth/lock.component.spec.ts | 6 + apps/desktop/src/auth/lock.component.ts | 3 + .../src/auth/set-password.component.ts | 6 + apps/desktop/src/auth/sso.component.ts | 6 + apps/desktop/src/auth/two-factor.component.ts | 6 + .../services/electron-crypto.service.spec.ts | 4 + .../services/electron-crypto.service.ts | 13 +- .../src/services/native-messaging.service.ts | 6 +- .../user-key-rotation.service.spec.ts | 14 +- .../key-rotation/user-key-rotation.service.ts | 5 +- apps/web/src/app/auth/lock.component.ts | 70 +----- apps/web/src/app/auth/sso.component.ts | 6 + apps/web/src/app/auth/two-factor.component.ts | 6 + libs/angular/jest.config.js | 10 +- .../src/auth/components/lock.component.ts | 17 +- .../auth/components/set-password.component.ts | 24 +- .../src/auth/components/sso.component.spec.ts | 15 +- .../src/auth/components/sso.component.ts | 8 +- .../components/two-factor.component.spec.ts | 16 +- .../auth/components/two-factor.component.ts | 8 +- .../update-temp-password.component.ts | 14 +- libs/angular/src/auth/guards/auth.guard.ts | 12 +- .../src/services/jslib-services.module.ts | 28 ++- .../auth-request-login.strategy.spec.ts | 25 ++- .../auth-request-login.strategy.ts | 22 +- .../login-strategies/login.strategy.spec.ts | 18 +- .../common/login-strategies/login.strategy.ts | 4 + .../password-login.strategy.spec.ts | 26 ++- .../password-login.strategy.ts | 23 +- .../sso-login.strategy.spec.ts | 22 +- .../login-strategies/sso-login.strategy.ts | 17 +- .../user-api-login.strategy.spec.ts | 13 +- .../user-api-login.strategy.ts | 9 +- .../webauthn-login.strategy.spec.ts | 11 +- .../webauthn-login.strategy.ts | 6 + .../auth-request/auth-request.service.spec.ts | 42 +++- .../auth-request/auth-request.service.ts | 18 +- .../login-strategy.service.spec.ts | 17 +- .../login-strategy.service.ts | 19 +- .../master-password.service.abstraction.ts | 67 ++++++ .../services/key-connector.service.spec.ts | 16 +- .../auth/services/key-connector.service.ts | 13 +- .../fake-master-password.service.ts | 56 +++++ .../master-password.service.ts | 125 +++++++++++ .../user-verification.service.ts | 16 +- .../platform/abstractions/crypto.service.ts | 31 --- .../platform/abstractions/state.service.ts | 30 --- .../models/domain/account-keys.spec.ts | 7 - .../src/platform/models/domain/account.ts | 10 - .../platform/services/crypto.service.spec.ts | 14 +- .../src/platform/services/crypto.service.ts | 113 +++++----- .../src/platform/services/state.service.ts | 103 --------- .../src/platform/state/state-definitions.ts | 2 + .../vault-timeout.service.spec.ts | 23 +- .../vault-timeout/vault-timeout.service.ts | 9 +- libs/common/src/state-migrations/migrate.ts | 6 +- ...-move-master-key-state-to-provider.spec.ts | 210 ++++++++++++++++++ .../55-move-master-key-state-to-provider.ts | 111 +++++++++ .../src/vault/services/sync/sync.service.ts | 12 +- 79 files changed, 1340 insertions(+), 498 deletions(-) create mode 100644 apps/browser/src/auth/background/service-factories/master-password-service.factory.ts create mode 100644 libs/common/src/auth/abstractions/master-password.service.abstraction.ts create mode 100644 libs/common/src/auth/services/master-password/fake-master-password.service.ts create mode 100644 libs/common/src/auth/services/master-password/master-password.service.ts create mode 100644 libs/common/src/state-migrations/migrations/55-move-master-key-state-to-provider.spec.ts create mode 100644 libs/common/src/state-migrations/migrations/55-move-master-key-state-to-provider.ts 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..295fedbadd 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 @@ -17,18 +17,21 @@ import { FactoryOptions, factory, } from "../../../platform/background/service-factories/factory-options"; + +import { accountServiceFactory, AccountServiceInitOptions } from "./account-service.factory"; import { - stateServiceFactory, - StateServiceInitOptions, -} from "../../../platform/background/service-factories/state-service.factory"; + internalMasterPasswordServiceFactory, + MasterPasswordServiceInitOptions, +} from "./master-password-service.factory"; type AuthRequestServiceFactoryOptions = FactoryOptions; export type AuthRequestServiceInitOptions = AuthRequestServiceFactoryOptions & AppIdServiceInitOptions & + AccountServiceInitOptions & + MasterPasswordServiceInitOptions & CryptoServiceInitOptions & - ApiServiceInitOptions & - StateServiceInitOptions; + ApiServiceInitOptions; export function authRequestServiceFactory( cache: { authRequestService?: AuthRequestServiceAbstraction } & CachedServices, @@ -41,9 +44,10 @@ 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), ), ); } 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 4a0dd07b32..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 @@ -31,6 +31,11 @@ import { 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,6 +45,8 @@ type KeyConnectorServiceFactoryOptions = FactoryOptions & { }; export type KeyConnectorServiceInitOptions = KeyConnectorServiceFactoryOptions & + AccountServiceInitOptions & + MasterPasswordServiceInitOptions & CryptoServiceInitOptions & ApiServiceInitOptions & TokenServiceInitOptions & @@ -58,6 +65,8 @@ export function keyConnectorServiceFactory( opts, async () => new KeyConnectorService( + 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/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<InternalMasterPasswordServiceAbstraction> { + return factory( + cache, + "masterPasswordService", + opts, + async () => new MasterPasswordService(await stateProviderFactory(cache, opts)), + ); +} + +export async function masterPasswordServiceFactory( + cache: { masterPasswordService?: InternalMasterPasswordServiceAbstraction } & CachedServices, + opts: MasterPasswordServiceInitOptions, +): Promise<MasterPasswordServiceAbstraction> { + return (await internalMasterPasswordServiceFactory( + cache, + opts, + )) as MasterPasswordServiceAbstraction; +} 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/lock.component.ts b/apps/browser/src/auth/popup/lock.component.ts index f232eca45a..16c32337cf 100644 --- a/apps/browser/src/auth/popup/lock.component.ts +++ b/apps/browser/src/auth/popup/lock.component.ts @@ -12,6 +12,7 @@ import { InternalPolicyService } from "@bitwarden/common/admin-console/abstracti 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"; @@ -41,6 +42,7 @@ export class LockComponent extends BaseLockComponent { fido2PopoutSessionData$ = fido2PopoutSessionData$(); constructor( + masterPasswordService: InternalMasterPasswordServiceAbstraction, router: Router, i18nService: I18nService, platformUtilsService: PlatformUtilsService, @@ -66,6 +68,7 @@ export class LockComponent extends BaseLockComponent { accountService: AccountService, ) { super( + masterPasswordService, router, i18nService, platformUtilsService, 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 228c7401fd..14df0d1752 100644 --- a/apps/browser/src/auth/popup/sso.component.ts +++ b/apps/browser/src/auth/popup/sso.component.ts @@ -9,7 +9,9 @@ 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 { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -45,7 +47,9 @@ export class SsoComponent extends BaseSsoComponent { logService: LogService, userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, configService: ConfigService, - protected authService: AuthService, + 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 dd541f63f8..9424369971 100644 --- a/apps/browser/src/auth/popup/two-factor.component.ts +++ b/apps/browser/src/auth/popup/two-factor.component.ts @@ -11,6 +11,8 @@ 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 { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; @@ -62,6 +64,8 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { configService: ConfigService, ssoLoginService: SsoLoginServiceAbstraction, private dialogService: DialogService, + masterPasswordService: InternalMasterPasswordServiceAbstraction, + accountService: AccountService, @Inject(WINDOW) protected win: Window, private browserMessagingApi: ZonedMessageListenerService, ) { @@ -82,6 +86,8 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { 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/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 255538de52..ed7e8dc100 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -32,6 +32,7 @@ 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 { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service"; @@ -46,6 +47,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 { MasterPasswordService } from "@bitwarden/common/auth/services/master-password/master-password.service"; import { SsoLoginService } from "@bitwarden/common/auth/services/sso-login.service"; import { TokenService } from "@bitwarden/common/auth/services/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/services/two-factor.service"; @@ -242,6 +244,7 @@ export default class MainBackground { keyGenerationService: KeyGenerationServiceAbstraction; cryptoService: CryptoServiceAbstraction; cryptoFunctionService: CryptoFunctionServiceAbstraction; + masterPasswordService: InternalMasterPasswordServiceAbstraction; tokenService: TokenServiceAbstraction; appIdService: AppIdServiceAbstraction; apiService: ApiServiceAbstraction; @@ -480,8 +483,11 @@ export default class MainBackground { const themeStateService = new DefaultThemeStateService(this.globalStateProvider); + this.masterPasswordService = new MasterPasswordService(this.stateProvider); + this.i18nService = new I18nService(BrowserApi.getUILanguage(), this.globalStateProvider); this.cryptoService = new BrowserCryptoService( + this.masterPasswordService, this.keyGenerationService, this.cryptoFunctionService, this.encryptService, @@ -525,6 +531,8 @@ export default class MainBackground { this.badgeSettingsService = new BadgeSettingsService(this.stateProvider); this.policyApiService = new PolicyApiService(this.policyService, this.apiService); this.keyConnectorService = new KeyConnectorService( + this.accountService, + this.masterPasswordService, this.cryptoService, this.apiService, this.tokenService, @@ -578,9 +586,10 @@ export default class MainBackground { this.authRequestService = new AuthRequestService( this.appIdService, + this.accountService, + this.masterPasswordService, this.cryptoService, this.apiService, - this.stateService, ); this.authService = new AuthService( @@ -597,6 +606,8 @@ export default class MainBackground { ); this.loginStrategyService = new LoginStrategyService( + this.accountService, + this.masterPasswordService, this.cryptoService, this.apiService, this.tokenService, @@ -672,6 +683,8 @@ export default class MainBackground { this.userVerificationService = new UserVerificationService( this.stateService, this.cryptoService, + this.accountService, + this.masterPasswordService, this.i18nService, this.userVerificationApiService, this.userDecryptionOptionsService, @@ -694,6 +707,8 @@ export default class MainBackground { this.vaultSettingsService = new VaultSettingsService(this.stateProvider); this.vaultTimeoutService = new VaultTimeoutService( + this.accountService, + this.masterPasswordService, this.cipherService, this.folderService, this.collectionService, @@ -729,6 +744,8 @@ export default class MainBackground { this.providerService = new ProviderService(this.stateProvider); this.syncService = new SyncService( + this.masterPasswordService, + this.accountService, this.apiService, this.domainSettingsService, this.folderService, @@ -878,6 +895,8 @@ export default class MainBackground { this.fido2Service, ); this.nativeMessagingBackground = new NativeMessagingBackground( + this.accountService, + this.masterPasswordService, this.cryptoService, this.cryptoFunctionService, this.runtimeBackground, @@ -1106,7 +1125,7 @@ export default class MainBackground { const status = await this.authService.getAuthStatus(userId); const forcePasswordReset = - (await this.stateService.getForceSetPasswordReason({ userId: userId })) != + (await firstValueFrom(this.masterPasswordService.forceSetPasswordReason$(userId))) != ForceSetPasswordReason.None; await this.systemService.clearPendingClipboard(); diff --git a/apps/browser/src/background/nativeMessaging.background.ts b/apps/browser/src/background/nativeMessaging.background.ts index 240fb1dede..faf2e6e2cc 100644 --- a/apps/browser/src/background/nativeMessaging.background.ts +++ b/apps/browser/src/background/nativeMessaging.background.ts @@ -1,6 +1,8 @@ import { firstValueFrom } from "rxjs"; +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 { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; @@ -71,6 +73,8 @@ export class NativeMessagingBackground { private validatingFingerprint: boolean; constructor( + private accountService: AccountService, + private masterPasswordService: InternalMasterPasswordServiceAbstraction, private cryptoService: CryptoService, private cryptoFunctionService: CryptoFunctionService, private runtimeBackground: RuntimeBackground, @@ -336,10 +340,14 @@ export class NativeMessagingBackground { ) as UserKey; await this.cryptoService.setUserKey(userKey); } else if (message.keyB64) { + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; // Backwards compatibility to support cases in which the user hasn't updated their desktop app // TODO: Remove after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3472) - let encUserKey = await this.stateService.getEncryptedCryptoSymmetricKey(); - encUserKey ||= await this.stateService.getMasterKeyEncryptedUserKey(); + const encUserKeyPrim = await this.stateService.getEncryptedCryptoSymmetricKey(); + const encUserKey = + encUserKeyPrim != null + ? new EncString(encUserKeyPrim) + : await this.masterPasswordService.getMasterKeyEncryptedUserKey(userId); if (!encUserKey) { throw new Error("No encrypted user key found"); } @@ -348,9 +356,9 @@ export class NativeMessagingBackground { ) as MasterKey; const userKey = await this.cryptoService.decryptUserKeyWithMasterKey( masterKey, - new EncString(encUserKey), + encUserKey, ); - await this.cryptoService.setMasterKey(masterKey); + await this.masterPasswordService.setMasterKey(masterKey, userId); await this.cryptoService.setUserKey(userKey); } else { throw new Error("No key received"); diff --git a/apps/browser/src/background/service-factories/vault-timeout-service.factory.ts b/apps/browser/src/background/service-factories/vault-timeout-service.factory.ts index 0e4d1420da..14f055114b 100644 --- a/apps/browser/src/background/service-factories/vault-timeout-service.factory.ts +++ b/apps/browser/src/background/service-factories/vault-timeout-service.factory.ts @@ -1,9 +1,17 @@ import { VaultTimeoutService as AbstractVaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; +import { + accountServiceFactory, + AccountServiceInitOptions, +} from "../../auth/background/service-factories/account-service.factory"; import { authServiceFactory, AuthServiceInitOptions, } from "../../auth/background/service-factories/auth-service.factory"; +import { + internalMasterPasswordServiceFactory, + MasterPasswordServiceInitOptions, +} from "../../auth/background/service-factories/master-password-service.factory"; import { CryptoServiceInitOptions, cryptoServiceFactory, @@ -57,6 +65,8 @@ type VaultTimeoutServiceFactoryOptions = FactoryOptions & { }; export type VaultTimeoutServiceInitOptions = VaultTimeoutServiceFactoryOptions & + AccountServiceInitOptions & + MasterPasswordServiceInitOptions & CipherServiceInitOptions & FolderServiceInitOptions & CollectionServiceInitOptions & @@ -79,6 +89,8 @@ export function vaultTimeoutServiceFactory( opts, async () => new VaultTimeoutService( + await accountServiceFactory(cache, opts), + await internalMasterPasswordServiceFactory(cache, opts), await cipherServiceFactory(cache, opts), await folderServiceFactory(cache, opts), await collectionServiceFactory(cache, opts), diff --git a/apps/browser/src/platform/background/service-factories/crypto-service.factory.ts b/apps/browser/src/platform/background/service-factories/crypto-service.factory.ts index 97614660d1..ed4fde162c 100644 --- a/apps/browser/src/platform/background/service-factories/crypto-service.factory.ts +++ b/apps/browser/src/platform/background/service-factories/crypto-service.factory.ts @@ -4,6 +4,10 @@ import { AccountServiceInitOptions, accountServiceFactory, } from "../../../auth/background/service-factories/account-service.factory"; +import { + internalMasterPasswordServiceFactory, + MasterPasswordServiceInitOptions, +} from "../../../auth/background/service-factories/master-password-service.factory"; import { StateServiceInitOptions, stateServiceFactory, @@ -34,6 +38,7 @@ import { StateProviderInitOptions, stateProviderFactory } from "./state-provider type CryptoServiceFactoryOptions = FactoryOptions; export type CryptoServiceInitOptions = CryptoServiceFactoryOptions & + MasterPasswordServiceInitOptions & KeyGenerationServiceInitOptions & CryptoFunctionServiceInitOptions & EncryptServiceInitOptions & @@ -53,6 +58,7 @@ export function cryptoServiceFactory( opts, async () => new BrowserCryptoService( + await internalMasterPasswordServiceFactory(cache, opts), await keyGenerationServiceFactory(cache, opts), await cryptoFunctionServiceFactory(cache, opts), await encryptServiceFactory(cache, opts), diff --git a/apps/browser/src/platform/services/browser-crypto.service.ts b/apps/browser/src/platform/services/browser-crypto.service.ts index 969dbdf761..d7533a22d6 100644 --- a/apps/browser/src/platform/services/browser-crypto.service.ts +++ b/apps/browser/src/platform/services/browser-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"; @@ -17,6 +18,7 @@ import { UserKey } from "@bitwarden/common/types/key"; export class BrowserCryptoService extends CryptoService { constructor( + masterPasswordService: InternalMasterPasswordServiceAbstraction, keyGenerationService: KeyGenerationService, cryptoFunctionService: CryptoFunctionService, encryptService: EncryptService, @@ -28,6 +30,7 @@ export class BrowserCryptoService extends CryptoService { private biometricStateService: BiometricStateService, ) { super( + masterPasswordService, keyGenerationService, cryptoFunctionService, encryptService, diff --git a/apps/cli/src/auth/commands/unlock.command.ts b/apps/cli/src/auth/commands/unlock.command.ts index 98bc926079..6f7dea2074 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,8 @@ export class UnlockCommand { } if (passwordValid) { - await this.cryptoService.setMasterKey(masterKey); + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + 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 3815fc773b..3659859f73 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -28,6 +28,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv 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"; @@ -168,6 +169,7 @@ export class Main { organizationUserService: OrganizationUserService; collectionService: CollectionService; vaultTimeoutService: VaultTimeoutService; + masterPasswordService: InternalMasterPasswordServiceAbstraction; vaultTimeoutSettingsService: VaultTimeoutSettingsService; syncService: SyncService; eventCollectionService: EventCollectionServiceAbstraction; @@ -352,6 +354,7 @@ export class Main { ); this.cryptoService = new CryptoService( + this.masterPasswordService, this.keyGenerationService, this.cryptoFunctionService, this.encryptService, @@ -432,6 +435,8 @@ export class Main { this.policyApiService = new PolicyApiService(this.policyService, this.apiService); this.keyConnectorService = new KeyConnectorService( + this.accountService, + this.masterPasswordService, this.cryptoService, this.apiService, this.tokenService, @@ -471,9 +476,10 @@ export class Main { this.authRequestService = new AuthRequestService( this.appIdService, + this.accountService, + this.masterPasswordService, this.cryptoService, this.apiService, - this.stateService, ); this.billingAccountProfileStateService = new DefaultBillingAccountProfileStateService( @@ -481,6 +487,8 @@ export class Main { ); this.loginStrategyService = new LoginStrategyService( + this.accountService, + this.masterPasswordService, this.cryptoService, this.apiService, this.tokenService, @@ -568,6 +576,8 @@ export class Main { this.userVerificationService = new UserVerificationService( this.stateService, this.cryptoService, + this.accountService, + this.masterPasswordService, this.i18nService, this.userVerificationApiService, this.userDecryptionOptionsService, @@ -578,6 +588,8 @@ export class Main { ); this.vaultTimeoutService = new VaultTimeoutService( + this.accountService, + this.masterPasswordService, this.cipherService, this.folderService, this.collectionService, @@ -596,6 +608,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, 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/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/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index 4e74135c49..f060d5f854 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -26,6 +26,7 @@ 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"; @@ -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, @@ -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) { diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index 84932ce7d9..8e412d4977 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -20,6 +20,7 @@ import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaul 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 { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { BroadcasterService as BroadcasterServiceAbstraction } from "@bitwarden/common/platform/abstractions/broadcaster.service"; @@ -228,6 +229,7 @@ const safeProviders: SafeProvider[] = [ provide: CryptoServiceAbstraction, useClass: ElectronCryptoService, deps: [ + InternalMasterPasswordServiceAbstraction, KeyGenerationServiceAbstraction, CryptoFunctionServiceAbstraction, EncryptService, diff --git a/apps/desktop/src/auth/lock.component.spec.ts b/apps/desktop/src/auth/lock.component.spec.ts index 0339889bf7..c125eba022 100644 --- a/apps/desktop/src/auth/lock.component.spec.ts +++ b/apps/desktop/src/auth/lock.component.spec.ts @@ -14,7 +14,9 @@ import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abs 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 { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; @@ -52,6 +54,7 @@ describe("LockComponent", () => { let broadcasterServiceMock: MockProxy<BroadcasterService>; let platformUtilsServiceMock: MockProxy<PlatformUtilsService>; let activatedRouteMock: MockProxy<ActivatedRoute>; + let mockMasterPasswordService: FakeMasterPasswordService; const mockUserId = Utils.newGuid() as UserId; const accountService: FakeAccountService = mockAccountServiceWith(mockUserId); @@ -67,6 +70,8 @@ describe("LockComponent", () => { activatedRouteMock = mock<ActivatedRoute>(); activatedRouteMock.queryParams = mock<ActivatedRoute["queryParams"]>(); + mockMasterPasswordService = new FakeMasterPasswordService(); + biometricStateService.dismissedRequirePasswordOnStartCallout$ = of(false); biometricStateService.promptAutomatically$ = of(false); biometricStateService.promptCancelled$ = of(false); @@ -74,6 +79,7 @@ describe("LockComponent", () => { await TestBed.configureTestingModule({ declarations: [LockComponent, I18nPipe], providers: [ + { provide: InternalMasterPasswordServiceAbstraction, useValue: mockMasterPasswordService }, { provide: I18nService, useValue: mock<I18nService>(), diff --git a/apps/desktop/src/auth/lock.component.ts b/apps/desktop/src/auth/lock.component.ts index 8b1448c06f..16b58c5bbe 100644 --- a/apps/desktop/src/auth/lock.component.ts +++ b/apps/desktop/src/auth/lock.component.ts @@ -11,6 +11,7 @@ import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abs 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"; @@ -38,6 +39,7 @@ export class LockComponent extends BaseLockComponent { private autoPromptBiometric = false; constructor( + masterPasswordService: InternalMasterPasswordServiceAbstraction, router: Router, i18nService: I18nService, platformUtilsService: PlatformUtilsService, @@ -63,6 +65,7 @@ export class LockComponent extends BaseLockComponent { accountService: AccountService, ) { super( + masterPasswordService, router, i18nService, platformUtilsService, 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 210319b9ed..cc261f1235 100644 --- a/apps/desktop/src/auth/sso.component.ts +++ b/apps/desktop/src/auth/sso.component.ts @@ -7,6 +7,8 @@ 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 { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; @@ -39,6 +41,8 @@ export class SsoComponent extends BaseSsoComponent { logService: LogService, userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, 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 fdbc52b4bf..d1b84c1fa0 100644 --- a/apps/desktop/src/auth/two-factor.component.ts +++ b/apps/desktop/src/auth/two-factor.component.ts @@ -11,6 +11,8 @@ 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 { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; @@ -60,6 +62,8 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction, configService: ConfigService, + masterPasswordService: InternalMasterPasswordServiceAbstraction, + accountService: AccountService, @Inject(WINDOW) protected win: Window, ) { super( @@ -79,6 +83,8 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { 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/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<PlatformUtilsService>(); const logService = mock<LogService>(); const stateService = mock<StateService>(); + let masterPasswordService: FakeMasterPasswordService; let accountService: FakeAccountService; let stateProvider: FakeStateProvider; const biometricStateService = mock<BiometricStateService>(); @@ -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/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<string, SymmetricCryptoKey>(); 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/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 09c7bf9ace..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,6 +2,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject } from "rxjs"; import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.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"; @@ -9,7 +10,6 @@ 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 { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; 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"; @@ -22,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"; @@ -46,8 +50,10 @@ describe("KeyRotationService", () => { const mockUserId = Utils.newGuid() as UserId; const mockAccountService: FakeAccountService = mockAccountServiceWith(mockUserId); + let mockMasterPasswordService: FakeMasterPasswordService = new FakeMasterPasswordService(); beforeAll(() => { + mockMasterPasswordService = new FakeMasterPasswordService(); mockApiService = mock<UserKeyRotationApiService>(); mockCipherService = mock<CipherService>(); mockFolderService = mock<FolderService>(); @@ -61,6 +67,7 @@ describe("KeyRotationService", () => { mockConfigService = mock<ConfigService>(); keyRotationService = new UserKeyRotationService( + mockMasterPasswordService, mockApiService, mockCipherService, mockFolderService, @@ -174,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 03bc604b4d..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 @@ -3,6 +3,7 @@ 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 { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -25,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, @@ -61,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) { diff --git a/apps/web/src/app/auth/lock.component.ts b/apps/web/src/app/auth/lock.component.ts index a1d4724396..021bf0f9df 100644 --- a/apps/web/src/app/auth/lock.component.ts +++ b/apps/web/src/app/auth/lock.component.ts @@ -1,80 +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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -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, - accountService: AccountService, - ) { - super( - router, - i18nService, - platformUtilsService, - messagingService, - cryptoService, - vaultTimeoutService, - vaultTimeoutSettingsService, - environmentService, - stateService, - apiService, - logService, - ngZone, - policyApiService, - policyService, - passwordStrengthService, - dialogService, - deviceTrustCryptoService, - userVerificationService, - pinCryptoService, - biometricStateService, - accountService, - ); - } - async ngOnInit() { await super.ngOnInit(); this.onSuccessfulSubmit = async () => { diff --git a/apps/web/src/app/auth/sso.component.ts b/apps/web/src/app/auth/sso.component.ts index cdd979aa89..e120b2749f 100644 --- a/apps/web/src/app/auth/sso.component.ts +++ b/apps/web/src/app/auth/sso.component.ts @@ -10,6 +10,8 @@ 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"; @@ -46,6 +48,8 @@ export class SsoComponent extends BaseSsoComponent { private validationService: ValidationService, userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, 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/two-factor.component.ts b/apps/web/src/app/auth/two-factor.component.ts index 65bf1dba58..eed84b91f1 100644 --- a/apps/web/src/app/auth/two-factor.component.ts +++ b/apps/web/src/app/auth/two-factor.component.ts @@ -10,6 +10,8 @@ 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 { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; @@ -50,6 +52,8 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnDest userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction, configService: ConfigService, + masterPasswordService: InternalMasterPasswordServiceAbstraction, + accountService: AccountService, @Inject(WINDOW) protected win: Window, ) { super( @@ -69,6 +73,8 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnDest userDecryptionOptionsService, ssoLoginService, configService, + masterPasswordService, + accountService, ); this.onSuccessfulLoginNavigate = this.goAfterLogIn; } 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: ["<rootDir>/test.setup.ts"], - moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, { - prefix: "<rootDir>/", - }), + moduleNameMapper: pathsToModuleNameMapper( + // lets us use @bitwarden/common/spec in tests + { "@bitwarden/common/spec": ["../common/spec"], ...(compilerOptions?.paths ?? {}) }, + { + prefix: "<rootDir>/", + }, + ), }; diff --git a/libs/angular/src/auth/components/lock.component.ts b/libs/angular/src/auth/components/lock.component.ts index aa3b801ded..6602a917c9 100644 --- a/libs/angular/src/auth/components/lock.component.ts +++ b/libs/angular/src/auth/components/lock.component.ts @@ -12,6 +12,7 @@ import { InternalPolicyService } from "@bitwarden/common/admin-console/abstracti 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"; @@ -56,6 +57,7 @@ export class LockComponent implements OnInit, OnDestroy { private destroy$ = new Subject<void>(); constructor( + protected masterPasswordService: InternalMasterPasswordServiceAbstraction, protected router: Router, protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, @@ -206,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(); @@ -215,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, @@ -244,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 { @@ -262,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); } @@ -292,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/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<void>; 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 c5c062d9a7..269ec51e30 100644 --- a/libs/angular/src/auth/components/sso.component.spec.ts +++ b/libs/angular/src/auth/components/sso.component.spec.ts @@ -12,10 +12,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 { 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 { 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"; @@ -23,7 +26,9 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic 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<TestSsoComponent>; + const userId = "userId" as UserId; // Mock Services let mockLoginStrategyService: MockProxy<LoginStrategyServiceAbstraction>; @@ -67,6 +73,8 @@ describe("SsoComponent", () => { let mockLogService: MockProxy<LogService>; let mockUserDecryptionOptionsService: MockProxy<UserDecryptionOptionsServiceAbstraction>; let mockConfigService: MockProxy<ConfigService>; + let mockMasterPasswordService: FakeMasterPasswordService; + let mockAccountService: FakeAccountService; // Mock authService.logIn params let code: string; @@ -117,6 +125,8 @@ describe("SsoComponent", () => { mockLogService = mock(); mockUserDecryptionOptionsService = mock(); mockConfigService = mock(); + mockAccountService = mockAccountServiceWith(userId); + mockMasterPasswordService = new FakeMasterPasswordService(); // Mock loginStrategyService.logIn params code = "code"; @@ -199,6 +209,8 @@ describe("SsoComponent", () => { }, { provide: LogService, useValue: mockLogService }, { 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 68d6e72e8d..30815beef8 100644 --- a/libs/angular/src/auth/components/sso.component.ts +++ b/libs/angular/src/auth/components/sso.component.ts @@ -11,6 +11,8 @@ 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"; @@ -66,6 +68,8 @@ export class SsoComponent { protected logService: LogService, protected userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, 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 bff39188ea..0eb248f6d9 100644 --- a/libs/angular/src/auth/components/two-factor.component.spec.ts +++ b/libs/angular/src/auth/components/two-factor.component.spec.ts @@ -15,11 +15,14 @@ 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 { 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 { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; @@ -27,6 +30,8 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic 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<TestTwoFactorComponent>; + const userId = "userId" as UserId; // Mock Services let mockLoginStrategyService: MockProxy<LoginStrategyServiceAbstraction>; @@ -63,6 +69,8 @@ describe("TwoFactorComponent", () => { let mockUserDecryptionOptionsService: MockProxy<UserDecryptionOptionsServiceAbstraction>; let mockSsoLoginService: MockProxy<SsoLoginServiceAbstraction>; let mockConfigService: MockProxy<ConfigService>; + let mockMasterPasswordService: FakeMasterPasswordService; + let mockAccountService: FakeAccountService; let mockUserDecryptionOpts: { noMasterPassword: UserDecryptionOptions; @@ -93,6 +101,8 @@ describe("TwoFactorComponent", () => { mockUserDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>(); mockSsoLoginService = mock<SsoLoginServiceAbstraction>(); mockConfigService = mock<ConfigService>(); + mockAccountService = mockAccountServiceWith(userId); + mockMasterPasswordService = new FakeMasterPasswordService(); mockUserDecryptionOpts = { noMasterPassword: new UserDecryptionOptions({ @@ -170,6 +180,8 @@ describe("TwoFactorComponent", () => { }, { provide: SsoLoginServiceAbstraction, useValue: mockSsoLoginService }, { provide: ConfigService, useValue: mockConfigService }, + { provide: InternalMasterPasswordServiceAbstraction, useValue: mockMasterPasswordService }, + { provide: AccountService, useValue: mockAccountService }, ], }); @@ -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 c306e6cc80..f73f0483be 100644 --- a/libs/angular/src/auth/components/two-factor.component.ts +++ b/libs/angular/src/auth/components/two-factor.component.ts @@ -14,6 +14,8 @@ 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 { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type"; @@ -92,6 +94,8 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI protected userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, protected ssoLoginService: SsoLoginServiceAbstraction, protected configService: ConfigService, + protected masterPasswordService: InternalMasterPasswordServiceAbstraction, + protected accountService: AccountService, ) { super(environmentService, i18nService, platformUtilsService); this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win); @@ -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/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 73f2bb4a32..2e35f0d62f 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -60,6 +60,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 { + 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,6 +82,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 { 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"; @@ -359,6 +364,8 @@ const safeProviders: SafeProvider[] = [ provide: LoginStrategyServiceAbstraction, useClass: LoginStrategyService, deps: [ + AccountServiceAbstraction, + InternalMasterPasswordServiceAbstraction, CryptoServiceAbstraction, ApiServiceAbstraction, TokenServiceAbstraction, @@ -521,6 +528,7 @@ const safeProviders: SafeProvider[] = [ provide: CryptoServiceAbstraction, useClass: CryptoService, deps: [ + InternalMasterPasswordServiceAbstraction, KeyGenerationServiceAbstraction, CryptoFunctionServiceAbstraction, EncryptService, @@ -587,6 +595,8 @@ const safeProviders: SafeProvider[] = [ provide: SyncServiceAbstraction, useClass: SyncService, deps: [ + InternalMasterPasswordServiceAbstraction, + AccountServiceAbstraction, ApiServiceAbstraction, DomainSettingsService, InternalFolderService, @@ -626,6 +636,8 @@ const safeProviders: SafeProvider[] = [ provide: VaultTimeoutService, useClass: VaultTimeoutService, deps: [ + AccountServiceAbstraction, + InternalMasterPasswordServiceAbstraction, CipherServiceAbstraction, FolderServiceAbstraction, CollectionServiceAbstraction, @@ -771,10 +783,21 @@ const safeProviders: SafeProvider[] = [ useClass: PolicyApiService, deps: [InternalPolicyService, ApiServiceAbstraction], }), + safeProvider({ + provide: InternalMasterPasswordServiceAbstraction, + useClass: MasterPasswordService, + deps: [StateProvider], + }), + safeProvider({ + provide: MasterPasswordServiceAbstraction, + useExisting: MasterPasswordServiceAbstraction, + }), safeProvider({ provide: KeyConnectorServiceAbstraction, useClass: KeyConnectorService, deps: [ + AccountServiceAbstraction, + InternalMasterPasswordServiceAbstraction, CryptoServiceAbstraction, ApiServiceAbstraction, TokenServiceAbstraction, @@ -791,6 +814,8 @@ const safeProviders: SafeProvider[] = [ deps: [ StateServiceAbstraction, CryptoServiceAbstraction, + AccountServiceAbstraction, + InternalMasterPasswordServiceAbstraction, I18nServiceAbstraction, UserVerificationApiServiceAbstraction, UserDecryptionOptionsServiceAbstraction, @@ -934,9 +959,10 @@ const safeProviders: SafeProvider[] = [ useClass: AuthRequestService, deps: [ AppIdServiceAbstraction, + AccountServiceAbstraction, + InternalMasterPasswordServiceAbstraction, CryptoServiceAbstraction, ApiServiceAbstraction, - StateServiceAbstraction, ], }), safeProvider({ 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<DeviceTrustCryptoServiceAbstraction>; let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>; + 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<DeviceTrustCryptoServiceAbstraction>(); billingAccountProfileStateService = mock<BillingAccountProfileStateService>(); + 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 31a0cebbfe..e47f0f88ee 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"; @@ -47,6 +49,8 @@ export class AuthRequestLoginStrategy extends LoginStrategy { constructor( data: AuthRequestLoginStrategyData, + accountService: AccountService, + masterPasswordService: InternalMasterPasswordServiceAbstraction, cryptoService: CryptoService, apiService: ApiService, tokenService: TokenService, @@ -61,6 +65,8 @@ export class AuthRequestLoginStrategy extends LoginStrategy { billingAccountProfileStateService: BillingAccountProfileStateService, ) { super( + accountService, + masterPasswordService, cryptoService, apiService, tokenService, @@ -114,8 +120,15 @@ 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, + ); } } @@ -137,7 +150,8 @@ export class AuthRequestLoginStrategy extends LoginStrategy { } private async trySetUserKeyWithMasterKey(): Promise<void> { - 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 0ac22047c5..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,11 +32,13 @@ 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 { UserId } from "@bitwarden/common/types/guid"; import { UserKey, MasterKey } from "@bitwarden/common/types/key"; import { LoginStrategyServiceAbstraction } from "../abstractions"; @@ -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<LoginStrategyServiceAbstraction>; let cryptoService: MockProxy<CryptoService>; @@ -118,6 +123,9 @@ describe("LoginStrategy", () => { let credentials: PasswordLoginCredentials; beforeEach(async () => { + accountService = mockAccountServiceWith(userId); + masterPasswordService = new FakeMasterPasswordService(); + loginStrategyService = mock<LoginStrategyServiceAbstraction>(); cryptoService = mock<CryptoService>(); apiService = mock<ApiService>(); @@ -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, @@ -241,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); @@ -260,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); @@ -382,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 4fe99b276c..df6aa171db 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"; @@ -60,6 +62,8 @@ export abstract class LoginStrategy { protected abstract cache: BehaviorSubject<LoginStrategyData>; constructor( + protected accountService: AccountService, + protected masterPasswordService: InternalMasterPasswordServiceAbstraction, protected cryptoService: CryptoService, protected apiService: ApiService, protected tokenService: TokenService, 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<LoginStrategyServiceAbstraction>; let cryptoService: MockProxy<CryptoService>; @@ -71,6 +77,9 @@ describe("PasswordLoginStrategy", () => { let tokenResponse: IdentityTokenResponse; beforeEach(async () => { + accountService = mockAccountServiceWith(userId); + masterPasswordService = new FakeMasterPasswordService(); + loginStrategyService = mock<LoginStrategyServiceAbstraction>(); cryptoService = mock<CryptoService>(); apiService = mock<ApiService>(); @@ -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 d3de3ea6ba..52c97d5d85 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"; @@ -70,6 +72,8 @@ export class PasswordLoginStrategy extends LoginStrategy { constructor( data: PasswordLoginStrategyData, + accountService: AccountService, + masterPasswordService: InternalMasterPasswordServiceAbstraction, cryptoService: CryptoService, apiService: ApiService, tokenService: TokenService, @@ -86,6 +90,8 @@ export class PasswordLoginStrategy extends LoginStrategy { billingAccountProfileStateService: BillingAccountProfileStateService, ) { super( + accountService, + masterPasswordService, cryptoService, apiService, tokenService, @@ -157,8 +163,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; } @@ -184,7 +192,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,8 +202,9 @@ 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<void> { @@ -204,7 +214,8 @@ export class PasswordLoginStrategy extends LoginStrategy { } await this.cryptoService.setMasterKeyEncryptedUserKey(response.key); - 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/sso-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts index d4b0b13eaf..bce62681d0 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<CryptoService>; let apiService: MockProxy<ApiService>; let tokenService: MockProxy<TokenService>; @@ -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<CryptoService>(); apiService = mock<ApiService>(); tokenService = mock<TokenService>(); @@ -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(); }); @@ -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 7745104bd1..db0228a338 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"; @@ -79,6 +81,8 @@ export class SsoLoginStrategy extends LoginStrategy { constructor( data: SsoLoginStrategyData, + accountService: AccountService, + masterPasswordService: InternalMasterPasswordServiceAbstraction, cryptoService: CryptoService, apiService: ApiService, tokenService: TokenService, @@ -96,6 +100,8 @@ export class SsoLoginStrategy extends LoginStrategy { billingAccountProfileStateService: BillingAccountProfileStateService, ) { super( + accountService, + masterPasswordService, cryptoService, apiService, tokenService, @@ -138,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({ @@ -323,7 +333,8 @@ export class SsoLoginStrategy extends LoginStrategy { } private async trySetUserKeyWithMasterKey(): Promise<void> { - 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<CryptoService>; let apiService: MockProxy<ApiService>; @@ -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<CryptoService>(); apiService = mock<ApiService>(); tokenService = mock<TokenService>(); @@ -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..421746b49c 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"; @@ -39,6 +41,8 @@ export class UserApiLoginStrategy extends LoginStrategy { constructor( data: UserApiLoginStrategyData, + accountService: AccountService, + masterPasswordService: InternalMasterPasswordServiceAbstraction, cryptoService: CryptoService, apiService: ApiService, tokenService: TokenService, @@ -54,6 +58,8 @@ export class UserApiLoginStrategy extends LoginStrategy { billingAccountProfileStateService: BillingAccountProfileStateService, ) { super( + accountService, + masterPasswordService, cryptoService, apiService, tokenService, @@ -95,7 +101,8 @@ export class UserApiLoginStrategy extends LoginStrategy { 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); 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<CryptoService>; let apiService!: MockProxy<ApiService>; @@ -63,6 +67,9 @@ describe("WebAuthnLoginStrategy", () => { beforeEach(() => { jest.clearAllMocks(); + accountService = new FakeAccountService(null); + masterPasswordService = new FakeMasterPasswordService(); + cryptoService = mock<CryptoService>(); apiService = mock<ApiService>(); tokenService = mock<TokenService>(); @@ -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..843978e2a2 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"; @@ -41,6 +43,8 @@ export class WebAuthnLoginStrategy extends LoginStrategy { constructor( data: WebAuthnLoginStrategyData, + accountService: AccountService, + masterPasswordService: InternalMasterPasswordServiceAbstraction, cryptoService: CryptoService, apiService: ApiService, tokenService: TokenService, @@ -54,6 +58,8 @@ export class WebAuthnLoginStrategy extends LoginStrategy { billingAccountProfileStateService: BillingAccountProfileStateService, ) { super( + accountService, + masterPasswordService, cryptoService, apiService, tokenService, 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 80d00b2a01..f04628ffd9 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,13 +2,15 @@ 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 { 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"; @@ -16,17 +18,27 @@ import { AuthRequestService } from "./auth-request.service"; describe("AuthRequestService", () => { let sut: AuthRequestService; + let accountService: FakeAccountService; + let masterPasswordService: FakeMasterPasswordService; const appIdService = mock<AppIdService>(); const cryptoService = mock<CryptoService>(); const apiService = mock<ApiService>(); - const stateService = mock<StateService>(); 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, + ); mockPrivateKey = new Uint8Array(64); }); @@ -67,8 +79,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, @@ -130,8 +142,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); @@ -144,10 +156,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 eb39659f53..5f8dcfd729 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,12 +1,13 @@ -import { Observable, Subject } from "rxjs"; +import { firstValueFrom, Observable, Subject } 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 { 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 { MasterKey, UserKey } from "@bitwarden/common/types/key"; @@ -19,9 +20,10 @@ export class AuthRequestService implements AuthRequestServiceAbstraction { constructor( private appIdService: AppIdService, + private accountService: AccountService, + private masterPasswordService: InternalMasterPasswordServiceAbstraction, private cryptoService: CryptoService, private apiService: ApiService, - private stateService: StateService, ) { this.authRequestPushNotification$ = this.authRequestPushNotificationSubject.asObservable(); } @@ -38,8 +40,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; @@ -92,8 +95,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); } 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<CryptoService>; let apiService: MockProxy<ApiService>; let tokenService: MockProxy<TokenService>; @@ -61,7 +70,11 @@ describe("LoginStrategyService", () => { let stateProvider: FakeGlobalStateProvider; let loginStrategyCacheExpirationState: FakeGlobalState<Date | null>; + const userId = "USER_ID" as UserId; + beforeEach(() => { + accountService = mockAccountServiceWith(userId); + masterPasswordService = new FakeMasterPasswordService(); cryptoService = mock<CryptoService>(); apiService = mock<ApiService>(); tokenService = mock<TokenService>(); @@ -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 b55f38af7f..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 @@ -9,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"; @@ -81,6 +83,8 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { currentAuthType$: Observable<AuthenticationType | null>; constructor( + protected accountService: AccountService, + protected masterPasswordService: InternalMasterPasswordServiceAbstraction, protected cryptoService: CryptoService, protected apiService: ApiService, protected tokenService: TokenService, @@ -257,7 +261,8 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { ): Promise<AuthRequestResponse> { 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; @@ -266,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), @@ -333,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, @@ -351,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, @@ -370,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, @@ -387,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, @@ -403,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/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..44fda403c6 --- /dev/null +++ b/libs/common/src/auth/abstractions/master-password.service.abstraction.ts @@ -0,0 +1,67 @@ +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<ForceSetPasswordReason>; + /** + * 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<MasterKey>; + /** + * 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<string>; + /** + * 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<EncString>; +} + +export abstract class InternalMasterPasswordServiceAbstraction extends MasterPasswordServiceAbstraction { + /** + * Set the master key for the user. + * @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<void>; + /** + * Set the master key hash for the user. + * @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<void>; + /** + * 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<void>; + /** + * 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<void>; +} diff --git a/libs/common/src/auth/services/key-connector.service.spec.ts b/libs/common/src/auth/services/key-connector.service.spec.ts index 50fed856f9..e3e5fbdbe7 100644 --- a/libs/common/src/auth/services/key-connector.service.spec.ts +++ b/libs/common/src/auth/services/key-connector.service.spec.ts @@ -21,6 +21,7 @@ import { 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", () => { @@ -36,6 +37,7 @@ describe("KeyConnectorService", () => { let stateProvider: FakeStateProvider; let accountService: FakeAccountService; + let masterPasswordService: FakeMasterPasswordService; const mockUserId = Utils.newGuid() as UserId; const mockOrgId = Utils.newGuid() as OrganizationId; @@ -47,10 +49,13 @@ describe("KeyConnectorService", () => { beforeEach(() => { jest.clearAllMocks(); + masterPasswordService = new FakeMasterPasswordService(); accountService = mockAccountServiceWith(mockUserId); stateProvider = new FakeStateProvider(accountService); keyConnectorService = new KeyConnectorService( + accountService, + masterPasswordService, cryptoService, apiService, tokenService, @@ -214,7 +219,10 @@ describe("KeyConnectorService", () => { // Assert expect(apiService.getMasterKeyFromKeyConnector).toHaveBeenCalledWith(url); - expect(cryptoService.setMasterKey).toHaveBeenCalledWith(masterKey); + expect(masterPasswordService.mock.setMasterKey).toHaveBeenCalledWith( + masterKey, + expect.any(String), + ); }); it("should handle errors thrown during the process", async () => { @@ -241,10 +249,10 @@ describe("KeyConnectorService", () => { // 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(cryptoService, "getMasterKey").mockResolvedValue(masterKey); jest.spyOn(apiService, "postUserKeyToKeyConnector").mockResolvedValue(); // Act @@ -252,7 +260,6 @@ describe("KeyConnectorService", () => { // Assert expect(keyConnectorService.getManagingOrganization).toHaveBeenCalled(); - expect(cryptoService.getMasterKey).toHaveBeenCalled(); expect(apiService.postUserKeyToKeyConnector).toHaveBeenCalledWith( organization.keyConnectorUrl, keyConnectorRequest, @@ -268,8 +275,8 @@ describe("KeyConnectorService", () => { 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(cryptoService, "getMasterKey").mockResolvedValue(masterKey); jest.spyOn(apiService, "postUserKeyToKeyConnector").mockRejectedValue(error); jest.spyOn(logService, "error"); @@ -280,7 +287,6 @@ describe("KeyConnectorService", () => { // Assert expect(logService.error).toHaveBeenCalledWith(error); expect(keyConnectorService.getManagingOrganization).toHaveBeenCalled(); - expect(cryptoService.getMasterKey).toHaveBeenCalled(); expect(apiService.postUserKeyToKeyConnector).toHaveBeenCalledWith( organization.keyConnectorUrl, keyConnectorRequest, diff --git a/libs/common/src/auth/services/key-connector.service.ts b/libs/common/src/auth/services/key-connector.service.ts index d1502ce06c..f8e523cce4 100644 --- a/libs/common/src/auth/services/key-connector.service.ts +++ b/libs/common/src/auth/services/key-connector.service.ts @@ -16,7 +16,9 @@ import { 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"; @@ -45,6 +47,8 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { private usesKeyConnectorState: ActiveUserState<boolean>; private convertAccountToKeyConnectorState: ActiveUserState<boolean>; constructor( + private accountService: AccountService, + private masterPasswordService: InternalMasterPasswordServiceAbstraction, private cryptoService: CryptoService, private apiService: ApiService, private tokenService: TokenService, @@ -78,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 { @@ -99,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); } @@ -136,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]); 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..f060fe1db1 --- /dev/null +++ b/libs/common/src/auth/services/master-password/fake-master-password.service.ts @@ -0,0 +1,56 @@ +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<InternalMasterPasswordServiceAbstraction>(); + + // eslint-disable-next-line rxjs/no-exposed-subjects -- test class + masterKeySubject = new ReplaySubject<MasterKey>(1); + // eslint-disable-next-line rxjs/no-exposed-subjects -- test class + masterKeyHashSubject = new ReplaySubject<string>(1); + // eslint-disable-next-line rxjs/no-exposed-subjects -- test class + forceSetPasswordReasonSubject = new ReplaySubject<ForceSetPasswordReason>(1); + + constructor(initialMasterKey?: MasterKey, initialMasterKeyHash?: string) { + this.masterKeySubject.next(initialMasterKey); + this.masterKeyHashSubject.next(initialMasterKeyHash); + } + + masterKey$(userId: UserId): Observable<MasterKey> { + return this.masterKeySubject.asObservable(); + } + + setMasterKey(masterKey: MasterKey, userId: UserId): Promise<void> { + return this.mock.setMasterKey(masterKey, userId); + } + + masterKeyHash$(userId: UserId): Observable<string> { + return this.masterKeyHashSubject.asObservable(); + } + + getMasterKeyEncryptedUserKey(userId: UserId): Promise<EncString> { + return this.mock.getMasterKeyEncryptedUserKey(userId); + } + + setMasterKeyEncryptedUserKey(encryptedKey: EncString, userId: UserId): Promise<void> { + return this.mock.setMasterKeyEncryptedUserKey(encryptedKey, userId); + } + + setMasterKeyHash(masterKeyHash: string, userId: UserId): Promise<void> { + return this.mock.setMasterKeyHash(masterKeyHash, userId); + } + + forceSetPasswordReason$(userId: UserId): Observable<ForceSetPasswordReason> { + return this.forceSetPasswordReasonSubject.asObservable(); + } + + setForceSetPasswordReason(reason: ForceSetPasswordReason, userId: UserId): Promise<void> { + 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..d204a26570 --- /dev/null +++ b/libs/common/src/auth/services/master-password/master-password.service.ts @@ -0,0 +1,125 @@ +import { firstValueFrom, map, Observable } from "rxjs"; + +import { 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<MasterKey>(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<string>(MASTER_PASSWORD_DISK, "masterKeyHash", { + deserializer: (masterKeyHash) => masterKeyHash, + clearOn: ["logout"], +}); + +const MASTER_KEY_ENCRYPTED_USER_KEY = new UserKeyDefinition<EncString>( + MASTER_PASSWORD_DISK, + "masterKeyEncryptedUserKey", + { + deserializer: (key) => EncString.fromJSON(key), + clearOn: ["logout"], + }, +); + +/** Disk to persist through lock and account switches */ +const FORCE_SET_PASSWORD_REASON = new UserKeyDefinition<ForceSetPasswordReason>( + MASTER_PASSWORD_DISK, + "forceSetPasswordReason", + { + deserializer: (reason) => reason, + clearOn: ["logout"], + }, +); + +export class MasterPasswordService implements InternalMasterPasswordServiceAbstraction { + constructor(private stateProvider: StateProvider) {} + + masterKey$(userId: UserId): Observable<MasterKey> { + if (userId == null) { + throw new Error("User ID is required."); + } + return this.stateProvider.getUser(userId, MASTER_KEY).state$; + } + + masterKeyHash$(userId: UserId): Observable<string> { + if (userId == null) { + throw new Error("User ID is required."); + } + return this.stateProvider.getUser(userId, MASTER_KEY_HASH).state$; + } + + forceSetPasswordReason$(userId: UserId): Observable<ForceSetPasswordReason> { + 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<EncString> { + 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 key; + } + + async setMasterKey(masterKey: MasterKey, userId: UserId): Promise<void> { + 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 setMasterKeyHash(masterKeyHash: string, userId: UserId): Promise<void> { + 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 setMasterKeyEncryptedUserKey(encryptedKey: EncString, userId: UserId): Promise<void> { + 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); + } + + async setForceSetPasswordReason(reason: ForceSetPasswordReason, userId: UserId): Promise<void> { + 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/user-verification/user-verification.service.ts b/libs/common/src/auth/services/user-verification/user-verification.service.ts index 0b4cd96099..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, @@ -164,7 +170,8 @@ export class UserVerificationService implements UserVerificationServiceAbstracti private async verifyUserByMasterPassword( verification: MasterPasswordVerification, ): Promise<boolean> { - 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<boolean> { + 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/platform/abstractions/crypto.service.ts b/libs/common/src/platform/abstractions/crypto.service.ts index 85b2bfe82e..0745033f3a 100644 --- a/libs/common/src/platform/abstractions/crypto.service.ts +++ b/libs/common/src/platform/abstractions/crypto.service.ts @@ -112,18 +112,6 @@ export abstract class CryptoService { * @param userId The desired user */ abstract setMasterKeyEncryptedUserKey(UserKeyMasterKey: string, userId?: string): Promise<void>; - /** - * Sets the user's master key - * @param key The user's master key to set - * @param userId The desired user - */ - abstract setMasterKey(key: MasterKey, userId?: string): Promise<void>; - /** - * @param userId The desired user - * @returns The user's master key - */ - abstract getMasterKey(userId?: string): Promise<MasterKey>; - /** * @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 @@ -143,11 +131,6 @@ export abstract class CryptoService { kdf: KdfType, KdfConfig: KdfConfig, ): Promise<MasterKey>; - /** - * Clears the user's master key - * @param userId The desired user - */ - abstract clearMasterKey(userId?: string): Promise<void>; /** * Encrypts the existing (or provided) user key with the * provided master key @@ -185,20 +168,6 @@ export abstract class CryptoService { key: MasterKey, hashPurpose?: HashPurpose, ): Promise<string>; - /** - * Sets the user's master password hash - * @param keyHash The user's master password hash to set - */ - abstract setMasterKeyHash(keyHash: string): Promise<void>; - /** - * @returns The user's master password hash - */ - abstract getMasterKeyHash(): Promise<string>; - /** - * Clears the user's stored master password hash - * @param userId The desired user - */ - abstract clearMasterKeyHash(userId?: string): Promise<void>; /** * Compares the provided master password to the stored password hash and server password hash. * Updates the stored hash if outdated. diff --git a/libs/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index 4971481381..227cb43879 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -1,14 +1,12 @@ 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 { UserId } from "../../types/guid"; -import { 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"; @@ -17,7 +15,6 @@ import { KdfType } from "../enums"; 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. @@ -48,22 +45,6 @@ export abstract class StateService<T extends Account = Account> { getAddEditCipherInfo: (options?: StorageOptions) => Promise<AddEditCipherInfo>; setAddEditCipherInfo: (value: AddEditCipherInfo, options?: StorageOptions) => Promise<void>; - /** - * Gets the user's master key - */ - getMasterKey: (options?: StorageOptions) => Promise<MasterKey>; - /** - * Sets the user's master key - */ - setMasterKey: (value: MasterKey, options?: StorageOptions) => Promise<void>; - /** - * Gets the user key encrypted by the master key - */ - getMasterKeyEncryptedUserKey: (options?: StorageOptions) => Promise<string>; - /** - * Sets the user key encrypted by the master key - */ - setMasterKeyEncryptedUserKey: (value: string, options?: StorageOptions) => Promise<void>; /** * Gets the user's auto key */ @@ -108,10 +89,6 @@ export abstract class StateService<T extends Account = Account> { * @deprecated For migration purposes only, use getUserKeyMasterKey instead */ getEncryptedCryptoSymmetricKey: (options?: StorageOptions) => Promise<string>; - /** - * @deprecated For legacy purposes only, use getMasterKey instead - */ - getCryptoMasterKey: (options?: StorageOptions) => Promise<SymmetricCryptoKey>; /** * @deprecated For migration purposes only, use getUserKeyAuto instead */ @@ -189,18 +166,11 @@ export abstract class StateService<T extends Account = Account> { setEncryptedPinProtected: (value: string, options?: StorageOptions) => Promise<void>; getEverBeenUnlocked: (options?: StorageOptions) => Promise<boolean>; setEverBeenUnlocked: (value: boolean, options?: StorageOptions) => Promise<void>; - getForceSetPasswordReason: (options?: StorageOptions) => Promise<ForceSetPasswordReason>; - setForceSetPasswordReason: ( - value: ForceSetPasswordReason, - options?: StorageOptions, - ) => Promise<void>; getIsAuthenticated: (options?: StorageOptions) => Promise<boolean>; getKdfConfig: (options?: StorageOptions) => Promise<KdfConfig>; setKdfConfig: (kdfConfig: KdfConfig, options?: StorageOptions) => Promise<void>; getKdfType: (options?: StorageOptions) => Promise<KdfType>; setKdfType: (value: KdfType, options?: StorageOptions) => Promise<void>; - getKeyHash: (options?: StorageOptions) => Promise<string>; - setKeyHash: (value: string, options?: StorageOptions) => Promise<void>; getLastActive: (options?: StorageOptions) => Promise<number>; setLastActive: (value: number, options?: StorageOptions) => Promise<void>; getLastSync: (options?: StorageOptions) => Promise<string>; 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 4a96da1b48..6bdb08edd5 100644 --- a/libs/common/src/platform/models/domain/account-keys.spec.ts +++ b/libs/common/src/platform/models/domain/account-keys.spec.ts @@ -2,7 +2,6 @@ import { makeStaticByteArray } from "../../../../spec"; import { Utils } from "../../misc/utils"; import { AccountKeys, EncryptionPair } from "./account"; -import { SymmetricCryptoKey } from "./symmetric-crypto-key"; describe("AccountKeys", () => { describe("toJSON", () => { @@ -32,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({ diff --git a/libs/common/src/platform/models/domain/account.ts b/libs/common/src/platform/models/domain/account.ts index 4ed36fd389..753b15c09b 100644 --- a/libs/common/src/platform/models/domain/account.ts +++ b/libs/common/src/platform/models/domain/account.ts @@ -1,7 +1,6 @@ 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 { @@ -10,7 +9,6 @@ import { } from "../../../tools/generator/password"; import { UsernameGeneratorOptions } from "../../../tools/generator/username/username-generation-options"; 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"; @@ -90,12 +88,8 @@ export class AccountData { } export class AccountKeys { - masterKey?: MasterKey; - masterKeyEncryptedUserKey?: string; 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*/ @@ -120,8 +114,6 @@ export class AccountKeys { return null; } return Object.assign(new AccountKeys(), obj, { - masterKey: SymmetricCryptoKey.fromJSON(obj?.masterKey), - cryptoMasterKey: SymmetricCryptoKey.fromJSON(obj?.cryptoMasterKey), cryptoSymmetricKey: EncryptionPair.fromJSON( obj?.cryptoSymmetricKey, SymmetricCryptoKey.fromJSON, @@ -150,10 +142,8 @@ export class AccountProfile { email?: string; emailVerified?: boolean; everBeenUnlocked?: boolean; - forceSetPasswordReason?: ForceSetPasswordReason; lastSync?: string; userId?: string; - keyHash?: string; kdfIterations?: number; kdfMemory?: number; kdfParallelism?: number; diff --git a/libs/common/src/platform/services/crypto.service.spec.ts b/libs/common/src/platform/services/crypto.service.spec.ts index 9160664aa5..e9e4ec62c2 100644 --- a/libs/common/src/platform/services/crypto.service.spec.ts +++ b/libs/common/src/platform/services/crypto.service.spec.ts @@ -5,6 +5,7 @@ import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-a 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"; @@ -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); }); }); diff --git a/libs/common/src/platform/services/crypto.service.ts b/libs/common/src/platform/services/crypto.service.ts index dd3c497470..6d1143736c 100644 --- a/libs/common/src/platform/services/crypto.service.ts +++ b/libs/common/src/platform/services/crypto.service.ts @@ -6,6 +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 { InternalMasterPasswordServiceAbstraction } from "../../auth/abstractions/master-password.service.abstraction"; import { AuthenticationStatus } from "../../auth/enums/authentication-status"; import { KdfConfig } from "../../auth/models/domain/kdf-config"; import { Utils } from "../../platform/misc/utils"; @@ -82,6 +83,7 @@ export class CryptoService implements CryptoServiceAbstraction { readonly everHadUserKey$: Observable<boolean>; constructor( + protected masterPasswordService: InternalMasterPasswordServiceAbstraction, protected keyGenerationService: KeyGenerationService, protected cryptoFunctionService: CryptoFunctionService, protected encryptService: EncryptService, @@ -181,12 +183,16 @@ export class CryptoService implements CryptoServiceAbstraction { } async isLegacyUser(masterKey?: MasterKey, userId?: UserId): Promise<boolean> { - 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<UserKey> { + userId ??= await firstValueFrom(this.stateProvider.activeUserId$); + const userKey = await this.getUserKey(userId); if (userKey) { return userKey; @@ -194,7 +200,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<UserKey> { @@ -233,7 +240,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."); } @@ -271,28 +281,16 @@ export class CryptoService implements CryptoServiceAbstraction { } async setMasterKeyEncryptedUserKey(userKeyMasterKey: string, userId?: UserId): Promise<void> { - await this.stateService.setMasterKeyEncryptedUserKey(userKeyMasterKey, { userId: userId }); - } - - async setMasterKey(key: MasterKey, userId?: UserId): Promise<void> { - await this.stateService.setMasterKey(key, { userId: userId }); - } - - async getMasterKey(userId?: UserId): Promise<MasterKey> { - 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; + 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 }), @@ -306,6 +304,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, @@ -321,10 +320,6 @@ export class CryptoService implements CryptoServiceAbstraction { )) as MasterKey; } - async clearMasterKey(userId?: UserId): Promise<void> { - await this.stateService.setMasterKey(null, { userId: userId }); - } - async encryptUserKeyWithMasterKey( masterKey: MasterKey, userKey?: UserKey, @@ -333,32 +328,31 @@ 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<UserKey> { - masterKey ||= await this.getMasterKey(userId); + userId ??= await firstValueFrom(this.stateProvider.activeUserId$); + 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({ - userId: userId, - }); + if (userKey == null) { + let userKey = await this.masterPasswordService.getMasterKeyEncryptedUserKey(userId); // Try one more way to get the user key if it still wasn't found. - if (masterKeyEncryptedUserKey == null) { - masterKeyEncryptedUserKey = await this.stateService.getEncryptedCryptoSymmetricKey({ + if (userKey == null) { + const deprecatedKey = await this.stateService.getEncryptedCryptoSymmetricKey({ userId: userId, }); + if (deprecatedKey == null) { + throw new Error("No encrypted user key found."); + } + userKey = new EncString(deprecatedKey); } - - if (masterKeyEncryptedUserKey == null) { - throw new Error("No encrypted user key found."); - } - userKey = new EncString(masterKeyEncryptedUserKey); } let decUserKey: Uint8Array; @@ -377,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<string> { - 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."); @@ -393,20 +391,12 @@ export class CryptoService implements CryptoServiceAbstraction { return Utils.fromBufferToB64(hash); } - async setMasterKeyHash(keyHash: string): Promise<void> { - await this.stateService.setKeyHash(keyHash); - } - - async getMasterKeyHash(): Promise<string> { - return await this.stateService.getKeyHash(); - } - - async clearMasterKeyHash(userId?: UserId): Promise<void> { - return await this.stateService.setKeyHash(null, { userId: userId }); - } - + // TODO: move to MasterPasswordService async compareAndUpdateKeyHash(masterPassword: string, masterKey: MasterKey): Promise<boolean> { - 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, @@ -424,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; } } @@ -481,7 +471,7 @@ export class CryptoService implements CryptoServiceAbstraction { } async clearOrgKeys(memoryOnly?: boolean, userId?: UserId): Promise<void> { - const activeUserId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$); const userIdIsActive = userId == null || userId === activeUserId; if (!memoryOnly) { @@ -527,7 +517,7 @@ export class CryptoService implements CryptoServiceAbstraction { } async clearProviderKeys(memoryOnly?: boolean, userId?: UserId): Promise<void> { - const activeUserId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$); const userIdIsActive = userId == null || userId === activeUserId; if (!memoryOnly) { @@ -598,7 +588,7 @@ export class CryptoService implements CryptoServiceAbstraction { } async clearKeyPair(memoryOnly?: boolean, userId?: UserId): Promise<void[]> { - const activeUserId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$); const userIdIsActive = userId == null || userId === activeUserId; if (!memoryOnly) { @@ -681,8 +671,10 @@ export class CryptoService implements CryptoServiceAbstraction { } async clearKeys(userId?: UserId): Promise<any> { + userId ??= await firstValueFrom(this.stateProvider.activeUserId$); + await this.masterPasswordService.setMasterKeyHash(null, userId); + await this.clearUserKey(true, userId); - await this.clearMasterKeyHash(userId); await this.clearOrgKeys(false, userId); await this.clearProviderKeys(false, userId); await this.clearKeyPair(false, userId); @@ -1037,7 +1029,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/state.service.ts b/libs/common/src/platform/services/state.service.ts index a35659a7ac..b3e33cf362 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -5,14 +5,12 @@ 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 { UserId } from "../../types/guid"; -import { 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"; @@ -35,7 +33,6 @@ 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"; @@ -273,65 +270,6 @@ export class StateService< ); } - /** - * @deprecated Do not save the Master Key. Use the User Symmetric Key instead - */ - async getCryptoMasterKey(options?: StorageOptions): Promise<SymmetricCryptoKey> { - 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<MasterKey> { - 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<void> { - 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<string> { - 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<void> { - 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 */ @@ -823,30 +761,6 @@ export class StateService< ); } - async getForceSetPasswordReason(options?: StorageOptions): Promise<ForceSetPasswordReason> { - return ( - ( - await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()), - ) - )?.profile?.forceSetPasswordReason ?? ForceSetPasswordReason.None - ); - } - - async setForceSetPasswordReason( - value: ForceSetPasswordReason, - options?: StorageOptions, - ): Promise<void> { - 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 getIsAuthenticated(options?: StorageOptions): Promise<boolean> { return ( (await this.tokenService.getAccessToken(options?.userId as UserId)) != null && @@ -897,23 +811,6 @@ export class StateService< ); } - async getKeyHash(options?: StorageOptions): Promise<string> { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.profile?.keyHash; - } - - async setKeyHash(value: string, options?: StorageOptions): Promise<void> { - 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<number> { options = this.reconcileOptions(options, await this.defaultOnDiskOptions()); diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index 979321c1e3..39d9701fed 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -37,6 +37,8 @@ export const BILLING_DISK = new StateDefinition("billing", "disk"); 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", { 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..f7f025991c 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<CipherService>; let folderService: MockProxy<FolderService>; let collectionService: MockProxy<CollectionService>; @@ -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<VaultTimeoutAction[]>([]); vaultTimeoutService = new VaultTimeoutService( + accountService, + masterPasswordService, cipherService, folderService, collectionService, @@ -123,6 +135,15 @@ describe("VaultTimeoutService", () => { stateService.activeAccount$ = new BehaviorSubject<string>(globalSetups?.userId); + if (globalSetups?.userId) { + accountService.activeAccountSubject.next({ + id: globalSetups.userId as UserId, + status: accounts[globalSetups.userId]?.authStatus, + email: null, + name: null, + }); + } + platformUtilsService.isViewOpen.mockResolvedValue(globalSetups?.isViewOpen ?? false); vaultTimeoutSettingsService.vaultTimeoutAction$.mockImplementation((userId) => { @@ -156,8 +177,8 @@ describe("VaultTimeoutService", () => { expect(vaultTimeoutSettingsService.availableVaultTimeoutActions$).toHaveBeenCalledWith(userId); expect(stateService.setEverBeenUnlocked).toHaveBeenCalledWith(true, { userId: userId }); expect(stateService.setUserKeyAutoUnlock).toHaveBeenCalledWith(null, { userId: userId }); + expect(masterPasswordService.mock.setMasterKey).toHaveBeenCalledWith(null, userId); expect(cryptoService.clearUserKey).toHaveBeenCalledWith(false, userId); - expect(cryptoService.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..22eb070360 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,7 +88,7 @@ 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(); @@ -92,12 +96,13 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { await this.collectionService.clearActiveUserCache(); } + await this.masterPasswordService.setMasterKey(null, (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); diff --git a/libs/common/src/state-migrations/migrate.ts b/libs/common/src/state-migrations/migrate.ts index faccddb0af..76f0d7fd46 100644 --- a/libs/common/src/state-migrations/migrate.ts +++ b/libs/common/src/state-migrations/migrate.ts @@ -51,6 +51,7 @@ import { RememberedEmailMigrator } from "./migrations/51-move-remembered-email-t 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 { 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"; @@ -58,7 +59,7 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting import { MinVersionMigrator } from "./migrations/min-version"; export const MIN_VERSION = 3; -export const CURRENT_VERSION = 54; +export const CURRENT_VERSION = 55; export type MinVersion = typeof MIN_VERSION; @@ -115,7 +116,8 @@ export function createMigrationBuilder() { .with(RememberedEmailMigrator, 50, 51) .with(DeleteInstalledVersion, 51, 52) .with(DeviceTrustCryptoServiceStateProviderMigrator, 52, 53) - .with(SendMigrator, 53, 54); + .with(SendMigrator, 53, 54) + .with(MoveMasterKeyStateToProviderMigrator, 54, CURRENT_VERSION); } export async function currentVersion( 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<MigrationHelper>; + 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<void> { + const accounts = await helper.getAccounts<ExpectedAccountType>(); + async function migrateAccount(userId: string, account: ExpectedAccountType): Promise<void> { + 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<void> { + const accounts = await helper.getAccounts<ExpectedAccountType>(); + async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise<void> { + 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/vault/services/sync/sync.service.ts b/libs/common/src/vault/services/sync/sync.service.ts index d4601d9621..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, @@ -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, ); } } From bf2f570b61ed814ac93f0926c664509e0820b1de Mon Sep 17 00:00:00 2001 From: Oscar Hinton <Hinton@users.noreply.github.com> Date: Thu, 4 Apr 2024 16:40:21 +0200 Subject: [PATCH 113/351] [PM-7057] Add dev tool for toggling width in web vault (#8447) Add a dev tool for toggling the web vault width. This allows developers and designers to experiment with how the vault currently behaves with responsiveness and ensure new functionality looks good. --- .../organization-layout.component.html | 2 ++ .../layouts/organization-layout.component.ts | 2 ++ .../src/app/layouts/toggle-width.component.ts | 33 +++++++++++++++++++ .../app/layouts/user-layout.component.html | 2 ++ .../src/app/layouts/user-layout.component.ts | 2 ++ .../providers/providers-layout.component.html | 2 ++ .../providers/providers-layout.component.ts | 2 ++ .../secrets-manager/layout/layout.module.ts | 9 ++++- .../layout/navigation.component.html | 2 ++ 9 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 apps/web/src/app/layouts/toggle-width.component.ts 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 b9a277a2e9..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 @@ -105,6 +105,8 @@ *ngIf="organization.canManageScim" ></bit-nav-item> </bit-nav-group> + + <app-toggle-width></app-toggle-width> </nav> <ng-container *ngIf="organization$ | async as organization"> 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 680a9155e1..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 @@ -25,6 +25,7 @@ import { BannerModule, IconModule, LayoutComponent, NavigationModule } from "@bi 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({ @@ -41,6 +42,7 @@ import { AdminConsoleLogo } from "../../icons/admin-console-logo"; OrgSwitcherComponent, BannerModule, PaymentMethodWarningsModule, + ToggleWidthComponent, ], }) export class OrganizationLayoutComponent implements OnInit, OnDestroy { 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: `<bit-nav-item + text="Toggle Width" + icon="bwi-bug" + *ngIf="isDev" + class="tw-absolute tw-bottom-0 tw-w-full" + (click)="toggleWidth()" + ></bit-nav-item>`, + 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 c70b2f9ff7..15a01fa07b 100644 --- a/apps/web/src/app/layouts/user-layout.component.html +++ b/apps/web/src/app/layouts/user-layout.component.html @@ -32,6 +32,8 @@ *ngIf="hasFamilySponsorshipAvailable$ | async" ></bit-nav-item> </bit-nav-group> + + <app-toggle-width></app-toggle-width> </nav> <app-payment-method-warnings *ngIf="showPaymentMethodWarningBanners$ | async" diff --git a/apps/web/src/app/layouts/user-layout.component.ts b/apps/web/src/app/layouts/user-layout.component.ts index 1a225e49c7..fea0352867 100644 --- a/apps/web/src/app/layouts/user-layout.component.ts +++ b/apps/web/src/app/layouts/user-layout.component.ts @@ -16,6 +16,7 @@ import { IconModule, LayoutComponent, NavigationModule } from "@bitwarden/compon import { PaymentMethodWarningsModule } from "../billing/shared"; import { PasswordManagerLogo } from "./password-manager-logo"; +import { ToggleWidthComponent } from "./toggle-width.component"; @Component({ selector: "app-user-layout", @@ -29,6 +30,7 @@ import { PasswordManagerLogo } from "./password-manager-logo"; IconModule, NavigationModule, PaymentMethodWarningsModule, + ToggleWidthComponent, ], }) export class UserLayoutComponent implements OnInit { diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html index fe7f051652..ca2b1a3545 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html @@ -27,6 +27,8 @@ route="settings" *ngIf="showSettingsTab" ></bit-nav-item> + + <app-toggle-width></app-toggle-width> </nav> <app-payment-method-warnings *ngIf="showPaymentMethodWarningBanners$ | async" diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.ts index a28cf1ef37..c60c9d3a03 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.ts @@ -10,6 +10,7 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co import { IconModule, LayoutComponent, NavigationModule } from "@bitwarden/components"; import { ProviderPortalLogo } from "@bitwarden/web-vault/app/admin-console/icons/provider-portal-logo"; import { PaymentMethodWarningsModule } from "@bitwarden/web-vault/app/billing/shared"; +import { ToggleWidthComponent } from "@bitwarden/web-vault/app/layouts/toggle-width.component"; @Component({ selector: "providers-layout", @@ -23,6 +24,7 @@ import { PaymentMethodWarningsModule } from "@bitwarden/web-vault/app/billing/sh IconModule, NavigationModule, PaymentMethodWarningsModule, + ToggleWidthComponent, ], }) // eslint-disable-next-line rxjs-angular/prefer-takeuntil 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..5ac76a31fc 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 @@ -41,4 +41,6 @@ [relativeTo]="route.parent" ></bit-nav-item> </bit-nav-group> + + <app-toggle-width></app-toggle-width> </nav> From e2e593c0fe0e5b8048e5bf2542a8f5b8d6d2a0fa Mon Sep 17 00:00:00 2001 From: Cesar Gonzalez <cesar.a.gonzalezcs@gmail.com> Date: Thu, 4 Apr 2024 09:57:10 -0500 Subject: [PATCH 114/351] [PM-7278] Fix undefined reference to `keyManager` within `menu-trigger-for` directive (#8614) --- libs/components/src/menu/menu-trigger-for.directive.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libs/components/src/menu/menu-trigger-for.directive.ts b/libs/components/src/menu/menu-trigger-for.directive.ts index 7e392f241f..05f2e7a8ef 100644 --- a/libs/components/src/menu/menu-trigger-for.directive.ts +++ b/libs/components/src/menu/menu-trigger-for.directive.ts @@ -88,12 +88,12 @@ export class MenuTriggerForDirective implements OnDestroy { } this.destroyMenu(); }); - this.menu.keyManager.setFirstItemActive(); - 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() { From 775c8a1bbe2c71d7c1b66c3645766412bd505fa2 Mon Sep 17 00:00:00 2001 From: Jake Fink <jfink@bitwarden.com> Date: Thu, 4 Apr 2024 12:17:09 -0400 Subject: [PATCH 115/351] Revert "[PM-5362]Create MP Service for state provider migration (#7623)" (#8617) This reverts commit b1abfb0a5cd48c23affb1bd52e7d17e361037ff6. --- .../auth-request-service.factory.ts | 16 +- .../key-connector-service.factory.ts | 9 - .../login-strategy-service.factory.ts | 9 - .../master-password-service.factory.ts | 42 ---- .../user-verification-service.factory.ts | 9 - apps/browser/src/auth/popup/lock.component.ts | 3 - .../src/auth/popup/set-password.component.ts | 58 ++++- apps/browser/src/auth/popup/sso.component.ts | 8 +- .../src/auth/popup/two-factor.component.ts | 6 - .../browser/src/background/main.background.ts | 23 +- .../background/nativeMessaging.background.ts | 16 +- .../vault-timeout-service.factory.ts | 12 - .../crypto-service.factory.ts | 6 - .../services/browser-crypto.service.ts | 3 - apps/cli/src/auth/commands/unlock.command.ts | 18 +- apps/cli/src/bw.ts | 16 +- apps/cli/src/commands/serve.command.ts | 2 - apps/cli/src/program.ts | 4 - apps/desktop/src/app/app.component.ts | 7 +- .../src/app/services/services.module.ts | 2 - apps/desktop/src/auth/lock.component.spec.ts | 6 - apps/desktop/src/auth/lock.component.ts | 3 - .../src/auth/set-password.component.ts | 6 - apps/desktop/src/auth/sso.component.ts | 6 - apps/desktop/src/auth/two-factor.component.ts | 6 - .../services/electron-crypto.service.spec.ts | 4 - .../services/electron-crypto.service.ts | 13 +- .../src/services/native-messaging.service.ts | 6 +- .../user-key-rotation.service.spec.ts | 14 +- .../key-rotation/user-key-rotation.service.ts | 5 +- apps/web/src/app/auth/lock.component.ts | 70 +++++- apps/web/src/app/auth/sso.component.ts | 6 - apps/web/src/app/auth/two-factor.component.ts | 6 - libs/angular/jest.config.js | 10 +- .../src/auth/components/lock.component.ts | 17 +- .../auth/components/set-password.component.ts | 24 +- .../src/auth/components/sso.component.spec.ts | 15 +- .../src/auth/components/sso.component.ts | 8 +- .../components/two-factor.component.spec.ts | 16 +- .../auth/components/two-factor.component.ts | 8 +- .../update-temp-password.component.ts | 14 +- libs/angular/src/auth/guards/auth.guard.ts | 12 +- .../src/services/jslib-services.module.ts | 28 +-- .../auth-request-login.strategy.spec.ts | 25 +-- .../auth-request-login.strategy.ts | 22 +- .../login-strategies/login.strategy.spec.ts | 18 +- .../common/login-strategies/login.strategy.ts | 4 - .../password-login.strategy.spec.ts | 26 +-- .../password-login.strategy.ts | 23 +- .../sso-login.strategy.spec.ts | 22 +- .../login-strategies/sso-login.strategy.ts | 17 +- .../user-api-login.strategy.spec.ts | 13 +- .../user-api-login.strategy.ts | 9 +- .../webauthn-login.strategy.spec.ts | 11 +- .../webauthn-login.strategy.ts | 6 - .../auth-request/auth-request.service.spec.ts | 42 +--- .../auth-request/auth-request.service.ts | 18 +- .../login-strategy.service.spec.ts | 17 +- .../login-strategy.service.ts | 19 +- .../master-password.service.abstraction.ts | 67 ------ .../services/key-connector.service.spec.ts | 16 +- .../auth/services/key-connector.service.ts | 13 +- .../fake-master-password.service.ts | 56 ----- .../master-password.service.ts | 125 ----------- .../user-verification.service.ts | 16 +- .../platform/abstractions/crypto.service.ts | 31 +++ .../platform/abstractions/state.service.ts | 30 +++ .../models/domain/account-keys.spec.ts | 7 + .../src/platform/models/domain/account.ts | 10 + .../platform/services/crypto.service.spec.ts | 14 +- .../src/platform/services/crypto.service.ts | 113 +++++----- .../src/platform/services/state.service.ts | 103 +++++++++ .../src/platform/state/state-definitions.ts | 2 - .../vault-timeout.service.spec.ts | 23 +- .../vault-timeout/vault-timeout.service.ts | 9 +- libs/common/src/state-migrations/migrate.ts | 6 +- ...-move-master-key-state-to-provider.spec.ts | 210 ------------------ .../55-move-master-key-state-to-provider.ts | 111 --------- .../src/vault/services/sync/sync.service.ts | 12 +- 79 files changed, 498 insertions(+), 1340 deletions(-) delete mode 100644 apps/browser/src/auth/background/service-factories/master-password-service.factory.ts delete mode 100644 libs/common/src/auth/abstractions/master-password.service.abstraction.ts delete mode 100644 libs/common/src/auth/services/master-password/fake-master-password.service.ts delete mode 100644 libs/common/src/auth/services/master-password/master-password.service.ts delete mode 100644 libs/common/src/state-migrations/migrations/55-move-master-key-state-to-provider.spec.ts delete mode 100644 libs/common/src/state-migrations/migrations/55-move-master-key-state-to-provider.ts 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 295fedbadd..bd96a211ba 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 @@ -17,21 +17,18 @@ import { FactoryOptions, factory, } from "../../../platform/background/service-factories/factory-options"; - -import { accountServiceFactory, AccountServiceInitOptions } from "./account-service.factory"; import { - internalMasterPasswordServiceFactory, - MasterPasswordServiceInitOptions, -} from "./master-password-service.factory"; + stateServiceFactory, + StateServiceInitOptions, +} from "../../../platform/background/service-factories/state-service.factory"; type AuthRequestServiceFactoryOptions = FactoryOptions; export type AuthRequestServiceInitOptions = AuthRequestServiceFactoryOptions & AppIdServiceInitOptions & - AccountServiceInitOptions & - MasterPasswordServiceInitOptions & CryptoServiceInitOptions & - ApiServiceInitOptions; + ApiServiceInitOptions & + StateServiceInitOptions; export function authRequestServiceFactory( cache: { authRequestService?: AuthRequestServiceAbstraction } & CachedServices, @@ -44,10 +41,9 @@ 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), ), ); } 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 c602acadae..4a0dd07b32 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 @@ -31,11 +31,6 @@ import { 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 & { @@ -45,8 +40,6 @@ type KeyConnectorServiceFactoryOptions = FactoryOptions & { }; export type KeyConnectorServiceInitOptions = KeyConnectorServiceFactoryOptions & - AccountServiceInitOptions & - MasterPasswordServiceInitOptions & CryptoServiceInitOptions & ApiServiceInitOptions & TokenServiceInitOptions & @@ -65,8 +58,6 @@ export function keyConnectorServiceFactory( opts, async () => new KeyConnectorService( - 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/login-strategy-service.factory.ts b/apps/browser/src/auth/background/service-factories/login-strategy-service.factory.ts index f184072cce..2cc4692ca9 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,7 +59,6 @@ import { PasswordStrengthServiceInitOptions, } from "../../../tools/background/service_factories/password-strength-service.factory"; -import { accountServiceFactory, AccountServiceInitOptions } from "./account-service.factory"; import { authRequestServiceFactory, AuthRequestServiceInitOptions, @@ -72,10 +71,6 @@ 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 { @@ -86,8 +81,6 @@ import { type LoginStrategyServiceFactoryOptions = FactoryOptions; export type LoginStrategyServiceInitOptions = LoginStrategyServiceFactoryOptions & - AccountServiceInitOptions & - MasterPasswordServiceInitOptions & CryptoServiceInitOptions & ApiServiceInitOptions & TokenServiceInitOptions & @@ -118,8 +111,6 @@ 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 deleted file mode 100644 index a2f9052a3f..0000000000 --- a/apps/browser/src/auth/background/service-factories/master-password-service.factory.ts +++ /dev/null @@ -1,42 +0,0 @@ -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<InternalMasterPasswordServiceAbstraction> { - return factory( - cache, - "masterPasswordService", - opts, - async () => new MasterPasswordService(await stateProviderFactory(cache, opts)), - ); -} - -export async function masterPasswordServiceFactory( - cache: { masterPasswordService?: InternalMasterPasswordServiceAbstraction } & CachedServices, - opts: MasterPasswordServiceInitOptions, -): Promise<MasterPasswordServiceAbstraction> { - return (await internalMasterPasswordServiceFactory( - cache, - opts, - )) as MasterPasswordServiceAbstraction; -} 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 a8b67b21ca..e8be9099ca 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,11 +31,6 @@ 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, @@ -51,8 +46,6 @@ type UserVerificationServiceFactoryOptions = FactoryOptions; export type UserVerificationServiceInitOptions = UserVerificationServiceFactoryOptions & StateServiceInitOptions & CryptoServiceInitOptions & - AccountServiceInitOptions & - MasterPasswordServiceInitOptions & I18nServiceInitOptions & UserVerificationApiServiceInitOptions & UserDecryptionOptionsServiceInitOptions & @@ -73,8 +66,6 @@ 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/lock.component.ts b/apps/browser/src/auth/popup/lock.component.ts index 16c32337cf..f232eca45a 100644 --- a/apps/browser/src/auth/popup/lock.component.ts +++ b/apps/browser/src/auth/popup/lock.component.ts @@ -12,7 +12,6 @@ import { InternalPolicyService } from "@bitwarden/common/admin-console/abstracti 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"; @@ -42,7 +41,6 @@ export class LockComponent extends BaseLockComponent { fido2PopoutSessionData$ = fido2PopoutSessionData$(); constructor( - masterPasswordService: InternalMasterPasswordServiceAbstraction, router: Router, i18nService: I18nService, platformUtilsService: PlatformUtilsService, @@ -68,7 +66,6 @@ export class LockComponent extends BaseLockComponent { accountService: AccountService, ) { super( - masterPasswordService, router, i18nService, platformUtilsService, diff --git a/apps/browser/src/auth/popup/set-password.component.ts b/apps/browser/src/auth/popup/set-password.component.ts index accde2e9a0..ea1cacc7ac 100644 --- a/apps/browser/src/auth/popup/set-password.component.ts +++ b/apps/browser/src/auth/popup/set-password.component.ts @@ -1,9 +1,65 @@ 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 {} +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, + ); + } +} diff --git a/apps/browser/src/auth/popup/sso.component.ts b/apps/browser/src/auth/popup/sso.component.ts index 14df0d1752..228c7401fd 100644 --- a/apps/browser/src/auth/popup/sso.component.ts +++ b/apps/browser/src/auth/popup/sso.component.ts @@ -9,9 +9,7 @@ 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 { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -47,9 +45,7 @@ export class SsoComponent extends BaseSsoComponent { logService: LogService, userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, configService: ConfigService, - masterPasswordService: InternalMasterPasswordServiceAbstraction, - accountService: AccountService, - private authService: AuthService, + protected authService: AuthService, @Inject(WINDOW) private win: Window, ) { super( @@ -67,8 +63,6 @@ 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 9424369971..dd541f63f8 100644 --- a/apps/browser/src/auth/popup/two-factor.component.ts +++ b/apps/browser/src/auth/popup/two-factor.component.ts @@ -11,8 +11,6 @@ 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 { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; @@ -64,8 +62,6 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { configService: ConfigService, ssoLoginService: SsoLoginServiceAbstraction, private dialogService: DialogService, - masterPasswordService: InternalMasterPasswordServiceAbstraction, - accountService: AccountService, @Inject(WINDOW) protected win: Window, private browserMessagingApi: ZonedMessageListenerService, ) { @@ -86,8 +82,6 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { 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/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index ed7e8dc100..255538de52 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -32,7 +32,6 @@ 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 { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service"; @@ -47,7 +46,6 @@ 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 { MasterPasswordService } from "@bitwarden/common/auth/services/master-password/master-password.service"; import { SsoLoginService } from "@bitwarden/common/auth/services/sso-login.service"; import { TokenService } from "@bitwarden/common/auth/services/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/services/two-factor.service"; @@ -244,7 +242,6 @@ export default class MainBackground { keyGenerationService: KeyGenerationServiceAbstraction; cryptoService: CryptoServiceAbstraction; cryptoFunctionService: CryptoFunctionServiceAbstraction; - masterPasswordService: InternalMasterPasswordServiceAbstraction; tokenService: TokenServiceAbstraction; appIdService: AppIdServiceAbstraction; apiService: ApiServiceAbstraction; @@ -483,11 +480,8 @@ export default class MainBackground { const themeStateService = new DefaultThemeStateService(this.globalStateProvider); - this.masterPasswordService = new MasterPasswordService(this.stateProvider); - this.i18nService = new I18nService(BrowserApi.getUILanguage(), this.globalStateProvider); this.cryptoService = new BrowserCryptoService( - this.masterPasswordService, this.keyGenerationService, this.cryptoFunctionService, this.encryptService, @@ -531,8 +525,6 @@ export default class MainBackground { this.badgeSettingsService = new BadgeSettingsService(this.stateProvider); this.policyApiService = new PolicyApiService(this.policyService, this.apiService); this.keyConnectorService = new KeyConnectorService( - this.accountService, - this.masterPasswordService, this.cryptoService, this.apiService, this.tokenService, @@ -586,10 +578,9 @@ export default class MainBackground { this.authRequestService = new AuthRequestService( this.appIdService, - this.accountService, - this.masterPasswordService, this.cryptoService, this.apiService, + this.stateService, ); this.authService = new AuthService( @@ -606,8 +597,6 @@ export default class MainBackground { ); this.loginStrategyService = new LoginStrategyService( - this.accountService, - this.masterPasswordService, this.cryptoService, this.apiService, this.tokenService, @@ -683,8 +672,6 @@ export default class MainBackground { this.userVerificationService = new UserVerificationService( this.stateService, this.cryptoService, - this.accountService, - this.masterPasswordService, this.i18nService, this.userVerificationApiService, this.userDecryptionOptionsService, @@ -707,8 +694,6 @@ export default class MainBackground { this.vaultSettingsService = new VaultSettingsService(this.stateProvider); this.vaultTimeoutService = new VaultTimeoutService( - this.accountService, - this.masterPasswordService, this.cipherService, this.folderService, this.collectionService, @@ -744,8 +729,6 @@ export default class MainBackground { this.providerService = new ProviderService(this.stateProvider); this.syncService = new SyncService( - this.masterPasswordService, - this.accountService, this.apiService, this.domainSettingsService, this.folderService, @@ -895,8 +878,6 @@ export default class MainBackground { this.fido2Service, ); this.nativeMessagingBackground = new NativeMessagingBackground( - this.accountService, - this.masterPasswordService, this.cryptoService, this.cryptoFunctionService, this.runtimeBackground, @@ -1125,7 +1106,7 @@ export default class MainBackground { const status = await this.authService.getAuthStatus(userId); const forcePasswordReset = - (await firstValueFrom(this.masterPasswordService.forceSetPasswordReason$(userId))) != + (await this.stateService.getForceSetPasswordReason({ userId: userId })) != ForceSetPasswordReason.None; await this.systemService.clearPendingClipboard(); diff --git a/apps/browser/src/background/nativeMessaging.background.ts b/apps/browser/src/background/nativeMessaging.background.ts index faf2e6e2cc..240fb1dede 100644 --- a/apps/browser/src/background/nativeMessaging.background.ts +++ b/apps/browser/src/background/nativeMessaging.background.ts @@ -1,8 +1,6 @@ import { firstValueFrom } from "rxjs"; -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 { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; @@ -73,8 +71,6 @@ export class NativeMessagingBackground { private validatingFingerprint: boolean; constructor( - private accountService: AccountService, - private masterPasswordService: InternalMasterPasswordServiceAbstraction, private cryptoService: CryptoService, private cryptoFunctionService: CryptoFunctionService, private runtimeBackground: RuntimeBackground, @@ -340,14 +336,10 @@ export class NativeMessagingBackground { ) as UserKey; await this.cryptoService.setUserKey(userKey); } else if (message.keyB64) { - const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; // Backwards compatibility to support cases in which the user hasn't updated their desktop app // TODO: Remove after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3472) - const encUserKeyPrim = await this.stateService.getEncryptedCryptoSymmetricKey(); - const encUserKey = - encUserKeyPrim != null - ? new EncString(encUserKeyPrim) - : await this.masterPasswordService.getMasterKeyEncryptedUserKey(userId); + let encUserKey = await this.stateService.getEncryptedCryptoSymmetricKey(); + encUserKey ||= await this.stateService.getMasterKeyEncryptedUserKey(); if (!encUserKey) { throw new Error("No encrypted user key found"); } @@ -356,9 +348,9 @@ export class NativeMessagingBackground { ) as MasterKey; const userKey = await this.cryptoService.decryptUserKeyWithMasterKey( masterKey, - encUserKey, + new EncString(encUserKey), ); - await this.masterPasswordService.setMasterKey(masterKey, userId); + await this.cryptoService.setMasterKey(masterKey); await this.cryptoService.setUserKey(userKey); } else { throw new Error("No key received"); diff --git a/apps/browser/src/background/service-factories/vault-timeout-service.factory.ts b/apps/browser/src/background/service-factories/vault-timeout-service.factory.ts index 14f055114b..0e4d1420da 100644 --- a/apps/browser/src/background/service-factories/vault-timeout-service.factory.ts +++ b/apps/browser/src/background/service-factories/vault-timeout-service.factory.ts @@ -1,17 +1,9 @@ import { VaultTimeoutService as AbstractVaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; -import { - accountServiceFactory, - AccountServiceInitOptions, -} from "../../auth/background/service-factories/account-service.factory"; import { authServiceFactory, AuthServiceInitOptions, } from "../../auth/background/service-factories/auth-service.factory"; -import { - internalMasterPasswordServiceFactory, - MasterPasswordServiceInitOptions, -} from "../../auth/background/service-factories/master-password-service.factory"; import { CryptoServiceInitOptions, cryptoServiceFactory, @@ -65,8 +57,6 @@ type VaultTimeoutServiceFactoryOptions = FactoryOptions & { }; export type VaultTimeoutServiceInitOptions = VaultTimeoutServiceFactoryOptions & - AccountServiceInitOptions & - MasterPasswordServiceInitOptions & CipherServiceInitOptions & FolderServiceInitOptions & CollectionServiceInitOptions & @@ -89,8 +79,6 @@ export function vaultTimeoutServiceFactory( opts, async () => new VaultTimeoutService( - await accountServiceFactory(cache, opts), - await internalMasterPasswordServiceFactory(cache, opts), await cipherServiceFactory(cache, opts), await folderServiceFactory(cache, opts), await collectionServiceFactory(cache, opts), diff --git a/apps/browser/src/platform/background/service-factories/crypto-service.factory.ts b/apps/browser/src/platform/background/service-factories/crypto-service.factory.ts index ed4fde162c..97614660d1 100644 --- a/apps/browser/src/platform/background/service-factories/crypto-service.factory.ts +++ b/apps/browser/src/platform/background/service-factories/crypto-service.factory.ts @@ -4,10 +4,6 @@ import { AccountServiceInitOptions, accountServiceFactory, } from "../../../auth/background/service-factories/account-service.factory"; -import { - internalMasterPasswordServiceFactory, - MasterPasswordServiceInitOptions, -} from "../../../auth/background/service-factories/master-password-service.factory"; import { StateServiceInitOptions, stateServiceFactory, @@ -38,7 +34,6 @@ import { StateProviderInitOptions, stateProviderFactory } from "./state-provider type CryptoServiceFactoryOptions = FactoryOptions; export type CryptoServiceInitOptions = CryptoServiceFactoryOptions & - MasterPasswordServiceInitOptions & KeyGenerationServiceInitOptions & CryptoFunctionServiceInitOptions & EncryptServiceInitOptions & @@ -58,7 +53,6 @@ export function cryptoServiceFactory( opts, async () => new BrowserCryptoService( - await internalMasterPasswordServiceFactory(cache, opts), await keyGenerationServiceFactory(cache, opts), await cryptoFunctionServiceFactory(cache, opts), await encryptServiceFactory(cache, opts), diff --git a/apps/browser/src/platform/services/browser-crypto.service.ts b/apps/browser/src/platform/services/browser-crypto.service.ts index d7533a22d6..969dbdf761 100644 --- a/apps/browser/src/platform/services/browser-crypto.service.ts +++ b/apps/browser/src/platform/services/browser-crypto.service.ts @@ -1,7 +1,6 @@ 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"; @@ -18,7 +17,6 @@ import { UserKey } from "@bitwarden/common/types/key"; export class BrowserCryptoService extends CryptoService { constructor( - masterPasswordService: InternalMasterPasswordServiceAbstraction, keyGenerationService: KeyGenerationService, cryptoFunctionService: CryptoFunctionService, encryptService: EncryptService, @@ -30,7 +28,6 @@ export class BrowserCryptoService extends CryptoService { private biometricStateService: BiometricStateService, ) { super( - masterPasswordService, keyGenerationService, cryptoFunctionService, encryptService, diff --git a/apps/cli/src/auth/commands/unlock.command.ts b/apps/cli/src/auth/commands/unlock.command.ts index 6f7dea2074..98bc926079 100644 --- a/apps/cli/src/auth/commands/unlock.command.ts +++ b/apps/cli/src/auth/commands/unlock.command.ts @@ -1,10 +1,6 @@ -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"; @@ -22,8 +18,6 @@ import { CliUtils } from "../../utils"; export class UnlockCommand { constructor( - private accountService: AccountService, - private masterPasswordService: InternalMasterPasswordServiceAbstraction, private cryptoService: CryptoService, private stateService: StateService, private cryptoFunctionService: CryptoFunctionService, @@ -51,14 +45,11 @@ 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 userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; - const storedMasterKeyHash = await firstValueFrom( - this.masterPasswordService.masterKeyHash$(userId), - ); + const storedKeyHash = await this.cryptoService.getMasterKeyHash(); let passwordValid = false; if (masterKey != null) { - if (storedMasterKeyHash != null) { + if (storedKeyHash != null) { passwordValid = await this.cryptoService.compareAndUpdateKeyHash(password, masterKey); } else { const serverKeyHash = await this.cryptoService.hashMasterKey( @@ -76,7 +67,7 @@ export class UnlockCommand { masterKey, HashPurpose.LocalAuthorization, ); - await this.masterPasswordService.setMasterKeyHash(localKeyHash, userId); + await this.cryptoService.setMasterKeyHash(localKeyHash); } catch { // Ignore } @@ -84,8 +75,7 @@ export class UnlockCommand { } if (passwordValid) { - const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; - await this.masterPasswordService.setMasterKey(masterKey, userId); + await this.cryptoService.setMasterKey(masterKey); 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 3659859f73..3815fc773b 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -28,7 +28,6 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv 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"; @@ -169,7 +168,6 @@ export class Main { organizationUserService: OrganizationUserService; collectionService: CollectionService; vaultTimeoutService: VaultTimeoutService; - masterPasswordService: InternalMasterPasswordServiceAbstraction; vaultTimeoutSettingsService: VaultTimeoutSettingsService; syncService: SyncService; eventCollectionService: EventCollectionServiceAbstraction; @@ -354,7 +352,6 @@ export class Main { ); this.cryptoService = new CryptoService( - this.masterPasswordService, this.keyGenerationService, this.cryptoFunctionService, this.encryptService, @@ -435,8 +432,6 @@ export class Main { this.policyApiService = new PolicyApiService(this.policyService, this.apiService); this.keyConnectorService = new KeyConnectorService( - this.accountService, - this.masterPasswordService, this.cryptoService, this.apiService, this.tokenService, @@ -476,10 +471,9 @@ export class Main { this.authRequestService = new AuthRequestService( this.appIdService, - this.accountService, - this.masterPasswordService, this.cryptoService, this.apiService, + this.stateService, ); this.billingAccountProfileStateService = new DefaultBillingAccountProfileStateService( @@ -487,8 +481,6 @@ export class Main { ); this.loginStrategyService = new LoginStrategyService( - this.accountService, - this.masterPasswordService, this.cryptoService, this.apiService, this.tokenService, @@ -576,8 +568,6 @@ export class Main { this.userVerificationService = new UserVerificationService( this.stateService, this.cryptoService, - this.accountService, - this.masterPasswordService, this.i18nService, this.userVerificationApiService, this.userDecryptionOptionsService, @@ -588,8 +578,6 @@ export class Main { ); this.vaultTimeoutService = new VaultTimeoutService( - this.accountService, - this.masterPasswordService, this.cipherService, this.folderService, this.collectionService, @@ -608,8 +596,6 @@ 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, diff --git a/apps/cli/src/commands/serve.command.ts b/apps/cli/src/commands/serve.command.ts index 76447f769c..4d0d1e5798 100644 --- a/apps/cli/src/commands/serve.command.ts +++ b/apps/cli/src/commands/serve.command.ts @@ -122,8 +122,6 @@ 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/program.ts b/apps/cli/src/program.ts index fa71a88f54..a79f3847da 100644 --- a/apps/cli/src/program.ts +++ b/apps/cli/src/program.ts @@ -253,8 +253,6 @@ 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, @@ -615,8 +613,6 @@ 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/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index f060d5f854..4e74135c49 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -26,7 +26,6 @@ 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"; @@ -121,7 +120,6 @@ export class AppComponent implements OnInit, OnDestroy { private accountCleanUpInProgress: { [userId: string]: boolean } = {}; constructor( - private masterPasswordService: MasterPasswordServiceAbstraction, private broadcasterService: BroadcasterService, private folderService: InternalFolderService, private syncService: SyncService, @@ -410,9 +408,8 @@ export class AppComponent implements OnInit, OnDestroy { (await this.authService.getAuthStatus(message.userId)) === AuthenticationStatus.Locked; const forcedPasswordReset = - (await firstValueFrom( - this.masterPasswordService.forceSetPasswordReason$(message.userId), - )) != ForceSetPasswordReason.None; + (await this.stateService.getForceSetPasswordReason({ userId: message.userId })) != + ForceSetPasswordReason.None; if (locked) { this.messagingService.send("locked", { userId: message.userId }); } else if (forcedPasswordReset) { diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index 8e412d4977..84932ce7d9 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -20,7 +20,6 @@ import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaul 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 { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { BroadcasterService as BroadcasterServiceAbstraction } from "@bitwarden/common/platform/abstractions/broadcaster.service"; @@ -229,7 +228,6 @@ const safeProviders: SafeProvider[] = [ provide: CryptoServiceAbstraction, useClass: ElectronCryptoService, deps: [ - InternalMasterPasswordServiceAbstraction, KeyGenerationServiceAbstraction, CryptoFunctionServiceAbstraction, EncryptService, diff --git a/apps/desktop/src/auth/lock.component.spec.ts b/apps/desktop/src/auth/lock.component.spec.ts index c125eba022..0339889bf7 100644 --- a/apps/desktop/src/auth/lock.component.spec.ts +++ b/apps/desktop/src/auth/lock.component.spec.ts @@ -14,9 +14,7 @@ import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abs 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 { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; @@ -54,7 +52,6 @@ describe("LockComponent", () => { let broadcasterServiceMock: MockProxy<BroadcasterService>; let platformUtilsServiceMock: MockProxy<PlatformUtilsService>; let activatedRouteMock: MockProxy<ActivatedRoute>; - let mockMasterPasswordService: FakeMasterPasswordService; const mockUserId = Utils.newGuid() as UserId; const accountService: FakeAccountService = mockAccountServiceWith(mockUserId); @@ -70,8 +67,6 @@ describe("LockComponent", () => { activatedRouteMock = mock<ActivatedRoute>(); activatedRouteMock.queryParams = mock<ActivatedRoute["queryParams"]>(); - mockMasterPasswordService = new FakeMasterPasswordService(); - biometricStateService.dismissedRequirePasswordOnStartCallout$ = of(false); biometricStateService.promptAutomatically$ = of(false); biometricStateService.promptCancelled$ = of(false); @@ -79,7 +74,6 @@ describe("LockComponent", () => { await TestBed.configureTestingModule({ declarations: [LockComponent, I18nPipe], providers: [ - { provide: InternalMasterPasswordServiceAbstraction, useValue: mockMasterPasswordService }, { provide: I18nService, useValue: mock<I18nService>(), diff --git a/apps/desktop/src/auth/lock.component.ts b/apps/desktop/src/auth/lock.component.ts index 16b58c5bbe..8b1448c06f 100644 --- a/apps/desktop/src/auth/lock.component.ts +++ b/apps/desktop/src/auth/lock.component.ts @@ -11,7 +11,6 @@ import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abs 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"; @@ -39,7 +38,6 @@ export class LockComponent extends BaseLockComponent { private autoPromptBiometric = false; constructor( - masterPasswordService: InternalMasterPasswordServiceAbstraction, router: Router, i18nService: I18nService, platformUtilsService: PlatformUtilsService, @@ -65,7 +63,6 @@ export class LockComponent extends BaseLockComponent { accountService: AccountService, ) { super( - masterPasswordService, router, i18nService, platformUtilsService, diff --git a/apps/desktop/src/auth/set-password.component.ts b/apps/desktop/src/auth/set-password.component.ts index 93dfe0abd8..a75668a856 100644 --- a/apps/desktop/src/auth/set-password.component.ts +++ b/apps/desktop/src/auth/set-password.component.ts @@ -8,8 +8,6 @@ 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"; @@ -31,8 +29,6 @@ const BroadcasterSubscriptionId = "SetPasswordComponent"; }) export class SetPasswordComponent extends BaseSetPasswordComponent implements OnDestroy { constructor( - accountService: AccountService, - masterPasswordService: InternalMasterPasswordServiceAbstraction, apiService: ApiService, i18nService: I18nService, cryptoService: CryptoService, @@ -54,8 +50,6 @@ 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 cc261f1235..210319b9ed 100644 --- a/apps/desktop/src/auth/sso.component.ts +++ b/apps/desktop/src/auth/sso.component.ts @@ -7,8 +7,6 @@ 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 { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; @@ -41,8 +39,6 @@ export class SsoComponent extends BaseSsoComponent { logService: LogService, userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, configService: ConfigService, - masterPasswordService: InternalMasterPasswordServiceAbstraction, - accountService: AccountService, ) { super( ssoLoginService, @@ -59,8 +55,6 @@ 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 d1b84c1fa0..fdbc52b4bf 100644 --- a/apps/desktop/src/auth/two-factor.component.ts +++ b/apps/desktop/src/auth/two-factor.component.ts @@ -11,8 +11,6 @@ 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 { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; @@ -62,8 +60,6 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction, configService: ConfigService, - masterPasswordService: InternalMasterPasswordServiceAbstraction, - accountService: AccountService, @Inject(WINDOW) protected win: Window, ) { super( @@ -83,8 +79,6 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { 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/platform/services/electron-crypto.service.spec.ts b/apps/desktop/src/platform/services/electron-crypto.service.spec.ts index 3d9171b52e..04adfcac70 100644 --- a/apps/desktop/src/platform/services/electron-crypto.service.spec.ts +++ b/apps/desktop/src/platform/services/electron-crypto.service.spec.ts @@ -1,7 +1,6 @@ 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"; @@ -31,7 +30,6 @@ describe("electronCryptoService", () => { const platformUtilService = mock<PlatformUtilsService>(); const logService = mock<LogService>(); const stateService = mock<StateService>(); - let masterPasswordService: FakeMasterPasswordService; let accountService: FakeAccountService; let stateProvider: FakeStateProvider; const biometricStateService = mock<BiometricStateService>(); @@ -40,11 +38,9 @@ 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 d113a18200..6b9327a9c4 100644 --- a/apps/desktop/src/platform/services/electron-crypto.service.ts +++ b/apps/desktop/src/platform/services/electron-crypto.service.ts @@ -1,7 +1,6 @@ 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"; @@ -21,7 +20,6 @@ import { UserKey, MasterKey } from "@bitwarden/common/types/key"; export class ElectronCryptoService extends CryptoService { constructor( - masterPasswordService: InternalMasterPasswordServiceAbstraction, keyGenerationService: KeyGenerationService, cryptoFunctionService: CryptoFunctionService, encryptService: EncryptService, @@ -33,7 +31,6 @@ export class ElectronCryptoService extends CryptoService { private biometricStateService: BiometricStateService, ) { super( - masterPasswordService, keyGenerationService, cryptoFunctionService, encryptService, @@ -162,16 +159,12 @@ export class ElectronCryptoService extends CryptoService { const oldBiometricKey = await this.stateService.getCryptoMasterKeyBiometric({ userId }); // decrypt const masterKey = new SymmetricCryptoKey(Utils.fromB64ToArray(oldBiometricKey)) as MasterKey; - 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); + let encUserKey = await this.stateService.getEncryptedCryptoSymmetricKey(); + encUserKey = encUserKey ?? (await this.stateService.getMasterKeyEncryptedUserKey()); if (!encUserKey) { throw new Error("No user key found during biometric migration"); } - const userKey = await this.decryptUserKeyWithMasterKey(masterKey, encUserKey); + const userKey = await this.decryptUserKeyWithMasterKey(masterKey, new EncString(encUserKey)); // migrate await this.storeBiometricKey(userKey, userId); await this.stateService.setCryptoMasterKeyBiometric(null, { userId }); diff --git a/apps/desktop/src/services/native-messaging.service.ts b/apps/desktop/src/services/native-messaging.service.ts index 01d9476977..148e4f1e89 100644 --- a/apps/desktop/src/services/native-messaging.service.ts +++ b/apps/desktop/src/services/native-messaging.service.ts @@ -1,7 +1,6 @@ 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"; @@ -31,7 +30,6 @@ export class NativeMessagingService { private sharedSecrets = new Map<string, SymmetricCryptoKey>(); constructor( - private masterPasswordService: MasterPasswordServiceAbstraction, private cryptoFunctionService: CryptoFunctionService, private cryptoService: CryptoService, private platformUtilService: PlatformUtilsService, @@ -164,9 +162,7 @@ export class NativeMessagingService { KeySuffixOptions.Biometric, message.userId, ); - const masterKey = await firstValueFrom( - this.masterPasswordService.masterKey$(message.userId as UserId), - ); + const masterKey = await this.cryptoService.getMasterKey(message.userId); if (userKey != null) { // we send the master key still for backwards compatibility 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 0997f18864..09c7bf9ace 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,7 +2,6 @@ import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject } from "rxjs"; import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.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"; @@ -10,6 +9,7 @@ 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 { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; 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"; @@ -22,10 +22,6 @@ 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"; @@ -50,10 +46,8 @@ describe("KeyRotationService", () => { const mockUserId = Utils.newGuid() as UserId; const mockAccountService: FakeAccountService = mockAccountServiceWith(mockUserId); - let mockMasterPasswordService: FakeMasterPasswordService = new FakeMasterPasswordService(); beforeAll(() => { - mockMasterPasswordService = new FakeMasterPasswordService(); mockApiService = mock<UserKeyRotationApiService>(); mockCipherService = mock<CipherService>(); mockFolderService = mock<FolderService>(); @@ -67,7 +61,6 @@ describe("KeyRotationService", () => { mockConfigService = mock<ConfigService>(); keyRotationService = new UserKeyRotationService( - mockMasterPasswordService, mockApiService, mockCipherService, mockFolderService, @@ -181,10 +174,7 @@ describe("KeyRotationService", () => { it("saves the master key in state after creation", async () => { await keyRotationService.rotateUserKeyAndEncryptedData("mockMasterPassword"); - expect(mockMasterPasswordService.mock.setMasterKey).toHaveBeenCalledWith( - "mockMasterKey" as any, - mockUserId, - ); + expect(mockCryptoService.setMasterKey).toHaveBeenCalledWith("mockMasterKey" as any); }); 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 f5812d341a..03bc604b4d 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 @@ -3,7 +3,6 @@ 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 { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -26,7 +25,6 @@ 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, @@ -63,8 +61,7 @@ export class UserKeyRotationService { } // Set master key again in case it was lost (could be lost on refresh) - const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; - await this.masterPasswordService.setMasterKey(masterKey, userId); + await this.cryptoService.setMasterKey(masterKey); const [newUserKey, newEncUserKey] = await this.cryptoService.makeUserKey(masterKey); if (!newUserKey || !newEncUserKey) { diff --git a/apps/web/src/app/auth/lock.component.ts b/apps/web/src/app/auth/lock.component.ts index 021bf0f9df..a1d4724396 100644 --- a/apps/web/src/app/auth/lock.component.ts +++ b/apps/web/src/app/auth/lock.component.ts @@ -1,12 +1,80 @@ -import { Component } from "@angular/core"; +import { Component, NgZone } from "@angular/core"; +import { Router } from "@angular/router"; 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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +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, + accountService: AccountService, + ) { + super( + router, + i18nService, + platformUtilsService, + messagingService, + cryptoService, + vaultTimeoutService, + vaultTimeoutSettingsService, + environmentService, + stateService, + apiService, + logService, + ngZone, + policyApiService, + policyService, + passwordStrengthService, + dialogService, + deviceTrustCryptoService, + userVerificationService, + pinCryptoService, + biometricStateService, + accountService, + ); + } + async ngOnInit() { await super.ngOnInit(); this.onSuccessfulSubmit = async () => { diff --git a/apps/web/src/app/auth/sso.component.ts b/apps/web/src/app/auth/sso.component.ts index e120b2749f..cdd979aa89 100644 --- a/apps/web/src/app/auth/sso.component.ts +++ b/apps/web/src/app/auth/sso.component.ts @@ -10,8 +10,6 @@ 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"; @@ -48,8 +46,6 @@ export class SsoComponent extends BaseSsoComponent { private validationService: ValidationService, userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, configService: ConfigService, - masterPasswordService: InternalMasterPasswordServiceAbstraction, - accountService: AccountService, ) { super( ssoLoginService, @@ -66,8 +62,6 @@ 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/two-factor.component.ts b/apps/web/src/app/auth/two-factor.component.ts index eed84b91f1..65bf1dba58 100644 --- a/apps/web/src/app/auth/two-factor.component.ts +++ b/apps/web/src/app/auth/two-factor.component.ts @@ -10,8 +10,6 @@ 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 { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; @@ -52,8 +50,6 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnDest userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction, configService: ConfigService, - masterPasswordService: InternalMasterPasswordServiceAbstraction, - accountService: AccountService, @Inject(WINDOW) protected win: Window, ) { super( @@ -73,8 +69,6 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnDest userDecryptionOptionsService, ssoLoginService, configService, - masterPasswordService, - accountService, ); this.onSuccessfulLoginNavigate = this.goAfterLogIn; } diff --git a/libs/angular/jest.config.js b/libs/angular/jest.config.js index c8e748575c..e294e4ff47 100644 --- a/libs/angular/jest.config.js +++ b/libs/angular/jest.config.js @@ -10,11 +10,7 @@ module.exports = { displayName: "libs/angular tests", preset: "jest-preset-angular", setupFilesAfterEnv: ["<rootDir>/test.setup.ts"], - moduleNameMapper: pathsToModuleNameMapper( - // lets us use @bitwarden/common/spec in tests - { "@bitwarden/common/spec": ["../common/spec"], ...(compilerOptions?.paths ?? {}) }, - { - prefix: "<rootDir>/", - }, - ), + moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, { + prefix: "<rootDir>/", + }), }; diff --git a/libs/angular/src/auth/components/lock.component.ts b/libs/angular/src/auth/components/lock.component.ts index 6602a917c9..aa3b801ded 100644 --- a/libs/angular/src/auth/components/lock.component.ts +++ b/libs/angular/src/auth/components/lock.component.ts @@ -12,7 +12,6 @@ import { InternalPolicyService } from "@bitwarden/common/admin-console/abstracti 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"; @@ -57,7 +56,6 @@ export class LockComponent implements OnInit, OnDestroy { private destroy$ = new Subject<void>(); constructor( - protected masterPasswordService: InternalMasterPasswordServiceAbstraction, protected router: Router, protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, @@ -208,7 +206,6 @@ 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(); @@ -218,13 +215,11 @@ export class LockComponent implements OnInit, OnDestroy { kdf, kdfConfig, ); - const storedMasterKeyHash = await firstValueFrom( - this.masterPasswordService.masterKeyHash$(userId), - ); + const storedPasswordHash = await this.cryptoService.getMasterKeyHash(); let passwordValid = false; - if (storedMasterKeyHash != null) { + if (storedPasswordHash != null) { // Offline unlock possible passwordValid = await this.cryptoService.compareAndUpdateKeyHash( this.masterPassword, @@ -249,7 +244,7 @@ export class LockComponent implements OnInit, OnDestroy { masterKey, HashPurpose.LocalAuthorization, ); - await this.masterPasswordService.setMasterKeyHash(localKeyHash, userId); + await this.cryptoService.setMasterKeyHash(localKeyHash); } catch (e) { this.logService.error(e); } finally { @@ -267,7 +262,7 @@ export class LockComponent implements OnInit, OnDestroy { } const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey); - await this.masterPasswordService.setMasterKey(masterKey, userId); + await this.cryptoService.setMasterKey(masterKey); await this.setUserKeyAndContinue(userKey, true); } @@ -297,10 +292,8 @@ export class LockComponent implements OnInit, OnDestroy { } if (this.requirePasswordChange()) { - const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; - await this.masterPasswordService.setForceSetPasswordReason( + await this.stateService.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/set-password.component.ts b/libs/angular/src/auth/components/set-password.component.ts index eebf87655b..a7442f711b 100644 --- a/libs/angular/src/auth/components/set-password.component.ts +++ b/libs/angular/src/auth/components/set-password.component.ts @@ -12,8 +12,6 @@ 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"; @@ -31,7 +29,6 @@ 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"; @@ -48,14 +45,11 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { resetPasswordAutoEnroll = false; onSuccessfulChangePassword: () => Promise<void>; successRoute = "vault"; - userId: UserId; forceSetPasswordReason: ForceSetPasswordReason = ForceSetPasswordReason.None; ForceSetPasswordReason = ForceSetPasswordReason; constructor( - private accountService: AccountService, - private masterPasswordService: InternalMasterPasswordServiceAbstraction, i18nService: I18nService, cryptoService: CryptoService, messagingService: MessagingService, @@ -94,11 +88,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { await this.syncService.fullSync(true); this.syncLoading = false; - this.userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; - - this.forceSetPasswordReason = await firstValueFrom( - this.masterPasswordService.forceSetPasswordReason$(this.userId), - ); + this.forceSetPasswordReason = await this.stateService.getForceSetPasswordReason(); this.route.queryParams .pipe( @@ -186,6 +176,7 @@ 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 @@ -198,7 +189,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { return this.organizationUserService.putOrganizationUserResetPasswordEnrollment( this.orgId, - this.userId, + userId, resetRequest, ); }); @@ -235,10 +226,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { keyPair: [string, EncString] | null, ) { // Clear force set password reason to allow navigation back to vault. - await this.masterPasswordService.setForceSetPasswordReason( - ForceSetPasswordReason.None, - this.userId, - ); + await this.stateService.setForceSetPasswordReason(ForceSetPasswordReason.None); // User now has a password so update account decryption options in state const userDecryptionOpts = await firstValueFrom( @@ -249,7 +237,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { await this.stateService.setKdfType(this.kdf); await this.stateService.setKdfConfig(this.kdfConfig); - await this.masterPasswordService.setMasterKey(masterKey, this.userId); + await this.cryptoService.setMasterKey(masterKey); await this.cryptoService.setUserKey(userKey[0]); // Set private key only for new JIT provisioned users in MP encryption orgs @@ -267,6 +255,6 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { masterKey, HashPurpose.LocalAuthorization, ); - await this.masterPasswordService.setMasterKeyHash(localMasterKeyHash, this.userId); + await this.cryptoService.setMasterKeyHash(localMasterKeyHash); } } diff --git a/libs/angular/src/auth/components/sso.component.spec.ts b/libs/angular/src/auth/components/sso.component.spec.ts index 269ec51e30..c5c062d9a7 100644 --- a/libs/angular/src/auth/components/sso.component.spec.ts +++ b/libs/angular/src/auth/components/sso.component.spec.ts @@ -12,13 +12,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 { 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 { 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"; @@ -26,9 +23,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic 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 @@ -53,7 +48,6 @@ describe("SsoComponent", () => { let component: TestSsoComponent; let _component: SsoComponentProtected; let fixture: ComponentFixture<TestSsoComponent>; - const userId = "userId" as UserId; // Mock Services let mockLoginStrategyService: MockProxy<LoginStrategyServiceAbstraction>; @@ -73,8 +67,6 @@ describe("SsoComponent", () => { let mockLogService: MockProxy<LogService>; let mockUserDecryptionOptionsService: MockProxy<UserDecryptionOptionsServiceAbstraction>; let mockConfigService: MockProxy<ConfigService>; - let mockMasterPasswordService: FakeMasterPasswordService; - let mockAccountService: FakeAccountService; // Mock authService.logIn params let code: string; @@ -125,8 +117,6 @@ describe("SsoComponent", () => { mockLogService = mock(); mockUserDecryptionOptionsService = mock(); mockConfigService = mock(); - mockAccountService = mockAccountServiceWith(userId); - mockMasterPasswordService = new FakeMasterPasswordService(); // Mock loginStrategyService.logIn params code = "code"; @@ -209,8 +199,6 @@ describe("SsoComponent", () => { }, { provide: LogService, useValue: mockLogService }, { provide: ConfigService, useValue: mockConfigService }, - { provide: InternalMasterPasswordServiceAbstraction, useValue: mockMasterPasswordService }, - { provide: AccountService, useValue: mockAccountService }, ], }); @@ -377,9 +365,8 @@ describe("SsoComponent", () => { await _component.logIn(code, codeVerifier, orgIdFromState); expect(mockLoginStrategyService.logIn).toHaveBeenCalledTimes(1); - expect(mockMasterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledWith( + expect(mockStateService.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 30815beef8..68d6e72e8d 100644 --- a/libs/angular/src/auth/components/sso.component.ts +++ b/libs/angular/src/auth/components/sso.component.ts @@ -11,8 +11,6 @@ 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"; @@ -68,8 +66,6 @@ export class SsoComponent { protected logService: LogService, protected userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, protected configService: ConfigService, - protected masterPasswordService: InternalMasterPasswordServiceAbstraction, - protected accountService: AccountService, ) {} async ngOnInit() { @@ -294,10 +290,8 @@ 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. - const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; - await this.masterPasswordService.setForceSetPasswordReason( + await this.stateService.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 0eb248f6d9..bff39188ea 100644 --- a/libs/angular/src/auth/components/two-factor.component.spec.ts +++ b/libs/angular/src/auth/components/two-factor.component.spec.ts @@ -15,14 +15,11 @@ 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 { 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 { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; @@ -30,8 +27,6 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic 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"; @@ -51,7 +46,6 @@ describe("TwoFactorComponent", () => { let _component: TwoFactorComponentProtected; let fixture: ComponentFixture<TestTwoFactorComponent>; - const userId = "userId" as UserId; // Mock Services let mockLoginStrategyService: MockProxy<LoginStrategyServiceAbstraction>; @@ -69,8 +63,6 @@ describe("TwoFactorComponent", () => { let mockUserDecryptionOptionsService: MockProxy<UserDecryptionOptionsServiceAbstraction>; let mockSsoLoginService: MockProxy<SsoLoginServiceAbstraction>; let mockConfigService: MockProxy<ConfigService>; - let mockMasterPasswordService: FakeMasterPasswordService; - let mockAccountService: FakeAccountService; let mockUserDecryptionOpts: { noMasterPassword: UserDecryptionOptions; @@ -101,8 +93,6 @@ describe("TwoFactorComponent", () => { mockUserDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>(); mockSsoLoginService = mock<SsoLoginServiceAbstraction>(); mockConfigService = mock<ConfigService>(); - mockAccountService = mockAccountServiceWith(userId); - mockMasterPasswordService = new FakeMasterPasswordService(); mockUserDecryptionOpts = { noMasterPassword: new UserDecryptionOptions({ @@ -180,8 +170,6 @@ describe("TwoFactorComponent", () => { }, { provide: SsoLoginServiceAbstraction, useValue: mockSsoLoginService }, { provide: ConfigService, useValue: mockConfigService }, - { provide: InternalMasterPasswordServiceAbstraction, useValue: mockMasterPasswordService }, - { provide: AccountService, useValue: mockAccountService }, ], }); @@ -419,9 +407,9 @@ describe("TwoFactorComponent", () => { await component.doSubmit(); // Assert - expect(mockMasterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledWith( + + expect(mockStateService.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 f73f0483be..c306e6cc80 100644 --- a/libs/angular/src/auth/components/two-factor.component.ts +++ b/libs/angular/src/auth/components/two-factor.component.ts @@ -14,8 +14,6 @@ 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 { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type"; @@ -94,8 +92,6 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI protected userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, protected ssoLoginService: SsoLoginServiceAbstraction, protected configService: ConfigService, - protected masterPasswordService: InternalMasterPasswordServiceAbstraction, - protected accountService: AccountService, ) { super(environmentService, i18nService, platformUtilsService); this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win); @@ -346,10 +342,8 @@ 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. - const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; - await this.masterPasswordService.setForceSetPasswordReason( + await this.stateService.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 54fdc83239..0b4541fe52 100644 --- a/libs/angular/src/auth/components/update-temp-password.component.ts +++ b/libs/angular/src/auth/components/update-temp-password.component.ts @@ -1,12 +1,9 @@ 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"; @@ -59,8 +56,6 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent { private userVerificationService: UserVerificationService, protected router: Router, dialogService: DialogService, - private accountService: AccountService, - private masterPasswordService: InternalMasterPasswordServiceAbstraction, ) { super( i18nService, @@ -77,8 +72,7 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent { async ngOnInit() { await this.syncService.fullSync(true); - const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; - this.reason = await firstValueFrom(this.masterPasswordService.forceSetPasswordReason$(userId)); + this.reason = await this.stateService.getForceSetPasswordReason(); // If we somehow end up here without a reason, go back to the home page if (this.reason == ForceSetPasswordReason.None) { @@ -169,11 +163,7 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent { this.i18nService.t("updatedMasterPassword"), ); - const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; - await this.masterPasswordService.setForceSetPasswordReason( - ForceSetPasswordReason.None, - userId, - ); + await this.stateService.setForceSetPasswordReason(ForceSetPasswordReason.None); 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 b8e37d0af3..29024cfa0b 100644 --- a/libs/angular/src/auth/guards/auth.guard.ts +++ b/libs/angular/src/auth/guards/auth.guard.ts @@ -1,14 +1,12 @@ 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 { @@ -17,8 +15,7 @@ export class AuthGuard implements CanActivate { private router: Router, private messagingService: MessagingService, private keyConnectorService: KeyConnectorService, - private accountService: AccountService, - private masterPasswordService: MasterPasswordServiceAbstraction, + private stateService: StateService, ) {} async canActivate(route: ActivatedRouteSnapshot, routerState: RouterStateSnapshot) { @@ -43,10 +40,7 @@ export class AuthGuard implements CanActivate { return this.router.createUrlTree(["/remove-password"]); } - const userId = (await firstValueFrom(this.accountService.activeAccount$)).id; - const forceSetPasswordReason = await firstValueFrom( - this.masterPasswordService.forceSetPasswordReason$(userId), - ); + const forceSetPasswordReason = await this.stateService.getForceSetPasswordReason(); if ( forceSetPasswordReason === diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 2e35f0d62f..73f2bb4a32 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -60,10 +60,6 @@ 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 { - 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"; @@ -82,7 +78,6 @@ 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 { 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"; @@ -364,8 +359,6 @@ const safeProviders: SafeProvider[] = [ provide: LoginStrategyServiceAbstraction, useClass: LoginStrategyService, deps: [ - AccountServiceAbstraction, - InternalMasterPasswordServiceAbstraction, CryptoServiceAbstraction, ApiServiceAbstraction, TokenServiceAbstraction, @@ -528,7 +521,6 @@ const safeProviders: SafeProvider[] = [ provide: CryptoServiceAbstraction, useClass: CryptoService, deps: [ - InternalMasterPasswordServiceAbstraction, KeyGenerationServiceAbstraction, CryptoFunctionServiceAbstraction, EncryptService, @@ -595,8 +587,6 @@ const safeProviders: SafeProvider[] = [ provide: SyncServiceAbstraction, useClass: SyncService, deps: [ - InternalMasterPasswordServiceAbstraction, - AccountServiceAbstraction, ApiServiceAbstraction, DomainSettingsService, InternalFolderService, @@ -636,8 +626,6 @@ const safeProviders: SafeProvider[] = [ provide: VaultTimeoutService, useClass: VaultTimeoutService, deps: [ - AccountServiceAbstraction, - InternalMasterPasswordServiceAbstraction, CipherServiceAbstraction, FolderServiceAbstraction, CollectionServiceAbstraction, @@ -783,21 +771,10 @@ const safeProviders: SafeProvider[] = [ useClass: PolicyApiService, deps: [InternalPolicyService, ApiServiceAbstraction], }), - safeProvider({ - provide: InternalMasterPasswordServiceAbstraction, - useClass: MasterPasswordService, - deps: [StateProvider], - }), - safeProvider({ - provide: MasterPasswordServiceAbstraction, - useExisting: MasterPasswordServiceAbstraction, - }), safeProvider({ provide: KeyConnectorServiceAbstraction, useClass: KeyConnectorService, deps: [ - AccountServiceAbstraction, - InternalMasterPasswordServiceAbstraction, CryptoServiceAbstraction, ApiServiceAbstraction, TokenServiceAbstraction, @@ -814,8 +791,6 @@ const safeProviders: SafeProvider[] = [ deps: [ StateServiceAbstraction, CryptoServiceAbstraction, - AccountServiceAbstraction, - InternalMasterPasswordServiceAbstraction, I18nServiceAbstraction, UserVerificationApiServiceAbstraction, UserDecryptionOptionsServiceAbstraction, @@ -959,10 +934,9 @@ const safeProviders: SafeProvider[] = [ useClass: AuthRequestService, deps: [ AppIdServiceAbstraction, - AccountServiceAbstraction, - InternalMasterPasswordServiceAbstraction, CryptoServiceAbstraction, ApiServiceAbstraction, + StateServiceAbstraction, ], }), safeProvider({ 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 0ce6c9fed7..53722cd259 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,7 +5,6 @@ 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"; @@ -15,9 +14,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, 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"; @@ -45,10 +42,6 @@ describe("AuthRequestLoginStrategy", () => { let deviceTrustCryptoService: MockProxy<DeviceTrustCryptoServiceAbstraction>; let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>; - const mockUserId = Utils.newGuid() as UserId; - let accountService: FakeAccountService; - let masterPasswordService: FakeMasterPasswordService; - let authRequestLoginStrategy: AuthRequestLoginStrategy; let credentials: AuthRequestLoginCredentials; let tokenResponse: IdentityTokenResponse; @@ -78,17 +71,12 @@ describe("AuthRequestLoginStrategy", () => { deviceTrustCryptoService = mock<DeviceTrustCryptoServiceAbstraction>(); billingAccountProfileStateService = mock<BillingAccountProfileStateService>(); - 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, @@ -120,16 +108,13 @@ 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; - masterPasswordService.masterKeySubject.next(masterKey); + cryptoService.getMasterKey.mockResolvedValue(masterKey); cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey); await authRequestLoginStrategy.logIn(credentials); - expect(masterPasswordService.mock.setMasterKey).toHaveBeenCalledWith(masterKey, mockUserId); - expect(masterPasswordService.mock.setMasterKeyHash).toHaveBeenCalledWith( - decMasterKeyHash, - mockUserId, - ); + expect(cryptoService.setMasterKey).toHaveBeenCalledWith(masterKey); + expect(cryptoService.setMasterKeyHash).toHaveBeenCalledWith(decMasterKeyHash); expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key); expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey); expect(deviceTrustCryptoService.trustDeviceIfRequired).toHaveBeenCalled(); @@ -151,8 +136,8 @@ describe("AuthRequestLoginStrategy", () => { await authRequestLoginStrategy.logIn(credentials); // setMasterKey and setMasterKeyHash should not be called - expect(masterPasswordService.mock.setMasterKey).not.toHaveBeenCalled(); - expect(masterPasswordService.mock.setMasterKeyHash).not.toHaveBeenCalled(); + expect(cryptoService.setMasterKey).not.toHaveBeenCalled(); + expect(cryptoService.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 e47f0f88ee..31a0cebbfe 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,10 +1,8 @@ -import { firstValueFrom, Observable, map, BehaviorSubject } from "rxjs"; +import { 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"; @@ -49,8 +47,6 @@ export class AuthRequestLoginStrategy extends LoginStrategy { constructor( data: AuthRequestLoginStrategyData, - accountService: AccountService, - masterPasswordService: InternalMasterPasswordServiceAbstraction, cryptoService: CryptoService, apiService: ApiService, tokenService: TokenService, @@ -65,8 +61,6 @@ export class AuthRequestLoginStrategy extends LoginStrategy { billingAccountProfileStateService: BillingAccountProfileStateService, ) { super( - accountService, - masterPasswordService, cryptoService, apiService, tokenService, @@ -120,15 +114,8 @@ export class AuthRequestLoginStrategy extends LoginStrategy { authRequestCredentials.decryptedMasterKey && authRequestCredentials.decryptedMasterKeyHash ) { - const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; - await this.masterPasswordService.setMasterKey( - authRequestCredentials.decryptedMasterKey, - userId, - ); - await this.masterPasswordService.setMasterKeyHash( - authRequestCredentials.decryptedMasterKeyHash, - userId, - ); + await this.cryptoService.setMasterKey(authRequestCredentials.decryptedMasterKey); + await this.cryptoService.setMasterKeyHash(authRequestCredentials.decryptedMasterKeyHash); } } @@ -150,8 +137,7 @@ export class AuthRequestLoginStrategy extends LoginStrategy { } private async trySetUserKeyWithMasterKey(): Promise<void> { - const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; - const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); + const masterKey = await this.cryptoService.getMasterKey(); 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 431f736e94..0ac22047c5 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.spec.ts @@ -14,7 +14,6 @@ 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"; @@ -32,13 +31,11 @@ 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 { UserId } from "@bitwarden/common/types/guid"; import { UserKey, MasterKey } from "@bitwarden/common/types/key"; import { LoginStrategyServiceAbstraction } from "../abstractions"; @@ -59,7 +56,7 @@ const privateKey = "PRIVATE_KEY"; const captchaSiteKey = "CAPTCHA_SITE_KEY"; const kdf = 0; const kdfIterations = 10000; -const userId = Utils.newGuid() as UserId; +const userId = Utils.newGuid(); const masterPasswordHash = "MASTER_PASSWORD_HASH"; const name = "NAME"; const defaultUserDecryptionOptionsServerResponse: IUserDecryptionOptionsServerResponse = { @@ -101,8 +98,6 @@ 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<LoginStrategyServiceAbstraction>; let cryptoService: MockProxy<CryptoService>; @@ -123,9 +118,6 @@ describe("LoginStrategy", () => { let credentials: PasswordLoginCredentials; beforeEach(async () => { - accountService = mockAccountServiceWith(userId); - masterPasswordService = new FakeMasterPasswordService(); - loginStrategyService = mock<LoginStrategyServiceAbstraction>(); cryptoService = mock<CryptoService>(); apiService = mock<ApiService>(); @@ -147,8 +139,6 @@ describe("LoginStrategy", () => { // The base class is abstract so we test it via PasswordLoginStrategy passwordLoginStrategy = new PasswordLoginStrategy( cache, - accountService, - masterPasswordService, cryptoService, apiService, tokenService, @@ -251,7 +241,7 @@ describe("LoginStrategy", () => { }); apiService.postIdentityToken.mockResolvedValue(tokenResponse); - masterPasswordService.masterKeySubject.next(masterKey); + cryptoService.getMasterKey.mockResolvedValue(masterKey); cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey); const result = await passwordLoginStrategy.logIn(credentials); @@ -270,7 +260,7 @@ describe("LoginStrategy", () => { cryptoService.makeKeyPair.mockResolvedValue(["PUBLIC_KEY", new EncString("PRIVATE_KEY")]); apiService.postIdentityToken.mockResolvedValue(tokenResponse); - masterPasswordService.masterKeySubject.next(masterKey); + cryptoService.getMasterKey.mockResolvedValue(masterKey); cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey); await passwordLoginStrategy.logIn(credentials); @@ -392,8 +382,6 @@ 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 df6aa171db..4fe99b276c 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.ts @@ -1,8 +1,6 @@ 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"; @@ -62,8 +60,6 @@ export abstract class LoginStrategy { protected abstract cache: BehaviorSubject<LoginStrategyData>; constructor( - protected accountService: AccountService, - protected masterPasswordService: InternalMasterPasswordServiceAbstraction, protected cryptoService: CryptoService, protected apiService: ApiService, protected tokenService: TokenService, 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 b902fff574..470a4ac713 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,7 +9,6 @@ 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"; @@ -20,13 +19,11 @@ 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"; @@ -45,7 +42,6 @@ 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, @@ -54,8 +50,6 @@ const masterPasswordPolicy = new MasterPasswordPolicyResponse({ describe("PasswordLoginStrategy", () => { let cache: PasswordLoginStrategyData; - let accountService: FakeAccountService; - let masterPasswordService: FakeMasterPasswordService; let loginStrategyService: MockProxy<LoginStrategyServiceAbstraction>; let cryptoService: MockProxy<CryptoService>; @@ -77,9 +71,6 @@ describe("PasswordLoginStrategy", () => { let tokenResponse: IdentityTokenResponse; beforeEach(async () => { - accountService = mockAccountServiceWith(userId); - masterPasswordService = new FakeMasterPasswordService(); - loginStrategyService = mock<LoginStrategyServiceAbstraction>(); cryptoService = mock<CryptoService>(); apiService = mock<ApiService>(); @@ -111,8 +102,6 @@ describe("PasswordLoginStrategy", () => { passwordLoginStrategy = new PasswordLoginStrategy( cache, - accountService, - masterPasswordService, cryptoService, apiService, tokenService, @@ -156,16 +145,13 @@ describe("PasswordLoginStrategy", () => { it("sets keys after a successful authentication", async () => { const userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey; - masterPasswordService.masterKeySubject.next(masterKey); + cryptoService.getMasterKey.mockResolvedValue(masterKey); cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey); await passwordLoginStrategy.logIn(credentials); - expect(masterPasswordService.mock.setMasterKey).toHaveBeenCalledWith(masterKey, userId); - expect(masterPasswordService.mock.setMasterKeyHash).toHaveBeenCalledWith( - localHashedPassword, - userId, - ); + expect(cryptoService.setMasterKey).toHaveBeenCalledWith(masterKey); + expect(cryptoService.setMasterKeyHash).toHaveBeenCalledWith(localHashedPassword); expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key); expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey); expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey); @@ -197,9 +183,8 @@ describe("PasswordLoginStrategy", () => { const result = await passwordLoginStrategy.logIn(credentials); expect(policyService.evaluateMasterPassword).toHaveBeenCalled(); - expect(masterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledWith( + expect(stateService.setForceSetPasswordReason).toHaveBeenCalledWith( ForceSetPasswordReason.WeakMasterPassword, - userId, ); expect(result.forcePasswordReset).toEqual(ForceSetPasswordReason.WeakMasterPassword); }); @@ -237,9 +222,8 @@ describe("PasswordLoginStrategy", () => { expect(firstResult.forcePasswordReset).toEqual(ForceSetPasswordReason.None); // Second login attempt should save the force password reset options and return in result - expect(masterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledWith( + expect(stateService.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 52c97d5d85..d3de3ea6ba 100644 --- a/libs/auth/src/common/login-strategies/password-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/password-login.strategy.ts @@ -1,11 +1,9 @@ -import { BehaviorSubject, firstValueFrom, map, Observable } from "rxjs"; +import { BehaviorSubject, 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"; @@ -72,8 +70,6 @@ export class PasswordLoginStrategy extends LoginStrategy { constructor( data: PasswordLoginStrategyData, - accountService: AccountService, - masterPasswordService: InternalMasterPasswordServiceAbstraction, cryptoService: CryptoService, apiService: ApiService, tokenService: TokenService, @@ -90,8 +86,6 @@ export class PasswordLoginStrategy extends LoginStrategy { billingAccountProfileStateService: BillingAccountProfileStateService, ) { super( - accountService, - masterPasswordService, cryptoService, apiService, tokenService, @@ -163,10 +157,8 @@ export class PasswordLoginStrategy extends LoginStrategy { }); } else { // Authentication was successful, save the force update password options with the state service - const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; - await this.masterPasswordService.setForceSetPasswordReason( + await this.stateService.setForceSetPasswordReason( ForceSetPasswordReason.WeakMasterPassword, - userId, ); authResult.forcePasswordReset = ForceSetPasswordReason.WeakMasterPassword; } @@ -192,8 +184,7 @@ export class PasswordLoginStrategy extends LoginStrategy { !result.requiresCaptcha && forcePasswordResetReason != ForceSetPasswordReason.None ) { - const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; - await this.masterPasswordService.setForceSetPasswordReason(forcePasswordResetReason, userId); + await this.stateService.setForceSetPasswordReason(forcePasswordResetReason); result.forcePasswordReset = forcePasswordResetReason; } @@ -202,9 +193,8 @@ export class PasswordLoginStrategy extends LoginStrategy { protected override async setMasterKey(response: IdentityTokenResponse) { const { masterKey, localMasterKeyHash } = this.cache.value; - const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; - await this.masterPasswordService.setMasterKey(masterKey, userId); - await this.masterPasswordService.setMasterKeyHash(localMasterKeyHash, userId); + await this.cryptoService.setMasterKey(masterKey); + await this.cryptoService.setMasterKeyHash(localMasterKeyHash); } protected override async setUserKey(response: IdentityTokenResponse): Promise<void> { @@ -214,8 +204,7 @@ export class PasswordLoginStrategy extends LoginStrategy { } await this.cryptoService.setMasterKeyEncryptedUserKey(response.key); - const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; - const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); + const masterKey = await this.cryptoService.getMasterKey(); 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 bce62681d0..d4b0b13eaf 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,7 +9,6 @@ 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"; @@ -21,9 +20,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, 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 { @@ -36,9 +33,6 @@ import { identityTokenResponseFactory } from "./login.strategy.spec"; import { SsoLoginStrategy } from "./sso-login.strategy"; describe("SsoLoginStrategy", () => { - let accountService: FakeAccountService; - let masterPasswordService: FakeMasterPasswordService; - let cryptoService: MockProxy<CryptoService>; let apiService: MockProxy<ApiService>; let tokenService: MockProxy<TokenService>; @@ -58,7 +52,6 @@ describe("SsoLoginStrategy", () => { let ssoLoginStrategy: SsoLoginStrategy; let credentials: SsoLoginCredentials; - const userId = Utils.newGuid() as UserId; const deviceId = Utils.newGuid(); const keyConnectorUrl = "KEY_CONNECTOR_URL"; @@ -68,9 +61,6 @@ describe("SsoLoginStrategy", () => { const ssoOrgId = "SSO_ORG_ID"; beforeEach(async () => { - accountService = mockAccountServiceWith(userId); - masterPasswordService = new FakeMasterPasswordService(); - cryptoService = mock<CryptoService>(); apiService = mock<ApiService>(); tokenService = mock<TokenService>(); @@ -93,8 +83,6 @@ describe("SsoLoginStrategy", () => { ssoLoginStrategy = new SsoLoginStrategy( null, - accountService, - masterPasswordService, cryptoService, apiService, tokenService, @@ -142,7 +130,7 @@ describe("SsoLoginStrategy", () => { await ssoLoginStrategy.logIn(credentials); - expect(masterPasswordService.mock.setMasterKey).not.toHaveBeenCalled(); + expect(cryptoService.setMasterKey).not.toHaveBeenCalled(); expect(cryptoService.setUserKey).not.toHaveBeenCalled(); expect(cryptoService.setPrivateKey).not.toHaveBeenCalled(); }); @@ -407,7 +395,7 @@ describe("SsoLoginStrategy", () => { ) as MasterKey; apiService.postIdentityToken.mockResolvedValue(tokenResponse); - masterPasswordService.masterKeySubject.next(masterKey); + cryptoService.getMasterKey.mockResolvedValue(masterKey); await ssoLoginStrategy.logIn(credentials); @@ -434,7 +422,7 @@ describe("SsoLoginStrategy", () => { ) as MasterKey; apiService.postIdentityToken.mockResolvedValue(tokenResponse); - masterPasswordService.masterKeySubject.next(masterKey); + cryptoService.getMasterKey.mockResolvedValue(masterKey); cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey); await ssoLoginStrategy.logIn(credentials); @@ -458,7 +446,7 @@ describe("SsoLoginStrategy", () => { ) as MasterKey; apiService.postIdentityToken.mockResolvedValue(tokenResponse); - masterPasswordService.masterKeySubject.next(masterKey); + cryptoService.getMasterKey.mockResolvedValue(masterKey); await ssoLoginStrategy.logIn(credentials); @@ -485,7 +473,7 @@ describe("SsoLoginStrategy", () => { ) as MasterKey; apiService.postIdentityToken.mockResolvedValue(tokenResponse); - masterPasswordService.masterKeySubject.next(masterKey); + cryptoService.getMasterKey.mockResolvedValue(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 db0228a338..7745104bd1 100644 --- a/libs/auth/src/common/login-strategies/sso-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/sso-login.strategy.ts @@ -1,11 +1,9 @@ -import { firstValueFrom, Observable, map, BehaviorSubject } from "rxjs"; +import { 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"; @@ -81,8 +79,6 @@ export class SsoLoginStrategy extends LoginStrategy { constructor( data: SsoLoginStrategyData, - accountService: AccountService, - masterPasswordService: InternalMasterPasswordServiceAbstraction, cryptoService: CryptoService, apiService: ApiService, tokenService: TokenService, @@ -100,8 +96,6 @@ export class SsoLoginStrategy extends LoginStrategy { billingAccountProfileStateService: BillingAccountProfileStateService, ) { super( - accountService, - masterPasswordService, cryptoService, apiService, tokenService, @@ -144,11 +138,7 @@ export class SsoLoginStrategy extends LoginStrategy { // Auth guard currently handles redirects for this. if (ssoAuthResult.forcePasswordReset == ForceSetPasswordReason.AdminForcePasswordReset) { - const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; - await this.masterPasswordService.setForceSetPasswordReason( - ssoAuthResult.forcePasswordReset, - userId, - ); + await this.stateService.setForceSetPasswordReason(ssoAuthResult.forcePasswordReset); } this.cache.next({ @@ -333,8 +323,7 @@ export class SsoLoginStrategy extends LoginStrategy { } private async trySetUserKeyWithMasterKey(): Promise<void> { - const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; - const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); + const masterKey = await this.cryptoService.getMasterKey(); // 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 5e7d7985b1..02aed305a4 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,7 +5,6 @@ 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"; @@ -20,9 +19,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, 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"; @@ -33,8 +30,6 @@ import { UserApiLoginStrategy, UserApiLoginStrategyData } from "./user-api-login describe("UserApiLoginStrategy", () => { let cache: UserApiLoginStrategyData; - let accountService: FakeAccountService; - let masterPasswordService: FakeMasterPasswordService; let cryptoService: MockProxy<CryptoService>; let apiService: MockProxy<ApiService>; @@ -53,16 +48,12 @@ 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<CryptoService>(); apiService = mock<ApiService>(); tokenService = mock<TokenService>(); @@ -83,8 +74,6 @@ describe("UserApiLoginStrategy", () => { apiLogInStrategy = new UserApiLoginStrategy( cache, - accountService, - masterPasswordService, cryptoService, apiService, tokenService, @@ -183,7 +172,7 @@ describe("UserApiLoginStrategy", () => { environmentService.environment$ = new BehaviorSubject(env); apiService.postIdentityToken.mockResolvedValue(tokenResponse); - masterPasswordService.masterKeySubject.next(masterKey); + cryptoService.getMasterKey.mockResolvedValue(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 421746b49c..2af666f95c 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,9 +2,7 @@ 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"; @@ -41,8 +39,6 @@ export class UserApiLoginStrategy extends LoginStrategy { constructor( data: UserApiLoginStrategyData, - accountService: AccountService, - masterPasswordService: InternalMasterPasswordServiceAbstraction, cryptoService: CryptoService, apiService: ApiService, tokenService: TokenService, @@ -58,8 +54,6 @@ export class UserApiLoginStrategy extends LoginStrategy { billingAccountProfileStateService: BillingAccountProfileStateService, ) { super( - accountService, - masterPasswordService, cryptoService, apiService, tokenService, @@ -101,8 +95,7 @@ export class UserApiLoginStrategy extends LoginStrategy { await this.cryptoService.setMasterKeyEncryptedUserKey(response.key); if (response.apiUseKeyConnector) { - const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; - const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); + const masterKey = await this.cryptoService.getMasterKey(); if (masterKey) { const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey); await this.cryptoService.setUserKey(userKey); 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 1d96921286..edc1441361 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,7 +6,6 @@ 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"; @@ -17,7 +16,6 @@ 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"; @@ -28,8 +26,6 @@ import { WebAuthnLoginStrategy, WebAuthnLoginStrategyData } from "./webauthn-log describe("WebAuthnLoginStrategy", () => { let cache: WebAuthnLoginStrategyData; - let accountService: FakeAccountService; - let masterPasswordService: FakeMasterPasswordService; let cryptoService!: MockProxy<CryptoService>; let apiService!: MockProxy<ApiService>; @@ -67,9 +63,6 @@ describe("WebAuthnLoginStrategy", () => { beforeEach(() => { jest.clearAllMocks(); - accountService = new FakeAccountService(null); - masterPasswordService = new FakeMasterPasswordService(); - cryptoService = mock<CryptoService>(); apiService = mock<ApiService>(); tokenService = mock<TokenService>(); @@ -88,8 +81,6 @@ describe("WebAuthnLoginStrategy", () => { webAuthnLoginStrategy = new WebAuthnLoginStrategy( cache, - accountService, - masterPasswordService, cryptoService, apiService, tokenService, @@ -216,7 +207,7 @@ describe("WebAuthnLoginStrategy", () => { expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(idTokenResponse.privateKey); // Master key and private key should not be set - expect(masterPasswordService.mock.setMasterKey).not.toHaveBeenCalled(); + expect(cryptoService.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 843978e2a2..a8e67597b8 100644 --- a/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts @@ -2,8 +2,6 @@ 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"; @@ -43,8 +41,6 @@ export class WebAuthnLoginStrategy extends LoginStrategy { constructor( data: WebAuthnLoginStrategyData, - accountService: AccountService, - masterPasswordService: InternalMasterPasswordServiceAbstraction, cryptoService: CryptoService, apiService: ApiService, tokenService: TokenService, @@ -58,8 +54,6 @@ export class WebAuthnLoginStrategy extends LoginStrategy { billingAccountProfileStateService: BillingAccountProfileStateService, ) { super( - accountService, - masterPasswordService, cryptoService, apiService, tokenService, 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 f04628ffd9..80d00b2a01 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,15 +2,13 @@ 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 { 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"; @@ -18,27 +16,17 @@ import { AuthRequestService } from "./auth-request.service"; describe("AuthRequestService", () => { let sut: AuthRequestService; - let accountService: FakeAccountService; - let masterPasswordService: FakeMasterPasswordService; const appIdService = mock<AppIdService>(); const cryptoService = mock<CryptoService>(); const apiService = mock<ApiService>(); + const stateService = mock<StateService>(); let mockPrivateKey: Uint8Array; - const mockUserId = Utils.newGuid() as UserId; beforeEach(() => { jest.clearAllMocks(); - accountService = mockAccountServiceWith(mockUserId); - masterPasswordService = new FakeMasterPasswordService(); - sut = new AuthRequestService( - appIdService, - accountService, - masterPasswordService, - cryptoService, - apiService, - ); + sut = new AuthRequestService(appIdService, cryptoService, apiService, stateService); mockPrivateKey = new Uint8Array(64); }); @@ -79,8 +67,8 @@ describe("AuthRequestService", () => { }); it("should use the master key and hash if they exist", async () => { - masterPasswordService.masterKeySubject.next({ encKey: new Uint8Array(64) } as MasterKey); - masterPasswordService.masterKeyHashSubject.next("MASTER_KEY_HASH"); + cryptoService.getMasterKey.mockResolvedValueOnce({ encKey: new Uint8Array(64) } as MasterKey); + stateService.getKeyHash.mockResolvedValueOnce("KEY_HASH"); await sut.approveOrDenyAuthRequest( true, @@ -142,8 +130,8 @@ describe("AuthRequestService", () => { masterKeyHash: mockDecryptedMasterKeyHash, }); - masterPasswordService.masterKeySubject.next(undefined); - masterPasswordService.masterKeyHashSubject.next(undefined); + cryptoService.setMasterKey.mockResolvedValueOnce(undefined); + cryptoService.setMasterKeyHash.mockResolvedValueOnce(undefined); cryptoService.decryptUserKeyWithMasterKey.mockResolvedValueOnce(mockDecryptedUserKey); cryptoService.setUserKey.mockResolvedValueOnce(undefined); @@ -156,18 +144,10 @@ describe("AuthRequestService", () => { mockAuthReqResponse.masterPasswordHash, mockPrivateKey, ); - expect(masterPasswordService.mock.setMasterKey).toHaveBeenCalledWith( - mockDecryptedMasterKey, - mockUserId, - ); - expect(masterPasswordService.mock.setMasterKeyHash).toHaveBeenCalledWith( - mockDecryptedMasterKeyHash, - mockUserId, - ); - expect(cryptoService.decryptUserKeyWithMasterKey).toHaveBeenCalledWith( - mockDecryptedMasterKey, - ); - expect(cryptoService.setUserKey).toHaveBeenCalledWith(mockDecryptedUserKey); + expect(cryptoService.setMasterKey).toBeCalledWith(mockDecryptedMasterKey); + expect(cryptoService.setMasterKeyHash).toBeCalledWith(mockDecryptedMasterKeyHash); + expect(cryptoService.decryptUserKeyWithMasterKey).toBeCalledWith(mockDecryptedMasterKey); + expect(cryptoService.setUserKey).toBeCalledWith(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 5f8dcfd729..eb39659f53 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,13 +1,12 @@ -import { firstValueFrom, Observable, Subject } from "rxjs"; +import { Observable, Subject } 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 { 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 { MasterKey, UserKey } from "@bitwarden/common/types/key"; @@ -20,10 +19,9 @@ export class AuthRequestService implements AuthRequestServiceAbstraction { constructor( private appIdService: AppIdService, - private accountService: AccountService, - private masterPasswordService: InternalMasterPasswordServiceAbstraction, private cryptoService: CryptoService, private apiService: ApiService, + private stateService: StateService, ) { this.authRequestPushNotification$ = this.authRequestPushNotificationSubject.asObservable(); } @@ -40,9 +38,8 @@ export class AuthRequestService implements AuthRequestServiceAbstraction { } const pubKey = Utils.fromB64ToArray(authRequest.publicKey); - const userId = (await firstValueFrom(this.accountService.activeAccount$)).id; - const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); - const masterKeyHash = await firstValueFrom(this.masterPasswordService.masterKeyHash$(userId)); + const masterKey = await this.cryptoService.getMasterKey(); + const masterKeyHash = await this.stateService.getKeyHash(); let encryptedMasterKeyHash; let keyToEncrypt; @@ -95,9 +92,8 @@ export class AuthRequestService implements AuthRequestServiceAbstraction { const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey); // Set masterKey + masterKeyHash in state after decryption (in case decryption fails) - const userId = (await firstValueFrom(this.accountService.activeAccount$)).id; - await this.masterPasswordService.setMasterKey(masterKey, userId); - await this.masterPasswordService.setMasterKeyHash(masterKeyHash, userId); + await this.cryptoService.setMasterKey(masterKey); + await this.cryptoService.setMasterKeyHash(masterKeyHash); await this.cryptoService.setUserKey(userKey); } 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 fcc0220d0a..981e4d81ac 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,7 +11,6 @@ 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"; @@ -23,14 +22,8 @@ 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 { - FakeAccountService, - FakeGlobalState, - FakeGlobalStateProvider, - mockAccountServiceWith, -} from "@bitwarden/common/spec"; +import { FakeGlobalState, FakeGlobalStateProvider } from "@bitwarden/common/spec"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; -import { UserId } from "@bitwarden/common/types/guid"; import { AuthRequestServiceAbstraction, @@ -45,8 +38,6 @@ import { CACHE_EXPIRATION_KEY } from "./login-strategy.state"; describe("LoginStrategyService", () => { let sut: LoginStrategyService; - let accountService: FakeAccountService; - let masterPasswordService: FakeMasterPasswordService; let cryptoService: MockProxy<CryptoService>; let apiService: MockProxy<ApiService>; let tokenService: MockProxy<TokenService>; @@ -70,11 +61,7 @@ describe("LoginStrategyService", () => { let stateProvider: FakeGlobalStateProvider; let loginStrategyCacheExpirationState: FakeGlobalState<Date | null>; - const userId = "USER_ID" as UserId; - beforeEach(() => { - accountService = mockAccountServiceWith(userId); - masterPasswordService = new FakeMasterPasswordService(); cryptoService = mock<CryptoService>(); apiService = mock<ApiService>(); tokenService = mock<TokenService>(); @@ -97,8 +84,6 @@ 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 a8bd7bc2ff..b55f38af7f 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 @@ -9,10 +9,8 @@ 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"; @@ -83,8 +81,6 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { currentAuthType$: Observable<AuthenticationType | null>; constructor( - protected accountService: AccountService, - protected masterPasswordService: InternalMasterPasswordServiceAbstraction, protected cryptoService: CryptoService, protected apiService: ApiService, protected tokenService: TokenService, @@ -261,8 +257,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { ): Promise<AuthRequestResponse> { const pubKey = Utils.fromB64ToArray(key); - const userId = (await firstValueFrom(this.accountService.activeAccount$)).id; - const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); + const masterKey = await this.cryptoService.getMasterKey(); let keyToEncrypt; let encryptedMasterKeyHash = null; @@ -271,7 +266,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 firstValueFrom(this.masterPasswordService.masterKeyHash$(userId)); + const masterKeyHash = await this.stateService.getKeyHash(); if (masterKeyHash != null) { encryptedMasterKeyHash = await this.cryptoService.rsaEncrypt( Utils.fromUtf8ToArray(masterKeyHash), @@ -338,8 +333,6 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { case AuthenticationType.Password: return new PasswordLoginStrategy( data?.password, - this.accountService, - this.masterPasswordService, this.cryptoService, this.apiService, this.tokenService, @@ -358,8 +351,6 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { case AuthenticationType.Sso: return new SsoLoginStrategy( data?.sso, - this.accountService, - this.masterPasswordService, this.cryptoService, this.apiService, this.tokenService, @@ -379,8 +370,6 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { case AuthenticationType.UserApiKey: return new UserApiLoginStrategy( data?.userApiKey, - this.accountService, - this.masterPasswordService, this.cryptoService, this.apiService, this.tokenService, @@ -398,8 +387,6 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { case AuthenticationType.AuthRequest: return new AuthRequestLoginStrategy( data?.authRequest, - this.accountService, - this.masterPasswordService, this.cryptoService, this.apiService, this.tokenService, @@ -416,8 +403,6 @@ 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/common/src/auth/abstractions/master-password.service.abstraction.ts b/libs/common/src/auth/abstractions/master-password.service.abstraction.ts deleted file mode 100644 index 44fda403c6..0000000000 --- a/libs/common/src/auth/abstractions/master-password.service.abstraction.ts +++ /dev/null @@ -1,67 +0,0 @@ -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<ForceSetPasswordReason>; - /** - * 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<MasterKey>; - /** - * 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<string>; - /** - * 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<EncString>; -} - -export abstract class InternalMasterPasswordServiceAbstraction extends MasterPasswordServiceAbstraction { - /** - * Set the master key for the user. - * @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<void>; - /** - * Set the master key hash for the user. - * @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<void>; - /** - * 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<void>; - /** - * 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<void>; -} diff --git a/libs/common/src/auth/services/key-connector.service.spec.ts b/libs/common/src/auth/services/key-connector.service.spec.ts index e3e5fbdbe7..50fed856f9 100644 --- a/libs/common/src/auth/services/key-connector.service.spec.ts +++ b/libs/common/src/auth/services/key-connector.service.spec.ts @@ -21,7 +21,6 @@ import { 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", () => { @@ -37,7 +36,6 @@ describe("KeyConnectorService", () => { let stateProvider: FakeStateProvider; let accountService: FakeAccountService; - let masterPasswordService: FakeMasterPasswordService; const mockUserId = Utils.newGuid() as UserId; const mockOrgId = Utils.newGuid() as OrganizationId; @@ -49,13 +47,10 @@ describe("KeyConnectorService", () => { beforeEach(() => { jest.clearAllMocks(); - masterPasswordService = new FakeMasterPasswordService(); accountService = mockAccountServiceWith(mockUserId); stateProvider = new FakeStateProvider(accountService); keyConnectorService = new KeyConnectorService( - accountService, - masterPasswordService, cryptoService, apiService, tokenService, @@ -219,10 +214,7 @@ describe("KeyConnectorService", () => { // Assert expect(apiService.getMasterKeyFromKeyConnector).toHaveBeenCalledWith(url); - expect(masterPasswordService.mock.setMasterKey).toHaveBeenCalledWith( - masterKey, - expect.any(String), - ); + expect(cryptoService.setMasterKey).toHaveBeenCalledWith(masterKey); }); it("should handle errors thrown during the process", async () => { @@ -249,10 +241,10 @@ describe("KeyConnectorService", () => { // 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(cryptoService, "getMasterKey").mockResolvedValue(masterKey); jest.spyOn(apiService, "postUserKeyToKeyConnector").mockResolvedValue(); // Act @@ -260,6 +252,7 @@ describe("KeyConnectorService", () => { // Assert expect(keyConnectorService.getManagingOrganization).toHaveBeenCalled(); + expect(cryptoService.getMasterKey).toHaveBeenCalled(); expect(apiService.postUserKeyToKeyConnector).toHaveBeenCalledWith( organization.keyConnectorUrl, keyConnectorRequest, @@ -275,8 +268,8 @@ describe("KeyConnectorService", () => { 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(cryptoService, "getMasterKey").mockResolvedValue(masterKey); jest.spyOn(apiService, "postUserKeyToKeyConnector").mockRejectedValue(error); jest.spyOn(logService, "error"); @@ -287,6 +280,7 @@ describe("KeyConnectorService", () => { // Assert expect(logService.error).toHaveBeenCalledWith(error); expect(keyConnectorService.getManagingOrganization).toHaveBeenCalled(); + expect(cryptoService.getMasterKey).toHaveBeenCalled(); expect(apiService.postUserKeyToKeyConnector).toHaveBeenCalledWith( organization.keyConnectorUrl, keyConnectorRequest, diff --git a/libs/common/src/auth/services/key-connector.service.ts b/libs/common/src/auth/services/key-connector.service.ts index f8e523cce4..d1502ce06c 100644 --- a/libs/common/src/auth/services/key-connector.service.ts +++ b/libs/common/src/auth/services/key-connector.service.ts @@ -16,9 +16,7 @@ import { 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"; @@ -47,8 +45,6 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { private usesKeyConnectorState: ActiveUserState<boolean>; private convertAccountToKeyConnectorState: ActiveUserState<boolean>; constructor( - private accountService: AccountService, - private masterPasswordService: InternalMasterPasswordServiceAbstraction, private cryptoService: CryptoService, private apiService: ApiService, private tokenService: TokenService, @@ -82,8 +78,7 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { async migrateUser() { const organization = await this.getManagingOrganization(); - const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; - const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); + const masterKey = await this.cryptoService.getMasterKey(); const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64); try { @@ -104,8 +99,7 @@ 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; - const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; - await this.masterPasswordService.setMasterKey(masterKey, userId); + await this.cryptoService.setMasterKey(masterKey); } catch (e) { this.handleKeyConnectorError(e); } @@ -142,8 +136,7 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { kdfConfig, ); const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64); - const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; - await this.masterPasswordService.setMasterKey(masterKey, userId); + await this.cryptoService.setMasterKey(masterKey); const userKey = await this.cryptoService.makeUserKey(masterKey); await this.cryptoService.setUserKey(userKey[0]); 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 deleted file mode 100644 index f060fe1db1..0000000000 --- a/libs/common/src/auth/services/master-password/fake-master-password.service.ts +++ /dev/null @@ -1,56 +0,0 @@ -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<InternalMasterPasswordServiceAbstraction>(); - - // eslint-disable-next-line rxjs/no-exposed-subjects -- test class - masterKeySubject = new ReplaySubject<MasterKey>(1); - // eslint-disable-next-line rxjs/no-exposed-subjects -- test class - masterKeyHashSubject = new ReplaySubject<string>(1); - // eslint-disable-next-line rxjs/no-exposed-subjects -- test class - forceSetPasswordReasonSubject = new ReplaySubject<ForceSetPasswordReason>(1); - - constructor(initialMasterKey?: MasterKey, initialMasterKeyHash?: string) { - this.masterKeySubject.next(initialMasterKey); - this.masterKeyHashSubject.next(initialMasterKeyHash); - } - - masterKey$(userId: UserId): Observable<MasterKey> { - return this.masterKeySubject.asObservable(); - } - - setMasterKey(masterKey: MasterKey, userId: UserId): Promise<void> { - return this.mock.setMasterKey(masterKey, userId); - } - - masterKeyHash$(userId: UserId): Observable<string> { - return this.masterKeyHashSubject.asObservable(); - } - - getMasterKeyEncryptedUserKey(userId: UserId): Promise<EncString> { - return this.mock.getMasterKeyEncryptedUserKey(userId); - } - - setMasterKeyEncryptedUserKey(encryptedKey: EncString, userId: UserId): Promise<void> { - return this.mock.setMasterKeyEncryptedUserKey(encryptedKey, userId); - } - - setMasterKeyHash(masterKeyHash: string, userId: UserId): Promise<void> { - return this.mock.setMasterKeyHash(masterKeyHash, userId); - } - - forceSetPasswordReason$(userId: UserId): Observable<ForceSetPasswordReason> { - return this.forceSetPasswordReasonSubject.asObservable(); - } - - setForceSetPasswordReason(reason: ForceSetPasswordReason, userId: UserId): Promise<void> { - 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 deleted file mode 100644 index d204a26570..0000000000 --- a/libs/common/src/auth/services/master-password/master-password.service.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { firstValueFrom, map, Observable } from "rxjs"; - -import { 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<MasterKey>(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<string>(MASTER_PASSWORD_DISK, "masterKeyHash", { - deserializer: (masterKeyHash) => masterKeyHash, - clearOn: ["logout"], -}); - -const MASTER_KEY_ENCRYPTED_USER_KEY = new UserKeyDefinition<EncString>( - MASTER_PASSWORD_DISK, - "masterKeyEncryptedUserKey", - { - deserializer: (key) => EncString.fromJSON(key), - clearOn: ["logout"], - }, -); - -/** Disk to persist through lock and account switches */ -const FORCE_SET_PASSWORD_REASON = new UserKeyDefinition<ForceSetPasswordReason>( - MASTER_PASSWORD_DISK, - "forceSetPasswordReason", - { - deserializer: (reason) => reason, - clearOn: ["logout"], - }, -); - -export class MasterPasswordService implements InternalMasterPasswordServiceAbstraction { - constructor(private stateProvider: StateProvider) {} - - masterKey$(userId: UserId): Observable<MasterKey> { - if (userId == null) { - throw new Error("User ID is required."); - } - return this.stateProvider.getUser(userId, MASTER_KEY).state$; - } - - masterKeyHash$(userId: UserId): Observable<string> { - if (userId == null) { - throw new Error("User ID is required."); - } - return this.stateProvider.getUser(userId, MASTER_KEY_HASH).state$; - } - - forceSetPasswordReason$(userId: UserId): Observable<ForceSetPasswordReason> { - 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<EncString> { - 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 key; - } - - async setMasterKey(masterKey: MasterKey, userId: UserId): Promise<void> { - 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 setMasterKeyHash(masterKeyHash: string, userId: UserId): Promise<void> { - 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 setMasterKeyEncryptedUserKey(encryptedKey: EncString, userId: UserId): Promise<void> { - 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); - } - - async setForceSetPasswordReason(reason: ForceSetPasswordReason, userId: UserId): Promise<void> { - 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/user-verification/user-verification.service.ts b/libs/common/src/auth/services/user-verification/user-verification.service.ts index 5a443b784d..0b4cd96099 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,10 +10,7 @@ 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"; @@ -38,8 +35,6 @@ 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, @@ -112,8 +107,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti if (verification.type === VerificationType.OTP) { request.otp = verification.secret; } else { - const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; - let masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); + let masterKey = await this.cryptoService.getMasterKey(); if (!masterKey && !alreadyHashed) { masterKey = await this.cryptoService.makeMasterKey( verification.secret, @@ -170,8 +164,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti private async verifyUserByMasterPassword( verification: MasterPasswordVerification, ): Promise<boolean> { - const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; - let masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); + let masterKey = await this.cryptoService.getMasterKey(); if (!masterKey) { masterKey = await this.cryptoService.makeMasterKey( verification.secret, @@ -188,7 +181,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.masterPasswordService.setMasterKey(masterKey, userId); + await this.cryptoService.setMasterKey(masterKey); return true; } @@ -237,10 +230,9 @@ export class UserVerificationService implements UserVerificationServiceAbstracti } async hasMasterPasswordAndMasterKeyHash(userId?: string): Promise<boolean> { - userId ??= (await firstValueFrom(this.accountService.activeAccount$))?.id; return ( (await this.hasMasterPassword(userId)) && - (await firstValueFrom(this.masterPasswordService.masterKeyHash$(userId as UserId))) != null + (await this.cryptoService.getMasterKeyHash()) != null ); } diff --git a/libs/common/src/platform/abstractions/crypto.service.ts b/libs/common/src/platform/abstractions/crypto.service.ts index 0745033f3a..85b2bfe82e 100644 --- a/libs/common/src/platform/abstractions/crypto.service.ts +++ b/libs/common/src/platform/abstractions/crypto.service.ts @@ -112,6 +112,18 @@ export abstract class CryptoService { * @param userId The desired user */ abstract setMasterKeyEncryptedUserKey(UserKeyMasterKey: string, userId?: string): Promise<void>; + /** + * Sets the user's master key + * @param key The user's master key to set + * @param userId The desired user + */ + abstract setMasterKey(key: MasterKey, userId?: string): Promise<void>; + /** + * @param userId The desired user + * @returns The user's master key + */ + abstract getMasterKey(userId?: string): Promise<MasterKey>; + /** * @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 @@ -131,6 +143,11 @@ export abstract class CryptoService { kdf: KdfType, KdfConfig: KdfConfig, ): Promise<MasterKey>; + /** + * Clears the user's master key + * @param userId The desired user + */ + abstract clearMasterKey(userId?: string): Promise<void>; /** * Encrypts the existing (or provided) user key with the * provided master key @@ -168,6 +185,20 @@ export abstract class CryptoService { key: MasterKey, hashPurpose?: HashPurpose, ): Promise<string>; + /** + * Sets the user's master password hash + * @param keyHash The user's master password hash to set + */ + abstract setMasterKeyHash(keyHash: string): Promise<void>; + /** + * @returns The user's master password hash + */ + abstract getMasterKeyHash(): Promise<string>; + /** + * Clears the user's stored master password hash + * @param userId The desired user + */ + abstract clearMasterKeyHash(userId?: string): Promise<void>; /** * Compares the provided master password to the stored password hash and server password hash. * Updates the stored hash if outdated. diff --git a/libs/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index 227cb43879..4971481381 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -1,12 +1,14 @@ 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 { UserId } from "../../types/guid"; +import { 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"; @@ -15,6 +17,7 @@ import { KdfType } from "../enums"; 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. @@ -45,6 +48,22 @@ export abstract class StateService<T extends Account = Account> { getAddEditCipherInfo: (options?: StorageOptions) => Promise<AddEditCipherInfo>; setAddEditCipherInfo: (value: AddEditCipherInfo, options?: StorageOptions) => Promise<void>; + /** + * Gets the user's master key + */ + getMasterKey: (options?: StorageOptions) => Promise<MasterKey>; + /** + * Sets the user's master key + */ + setMasterKey: (value: MasterKey, options?: StorageOptions) => Promise<void>; + /** + * Gets the user key encrypted by the master key + */ + getMasterKeyEncryptedUserKey: (options?: StorageOptions) => Promise<string>; + /** + * Sets the user key encrypted by the master key + */ + setMasterKeyEncryptedUserKey: (value: string, options?: StorageOptions) => Promise<void>; /** * Gets the user's auto key */ @@ -89,6 +108,10 @@ export abstract class StateService<T extends Account = Account> { * @deprecated For migration purposes only, use getUserKeyMasterKey instead */ getEncryptedCryptoSymmetricKey: (options?: StorageOptions) => Promise<string>; + /** + * @deprecated For legacy purposes only, use getMasterKey instead + */ + getCryptoMasterKey: (options?: StorageOptions) => Promise<SymmetricCryptoKey>; /** * @deprecated For migration purposes only, use getUserKeyAuto instead */ @@ -166,11 +189,18 @@ export abstract class StateService<T extends Account = Account> { setEncryptedPinProtected: (value: string, options?: StorageOptions) => Promise<void>; getEverBeenUnlocked: (options?: StorageOptions) => Promise<boolean>; setEverBeenUnlocked: (value: boolean, options?: StorageOptions) => Promise<void>; + getForceSetPasswordReason: (options?: StorageOptions) => Promise<ForceSetPasswordReason>; + setForceSetPasswordReason: ( + value: ForceSetPasswordReason, + options?: StorageOptions, + ) => Promise<void>; getIsAuthenticated: (options?: StorageOptions) => Promise<boolean>; getKdfConfig: (options?: StorageOptions) => Promise<KdfConfig>; setKdfConfig: (kdfConfig: KdfConfig, options?: StorageOptions) => Promise<void>; getKdfType: (options?: StorageOptions) => Promise<KdfType>; setKdfType: (value: KdfType, options?: StorageOptions) => Promise<void>; + getKeyHash: (options?: StorageOptions) => Promise<string>; + setKeyHash: (value: string, options?: StorageOptions) => Promise<void>; getLastActive: (options?: StorageOptions) => Promise<number>; setLastActive: (value: number, options?: StorageOptions) => Promise<void>; getLastSync: (options?: StorageOptions) => Promise<string>; 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 6bdb08edd5..4a96da1b48 100644 --- a/libs/common/src/platform/models/domain/account-keys.spec.ts +++ b/libs/common/src/platform/models/domain/account-keys.spec.ts @@ -2,6 +2,7 @@ import { makeStaticByteArray } from "../../../../spec"; import { Utils } from "../../misc/utils"; import { AccountKeys, EncryptionPair } from "./account"; +import { SymmetricCryptoKey } from "./symmetric-crypto-key"; describe("AccountKeys", () => { describe("toJSON", () => { @@ -31,6 +32,12 @@ 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({ diff --git a/libs/common/src/platform/models/domain/account.ts b/libs/common/src/platform/models/domain/account.ts index 753b15c09b..4ed36fd389 100644 --- a/libs/common/src/platform/models/domain/account.ts +++ b/libs/common/src/platform/models/domain/account.ts @@ -1,6 +1,7 @@ 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,6 +10,7 @@ import { } from "../../../tools/generator/password"; import { UsernameGeneratorOptions } from "../../../tools/generator/username/username-generation-options"; 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"; @@ -88,8 +90,12 @@ export class AccountData { } export class AccountKeys { + masterKey?: MasterKey; + masterKeyEncryptedUserKey?: string; 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*/ @@ -114,6 +120,8 @@ export class AccountKeys { return null; } return Object.assign(new AccountKeys(), obj, { + masterKey: SymmetricCryptoKey.fromJSON(obj?.masterKey), + cryptoMasterKey: SymmetricCryptoKey.fromJSON(obj?.cryptoMasterKey), cryptoSymmetricKey: EncryptionPair.fromJSON( obj?.cryptoSymmetricKey, SymmetricCryptoKey.fromJSON, @@ -142,8 +150,10 @@ export class AccountProfile { email?: string; emailVerified?: boolean; everBeenUnlocked?: boolean; + forceSetPasswordReason?: ForceSetPasswordReason; lastSync?: string; userId?: string; + keyHash?: string; kdfIterations?: number; kdfMemory?: number; kdfParallelism?: number; diff --git a/libs/common/src/platform/services/crypto.service.spec.ts b/libs/common/src/platform/services/crypto.service.spec.ts index e9e4ec62c2..9160664aa5 100644 --- a/libs/common/src/platform/services/crypto.service.spec.ts +++ b/libs/common/src/platform/services/crypto.service.spec.ts @@ -5,7 +5,6 @@ import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-a 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"; @@ -41,15 +40,12 @@ 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, @@ -161,14 +157,14 @@ describe("cryptoService", () => { describe("getUserKeyWithLegacySupport", () => { let mockUserKey: UserKey; let mockMasterKey: MasterKey; - let getMasterKey: jest.SpyInstance; + let stateSvcGetMasterKey: 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; - getMasterKey = jest.spyOn(masterPasswordService, "masterKey$"); + stateSvcGetMasterKey = jest.spyOn(stateService, "getMasterKey"); }); it("returns the User Key if available", async () => { @@ -178,17 +174,17 @@ describe("cryptoService", () => { const userKey = await cryptoService.getUserKeyWithLegacySupport(mockUserId); expect(getKeySpy).toHaveBeenCalledWith(mockUserId); - expect(getMasterKey).not.toHaveBeenCalled(); + expect(stateSvcGetMasterKey).not.toHaveBeenCalled(); expect(userKey).toEqual(mockUserKey); }); it("returns the user's master key when User Key is not available", async () => { - masterPasswordService.masterKeySubject.next(mockMasterKey); + stateSvcGetMasterKey.mockResolvedValue(mockMasterKey); const userKey = await cryptoService.getUserKeyWithLegacySupport(mockUserId); - expect(getMasterKey).toHaveBeenCalledWith(mockUserId); + expect(stateSvcGetMasterKey).toHaveBeenCalledWith({ userId: mockUserId }); expect(userKey).toEqual(mockMasterKey); }); }); diff --git a/libs/common/src/platform/services/crypto.service.ts b/libs/common/src/platform/services/crypto.service.ts index 6d1143736c..dd3c497470 100644 --- a/libs/common/src/platform/services/crypto.service.ts +++ b/libs/common/src/platform/services/crypto.service.ts @@ -6,7 +6,6 @@ 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 { InternalMasterPasswordServiceAbstraction } from "../../auth/abstractions/master-password.service.abstraction"; import { AuthenticationStatus } from "../../auth/enums/authentication-status"; import { KdfConfig } from "../../auth/models/domain/kdf-config"; import { Utils } from "../../platform/misc/utils"; @@ -83,7 +82,6 @@ export class CryptoService implements CryptoServiceAbstraction { readonly everHadUserKey$: Observable<boolean>; constructor( - protected masterPasswordService: InternalMasterPasswordServiceAbstraction, protected keyGenerationService: KeyGenerationService, protected cryptoFunctionService: CryptoFunctionService, protected encryptService: EncryptService, @@ -183,16 +181,12 @@ export class CryptoService implements CryptoServiceAbstraction { } async isLegacyUser(masterKey?: MasterKey, userId?: UserId): Promise<boolean> { - userId ??= await firstValueFrom(this.stateProvider.activeUserId$); - masterKey ??= await firstValueFrom(this.masterPasswordService.masterKey$(userId)); - - return await this.validateUserKey(masterKey as unknown as UserKey); + return await this.validateUserKey( + (masterKey ?? (await this.getMasterKey(userId))) 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<UserKey> { - userId ??= await firstValueFrom(this.stateProvider.activeUserId$); - const userKey = await this.getUserKey(userId); if (userKey) { return userKey; @@ -200,8 +194,7 @@ 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. - const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); - return masterKey as unknown as UserKey; + return (await this.getMasterKey(userId)) as unknown as UserKey; } async getUserKeyFromStorage(keySuffix: KeySuffixOptions, userId?: UserId): Promise<UserKey> { @@ -240,10 +233,7 @@ export class CryptoService implements CryptoServiceAbstraction { } async makeUserKey(masterKey: MasterKey): Promise<[UserKey, EncString]> { - if (!masterKey) { - const userId = await firstValueFrom(this.stateProvider.activeUserId$); - masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); - } + masterKey ||= await this.getMasterKey(); if (masterKey == null) { throw new Error("No Master Key found."); } @@ -281,16 +271,28 @@ export class CryptoService implements CryptoServiceAbstraction { } async setMasterKeyEncryptedUserKey(userKeyMasterKey: string, userId?: UserId): Promise<void> { - await this.masterPasswordService.setMasterKeyEncryptedUserKey( - new EncString(userKeyMasterKey), - userId, - ); + await this.stateService.setMasterKeyEncryptedUserKey(userKeyMasterKey, { userId: userId }); + } + + async setMasterKey(key: MasterKey, userId?: UserId): Promise<void> { + await this.stateService.setMasterKey(key, { userId: userId }); + } + + async getMasterKey(userId?: UserId): Promise<MasterKey> { + 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; } - // TODO: Move to MasterPasswordService async getOrDeriveMasterKey(password: string, userId?: UserId) { - userId ??= await firstValueFrom(this.stateProvider.activeUserId$); - let masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); + let masterKey = await this.getMasterKey(userId); return (masterKey ||= await this.makeMasterKey( password, await this.stateService.getEmail({ userId: userId }), @@ -304,7 +306,6 @@ 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, @@ -320,6 +321,10 @@ export class CryptoService implements CryptoServiceAbstraction { )) as MasterKey; } + async clearMasterKey(userId?: UserId): Promise<void> { + await this.stateService.setMasterKey(null, { userId: userId }); + } + async encryptUserKeyWithMasterKey( masterKey: MasterKey, userKey?: UserKey, @@ -328,31 +333,32 @@ 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<UserKey> { - userId ??= await firstValueFrom(this.stateProvider.activeUserId$); - masterKey ??= await firstValueFrom(this.masterPasswordService.masterKey$(userId)); + masterKey ||= await this.getMasterKey(userId); if (masterKey == null) { throw new Error("No master key found."); } - if (userKey == null) { - let userKey = await this.masterPasswordService.getMasterKeyEncryptedUserKey(userId); + if (!userKey) { + let masterKeyEncryptedUserKey = await this.stateService.getMasterKeyEncryptedUserKey({ + userId: userId, + }); // Try one more way to get the user key if it still wasn't found. - if (userKey == null) { - const deprecatedKey = await this.stateService.getEncryptedCryptoSymmetricKey({ + if (masterKeyEncryptedUserKey == null) { + masterKeyEncryptedUserKey = await this.stateService.getEncryptedCryptoSymmetricKey({ userId: userId, }); - if (deprecatedKey == null) { - throw new Error("No encrypted user key found."); - } - userKey = new EncString(deprecatedKey); } + + if (masterKeyEncryptedUserKey == null) { + throw new Error("No encrypted user key found."); + } + userKey = new EncString(masterKeyEncryptedUserKey); } let decUserKey: Uint8Array; @@ -371,16 +377,12 @@ export class CryptoService implements CryptoServiceAbstraction { return new SymmetricCryptoKey(decUserKey) as UserKey; } - // TODO: move to MasterPasswordService async hashMasterKey( password: string, key: MasterKey, hashPurpose?: HashPurpose, ): Promise<string> { - if (!key) { - const userId = await firstValueFrom(this.stateProvider.activeUserId$); - key = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); - } + key ||= await this.getMasterKey(); if (password == null || key == null) { throw new Error("Invalid parameters."); @@ -391,12 +393,20 @@ export class CryptoService implements CryptoServiceAbstraction { return Utils.fromBufferToB64(hash); } - // TODO: move to MasterPasswordService + async setMasterKeyHash(keyHash: string): Promise<void> { + await this.stateService.setKeyHash(keyHash); + } + + async getMasterKeyHash(): Promise<string> { + return await this.stateService.getKeyHash(); + } + + async clearMasterKeyHash(userId?: UserId): Promise<void> { + return await this.stateService.setKeyHash(null, { userId: userId }); + } + async compareAndUpdateKeyHash(masterPassword: string, masterKey: MasterKey): Promise<boolean> { - const userId = await firstValueFrom(this.stateProvider.activeUserId$); - const storedPasswordHash = await firstValueFrom( - this.masterPasswordService.masterKeyHash$(userId), - ); + const storedPasswordHash = await this.getMasterKeyHash(); if (masterPassword != null && storedPasswordHash != null) { const localKeyHash = await this.hashMasterKey( masterPassword, @@ -414,7 +424,7 @@ export class CryptoService implements CryptoServiceAbstraction { HashPurpose.ServerAuthorization, ); if (serverKeyHash != null && storedPasswordHash === serverKeyHash) { - await this.masterPasswordService.setMasterKeyHash(localKeyHash, userId); + await this.setMasterKeyHash(localKeyHash); return true; } } @@ -471,7 +481,7 @@ export class CryptoService implements CryptoServiceAbstraction { } async clearOrgKeys(memoryOnly?: boolean, userId?: UserId): Promise<void> { - const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$); + const activeUserId = (await firstValueFrom(this.accountService.activeAccount$))?.id; const userIdIsActive = userId == null || userId === activeUserId; if (!memoryOnly) { @@ -517,7 +527,7 @@ export class CryptoService implements CryptoServiceAbstraction { } async clearProviderKeys(memoryOnly?: boolean, userId?: UserId): Promise<void> { - const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$); + const activeUserId = (await firstValueFrom(this.accountService.activeAccount$))?.id; const userIdIsActive = userId == null || userId === activeUserId; if (!memoryOnly) { @@ -588,7 +598,7 @@ export class CryptoService implements CryptoServiceAbstraction { } async clearKeyPair(memoryOnly?: boolean, userId?: UserId): Promise<void[]> { - const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$); + const activeUserId = (await firstValueFrom(this.accountService.activeAccount$))?.id; const userIdIsActive = userId == null || userId === activeUserId; if (!memoryOnly) { @@ -671,10 +681,8 @@ export class CryptoService implements CryptoServiceAbstraction { } async clearKeys(userId?: UserId): Promise<any> { - userId ??= await firstValueFrom(this.stateProvider.activeUserId$); - await this.masterPasswordService.setMasterKeyHash(null, userId); - await this.clearUserKey(true, userId); + await this.clearMasterKeyHash(userId); await this.clearOrgKeys(false, userId); await this.clearProviderKeys(false, userId); await this.clearKeyPair(false, userId); @@ -1029,8 +1037,7 @@ 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. - userId ??= await firstValueFrom(this.stateProvider.activeUserId$); - await this.masterPasswordService.setMasterKey(masterKey, userId); + await this.setMasterKey(masterKey, userId); return; } const encryptedUserKey = await this.stateService.getEncryptedCryptoSymmetricKey({ diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index b3e33cf362..a35659a7ac 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -5,12 +5,14 @@ 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 { UserId } from "../../types/guid"; +import { 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"; @@ -33,6 +35,7 @@ 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"; @@ -270,6 +273,65 @@ export class StateService< ); } + /** + * @deprecated Do not save the Master Key. Use the User Symmetric Key instead + */ + async getCryptoMasterKey(options?: StorageOptions): Promise<SymmetricCryptoKey> { + 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<MasterKey> { + 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<void> { + 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<string> { + 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<void> { + 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 */ @@ -761,6 +823,30 @@ export class StateService< ); } + async getForceSetPasswordReason(options?: StorageOptions): Promise<ForceSetPasswordReason> { + return ( + ( + await this.getAccount( + this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()), + ) + )?.profile?.forceSetPasswordReason ?? ForceSetPasswordReason.None + ); + } + + async setForceSetPasswordReason( + value: ForceSetPasswordReason, + options?: StorageOptions, + ): Promise<void> { + 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 getIsAuthenticated(options?: StorageOptions): Promise<boolean> { return ( (await this.tokenService.getAccessToken(options?.userId as UserId)) != null && @@ -811,6 +897,23 @@ export class StateService< ); } + async getKeyHash(options?: StorageOptions): Promise<string> { + return ( + await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) + )?.profile?.keyHash; + } + + async setKeyHash(value: string, options?: StorageOptions): Promise<void> { + 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<number> { options = this.reconcileOptions(options, await this.defaultOnDiskOptions()); diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index 39d9701fed..979321c1e3 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -37,8 +37,6 @@ export const BILLING_DISK = new StateDefinition("billing", "disk"); 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", { 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 f7f025991c..e48f2fe0a3 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,21 +1,17 @@ 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"; @@ -23,8 +19,6 @@ 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<CipherService>; let folderService: MockProxy<FolderService>; let collectionService: MockProxy<CollectionService>; @@ -45,11 +39,7 @@ describe("VaultTimeoutService", () => { let vaultTimeoutService: VaultTimeoutService; - const userId = Utils.newGuid() as UserId; - beforeEach(() => { - accountService = mockAccountServiceWith(userId); - masterPasswordService = new FakeMasterPasswordService(); cipherService = mock(); folderService = mock(); collectionService = mock(); @@ -76,8 +66,6 @@ describe("VaultTimeoutService", () => { availableVaultTimeoutActionsSubject = new BehaviorSubject<VaultTimeoutAction[]>([]); vaultTimeoutService = new VaultTimeoutService( - accountService, - masterPasswordService, cipherService, folderService, collectionService, @@ -135,15 +123,6 @@ describe("VaultTimeoutService", () => { stateService.activeAccount$ = new BehaviorSubject<string>(globalSetups?.userId); - if (globalSetups?.userId) { - accountService.activeAccountSubject.next({ - id: globalSetups.userId as UserId, - status: accounts[globalSetups.userId]?.authStatus, - email: null, - name: null, - }); - } - platformUtilsService.isViewOpen.mockResolvedValue(globalSetups?.isViewOpen ?? false); vaultTimeoutSettingsService.vaultTimeoutAction$.mockImplementation((userId) => { @@ -177,8 +156,8 @@ describe("VaultTimeoutService", () => { expect(vaultTimeoutSettingsService.availableVaultTimeoutActions$).toHaveBeenCalledWith(userId); expect(stateService.setEverBeenUnlocked).toHaveBeenCalledWith(true, { userId: userId }); expect(stateService.setUserKeyAutoUnlock).toHaveBeenCalledWith(null, { userId: userId }); - expect(masterPasswordService.mock.setMasterKey).toHaveBeenCalledWith(null, userId); expect(cryptoService.clearUserKey).toHaveBeenCalledWith(false, userId); + expect(cryptoService.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 22eb070360..c3270ac2b8 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout.service.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout.service.ts @@ -3,9 +3,7 @@ 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"; @@ -23,8 +21,6 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { private inited = false; constructor( - private accountService: AccountService, - private masterPasswordService: InternalMasterPasswordServiceAbstraction, private cipherService: CipherService, private folderService: FolderService, private collectionService: CollectionService, @@ -88,7 +84,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { await this.logOut(userId); } - const currentUserId = (await firstValueFrom(this.accountService.activeAccount$)).id; + const currentUserId = await this.stateService.getUserId(); if (userId == null || userId === currentUserId) { this.searchService.clearIndex(); @@ -96,13 +92,12 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { await this.collectionService.clearActiveUserCache(); } - await this.masterPasswordService.setMasterKey(null, (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); diff --git a/libs/common/src/state-migrations/migrate.ts b/libs/common/src/state-migrations/migrate.ts index 76f0d7fd46..faccddb0af 100644 --- a/libs/common/src/state-migrations/migrate.ts +++ b/libs/common/src/state-migrations/migrate.ts @@ -51,7 +51,6 @@ import { RememberedEmailMigrator } from "./migrations/51-move-remembered-email-t 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 { 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"; @@ -59,7 +58,7 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting import { MinVersionMigrator } from "./migrations/min-version"; export const MIN_VERSION = 3; -export const CURRENT_VERSION = 55; +export const CURRENT_VERSION = 54; export type MinVersion = typeof MIN_VERSION; @@ -116,8 +115,7 @@ export function createMigrationBuilder() { .with(RememberedEmailMigrator, 50, 51) .with(DeleteInstalledVersion, 51, 52) .with(DeviceTrustCryptoServiceStateProviderMigrator, 52, 53) - .with(SendMigrator, 53, 54) - .with(MoveMasterKeyStateToProviderMigrator, 54, CURRENT_VERSION); + .with(SendMigrator, 53, 54); } export async function currentVersion( 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 deleted file mode 100644 index bbf0352e95..0000000000 --- a/libs/common/src/state-migrations/migrations/55-move-master-key-state-to-provider.spec.ts +++ /dev/null @@ -1,210 +0,0 @@ -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<MigrationHelper>; - 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 deleted file mode 100644 index 99b22b5661..0000000000 --- a/libs/common/src/state-migrations/migrations/55-move-master-key-state-to-provider.ts +++ /dev/null @@ -1,111 +0,0 @@ -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<void> { - const accounts = await helper.getAccounts<ExpectedAccountType>(); - async function migrateAccount(userId: string, account: ExpectedAccountType): Promise<void> { - 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<void> { - const accounts = await helper.getAccounts<ExpectedAccountType>(); - async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise<void> { - 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/vault/services/sync/sync.service.ts b/libs/common/src/vault/services/sync/sync.service.ts index ff8e9f1f4f..d4601d9621 100644 --- a/libs/common/src/vault/services/sync/sync.service.ts +++ b/libs/common/src/vault/services/sync/sync.service.ts @@ -11,10 +11,8 @@ 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"; @@ -51,8 +49,6 @@ export class SyncService implements SyncServiceAbstraction { syncInProgress = false; constructor( - private masterPasswordService: InternalMasterPasswordServiceAbstraction, - private accountService: AccountService, private apiService: ApiService, private domainSettingsService: DomainSettingsService, private folderService: InternalFolderService, @@ -356,10 +352,8 @@ 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) { - const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; - await this.masterPasswordService.setForceSetPasswordReason( + await this.stateService.setForceSetPasswordReason( ForceSetPasswordReason.AdminForcePasswordReset, - userId, ); } @@ -393,10 +387,8 @@ 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. - const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; - await this.masterPasswordService.setForceSetPasswordReason( + await this.stateService.setForceSetPasswordReason( ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission, - userId, ); } } From 9f8f93ef9786386af5a49f2068649932b6a1f532 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Fri, 5 Apr 2024 09:23:12 -0400 Subject: [PATCH 116/351] Resolved subscription page problem for free org (#8622) --- .../subscription-status.component.html | 2 +- .../subscription-status.component.ts | 17 +++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/apps/web/src/app/billing/organizations/subscription-status.component.html b/apps/web/src/app/billing/organizations/subscription-status.component.html index 4bb2c91b85..99673a228e 100644 --- a/apps/web/src/app/billing/organizations/subscription-status.component.html +++ b/apps/web/src/app/billing/organizations/subscription-status.component.html @@ -14,7 +14,7 @@ <dl class="tw-grid tw-grid-flow-col tw-grid-rows-2"> <dt>{{ "billingPlan" | i18n }}</dt> <dd>{{ planName }}</dd> - <ng-container> + <ng-container *ngIf="data.status && data.date"> <dt>{{ data.status.label }}</dt> <dd> <span class="tw-capitalize"> diff --git a/apps/web/src/app/billing/organizations/subscription-status.component.ts b/apps/web/src/app/billing/organizations/subscription-status.component.ts index 54af940be5..9a0b634edc 100644 --- a/apps/web/src/app/billing/organizations/subscription-status.component.ts +++ b/apps/web/src/app/billing/organizations/subscription-status.component.ts @@ -5,11 +5,11 @@ import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/mode import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; type ComponentData = { - status: { + status?: { label: string; value: string; }; - date: { + date?: { label: string; value: string; }; @@ -44,13 +44,15 @@ export class SubscriptionStatusComponent { } get status(): string { - return this.subscription.status != "canceled" && this.subscription.cancelAtEndDate - ? "pending_cancellation" - : this.subscription.status; + return this.subscription + ? this.subscription.status != "canceled" && this.subscription.cancelAtEndDate + ? "pending_cancellation" + : this.subscription.status + : "free"; } get subscription() { - return this.organizationSubscriptionResponse.subscription; + return this.organizationSubscriptionResponse?.subscription; } get data(): ComponentData { @@ -61,6 +63,9 @@ export class SubscriptionStatusComponent { const cancellationDateLabel = this.i18nService.t("cancellationDate"); switch (this.status) { + case "free": { + return {}; + } case "trialing": { return { status: { From fb51aa570d2ba4096ba94c1fbc50093f4a90cf61 Mon Sep 17 00:00:00 2001 From: Colton Hurst <colton@coltonhurst.com> Date: Fri, 5 Apr 2024 10:52:21 -0400 Subject: [PATCH 117/351] SM-1159: Rename Service Accounts to Machine Accounts (#8444) * SM-1159: Rename service accounts to machine accounts. Visible text only. * SM-1159: Second round of adding service to machine account renames * SM-1159: Change title * SM-1159: Fix typo * SM-1159: Add more keys * SM-1159: Reordered keys * SM-1159: Keys update --- .../organizations/manage/events.component.ts | 2 +- .../sm-adjust-subscription.component.html | 16 +- .../shared/sm-subscribe.component.html | 10 +- .../onboarding/onboarding.stories.ts | 2 +- apps/web/src/locales/en/messages.json | 163 ++++++++++++++++++ .../layout/navigation.component.html | 2 +- .../overview/overview.component.html | 2 +- .../project-service-accounts.component.html | 10 +- .../projects/project/project.component.html | 2 +- ...rvice-account-delete-dialog.component.html | 2 +- ...service-account-delete-dialog.component.ts | 20 +-- .../service-account-dialog.component.html | 2 +- .../service-account-dialog.component.ts | 8 +- .../service-accounts-events.component.ts | 2 +- .../service-account-people.component.html | 2 +- .../service-account-people.component.ts | 8 +- .../service-account-projects.component.html | 4 +- .../service-account.component.html | 2 +- .../service-account.component.ts | 2 +- .../service-accounts-list.component.html | 14 +- .../service-accounts.component.html | 2 +- .../shared/new-menu.component.html | 2 +- .../app/secrets-manager/sm-routing.module.ts | 2 +- 23 files changed, 222 insertions(+), 59 deletions(-) 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/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 @@ </bit-hint> </bit-form-field> <bit-form-field class="tw-w-2/3"> - <bit-label>{{ "additionalServiceAccounts" | i18n }}</bit-label> + <bit-label>{{ "additionalMachineAccounts" | i18n }}</bit-label> <input bitInput id="additionalServiceAccountCount" @@ -44,8 +44,8 @@ /> <bit-hint> <div> - {{ "includedServiceAccounts" | i18n: options.baseServiceAccountCount }} - {{ "addAdditionalServiceAccounts" | i18n: (monthlyServiceAccountPrice | currency: "$") }} + {{ "includedMachineAccounts" | i18n: options.baseServiceAccountCount }} + {{ "addAdditionalMachineAccounts" | i18n: (monthlyServiceAccountPrice | currency: "$") }} </div> <div> <strong>{{ "total" | i18n }}:</strong> @@ -56,7 +56,7 @@ </bit-hint> </bit-form-field> <bit-form-control> - <bit-label>{{ "limitServiceAccounts" | i18n }}</bit-label> + <bit-label>{{ "limitMachineAccounts" | i18n }}</bit-label> <input type="checkbox" bitCheckbox @@ -64,11 +64,11 @@ formControlName="limitServiceAccounts" /> <bit-hint> - {{ "limitServiceAccountsDesc" | i18n }} + {{ "limitMachineAccountsDesc" | i18n }} </bit-hint> </bit-form-control> <bit-form-field class="tw-w-2/3" *ngIf="formGroup.value.limitServiceAccounts"> - <bit-label>{{ "serviceAccountLimit" | i18n }}</bit-label> + <bit-label>{{ "machineAccountLimit" | i18n }}</bit-label> <input bitInput id="additionalServiceAccountLimit" @@ -79,9 +79,9 @@ /> <bit-hint> <div> - {{ "includedServiceAccounts" | i18n: options.baseServiceAccountCount }} + {{ "includedMachineAccounts" | i18n: options.baseServiceAccountCount }} </div> - <strong>{{ "maxServiceAccountCost" | i18n }}:</strong> + <strong>{{ "maxMachineAccountCost" | i18n }}:</strong> {{ maxAdditionalServiceAccounts }} &times; {{ options.additionalServiceAccountPrice | currency: "$" }} = {{ maxServiceAccountTotalCost | currency: "$" }} / {{ options.interval | i18n }} 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 @@ <ng-template #unlimitedProjects> <li>{{ "unlimitedProjects" | i18n }}</li> </ng-template> - <li>{{ "serviceAccountsIncluded" | i18n: serviceAccountsIncluded }}</li> + <li>{{ "machineAccountsIncluded" | i18n: serviceAccountsIncluded }}</li> <li *ngIf="product != productTypes.Free"> {{ - "additionalServiceAccountCost" | i18n: (monthlyCostPerServiceAccount | currency: "$") + "additionalMachineAccountCost" | i18n: (monthlyCostPerServiceAccount | currency: "$") }} </li> </ul> @@ -54,12 +54,12 @@ </div> <div *ngIf="selectedPlan.SecretsManager.hasAdditionalServiceAccountOption" class="tw-w-1/2"> <bit-form-field> - <bit-label>{{ "additionalServiceAccounts" | i18n }}</bit-label> + <bit-label>{{ "additionalMachineAccounts" | i18n }}</bit-label> <input bitInput formControlName="additionalServiceAccounts" type="number" /> <bit-hint> - {{ "includedServiceAccounts" | i18n: serviceAccountsIncluded }} + {{ "includedMachineAccounts" | i18n: serviceAccountsIncluded }} {{ - "addAdditionalServiceAccounts" | i18n: (monthlyCostPerServiceAccount | currency: "$") + "addAdditionalMachineAccounts" | i18n: (monthlyCostPerServiceAccount | currency: "$") }} </bit-hint> </bit-form-field> 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: ` <app-onboarding title="Get started"> <app-onboarding-task - [title]="'createServiceAccount' | i18n" + [title]="'createMachineAccount' | i18n" icon="bwi-cli" [completed]="createServiceAccount" > diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 05697461a9..1604057f70 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -7730,5 +7730,168 @@ "cancellationDate": { "message": "Cancellation date", "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + }, + "machineAccount": { + "message": "Machine account", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Machine accounts", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "New machine account", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Create a new machine account to get started automating secret access.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Nothing to show yet", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Delete machine accounts", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Delete machine account", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "View machine account", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Deleting machine accounts is permanent and irreversible." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Delete $COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Machine account deleted" + }, + "deleteMachineAccountsToast": { + "message": "Machine accounts deleted" + }, + "searchMachineAccounts": { + "message": "Search machine accounts", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Edit machine account", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Machine account name", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Machine account created", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Machine account updated", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Grant machine accounts access to this project." + }, + "projectMachineAccountsSelectHint": { + "message": "Type or select machine accounts" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Add machine accounts to grant access" + }, + "machineAccountPeopleDescription": { + "message": "Grant groups or people access to this machine account." + }, + "machineAccountProjectsDescription": { + "message": "Assign projects to this machine account. " + }, + "createMachineAccount": { + "message": "Create a machine account" + }, + "maPeopleWarningMessage": { + "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Remove access to this machine account" + }, + "smAccessRemovalWarningMaMessage": { + "message": "This action will remove your access to the machine account." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ machine accounts included", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ per month for additional machine accounts", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Additional machine accounts" + }, + "includedMachineAccounts": { + "message": "Your plan comes with $COUNT$ machine accounts.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "You can add additional machine accounts for $COST$ per month.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Limit machine accounts (optional)" + }, + "limitMachineAccountsDesc": { + "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + }, + "machineAccountLimit": { + "message": "Machine account limit (optional)" + }, + "maxMachineAccountCost": { + "message": "Max potential machine account cost" + }, + "machineAccountAccessUpdated": { + "message": "Machine account access updated" } } 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 5ac76a31fc..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 @@ ></bit-nav-item> <bit-nav-item icon="bwi-wrench" - [text]="'serviceAccounts' | i18n" + [text]="'machineAccounts' | i18n" route="service-accounts" [relativeTo]="route.parent" ></bit-nav-item> 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 @@ <div *ngIf="!loading && view$ | async as view; else spinner"> <app-onboarding [title]="'getStarted' | i18n" *ngIf="showOnboarding" (dismiss)="hideOnboarding()"> <app-onboarding-task - [title]="'createServiceAccount' | i18n" + [title]="'createMachineAccount' | i18n" (click)="openServiceAccountDialog()" icon="bwi-cli" [completed]="view.tasks.createServiceAccount" 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 @@ <div class="tw-w-2/5"> <p class="tw-mt-8"> - {{ "projectServiceAccountsDescription" | i18n }} + {{ "projectMachineAccountsDescription" | i18n }} </p> <sm-access-selector [rows]="rows$ | async" granteeType="serviceAccounts" - [label]="'serviceAccounts' | i18n" - [hint]="'projectServiceAccountsSelectHint' | i18n" - [columnTitle]="'serviceAccounts' | i18n" - [emptyMessage]="'projectEmptyServiceAccountAccessPolicies' | i18n" + [label]="'machineAccounts' | i18n" + [hint]="'projectMachineAccountsSelectHint' | i18n" + [columnTitle]="'machineAccounts' | i18n" + [emptyMessage]="'projectEmptyMachineAccountAccessPolicies' | i18n" (onCreateAccessPolicies)="handleCreateAccessPolicies($event)" (onDeleteAccessPolicy)="handleDeleteAccessPolicy($event)" (onUpdateAccessPolicy)="handleUpdateAccessPolicy($event)" diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project.component.html index efcbe89f08..b399eef8d2 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project.component.html @@ -6,7 +6,7 @@ <bit-tab-link [route]="['secrets']">{{ "secrets" | i18n }}</bit-tab-link> <ng-container *ngIf="project.write"> <bit-tab-link [route]="['people']">{{ "people" | i18n }}</bit-tab-link> - <bit-tab-link [route]="['service-accounts']">{{ "serviceAccounts" | i18n }}</bit-tab-link> + <bit-tab-link [route]="['service-accounts']">{{ "machineAccounts" | i18n }}</bit-tab-link> </ng-container> </bit-tab-nav-bar> <sm-new-menu></sm-new-menu> 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 @@ </ng-container> <ng-container *ngIf="data.serviceAccounts.length > 1"> {{ data.serviceAccounts.length }} - {{ "serviceAccounts" | i18n }} + {{ "machineAccounts" | i18n }} </ng-container> </span> </ng-container> 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<unknown, BulkStatusDetails>(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 @@ </div> <div *ngIf="!loading"> <bit-form-field> - <bit-label>{{ "serviceAccountName" | i18n }}</bit-label> + <bit-label>{{ "machineAccountName" | i18n }}</bit-label> <input appAutofocus formControlName="name" bitInput /> </bit-form-field> </div> 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 @@ <form [formGroup]="formGroup" [bitSubmit]="submit"> <div class="tw-w-2/5"> <p class="tw-mt-8" *ngIf="!loading"> - {{ "serviceAccountPeopleDescription" | i18n }} + {{ "machineAccountPeopleDescription" | i18n }} </p> <sm-access-policy-selector [loading]="loading" diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/people/service-account-people.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/people/service-account-people.component.ts index ab6a627fd9..76b5e8928d 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/people/service-account-people.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/people/service-account-people.component.ts @@ -131,7 +131,7 @@ export class ServiceAccountPeopleComponent implements OnInit, OnDestroy { this.platformUtilsService.showToast( "success", null, - this.i18nService.t("serviceAccountAccessUpdated"), + this.i18nService.t("machineAccountAccessUpdated"), ); } catch (e) { this.validationService.showError(e); @@ -210,8 +210,8 @@ export class ServiceAccountPeopleComponent implements OnInit, OnDestroy { private async showWarning(): Promise<boolean> { 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<void> { 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..772579426a 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 @@ <div class="tw-mt-4 tw-w-2/5"> <p class="tw-mt-6"> - {{ "serviceAccountProjectsDescription" | i18n }} + {{ "machineAccountProjectsDescription" | i18n }} </p> <sm-access-selector [rows]="rows$ | async" @@ -8,7 +8,7 @@ [label]="'projects' | i18n" [hint]="'newSaSelectAccess' | i18n" [columnTitle]="'projects' | i18n" - [emptyMessage]="'serviceAccountEmptyProjectAccesspolicies' | i18n" + [emptyMessage]="'serviceAccountEmptyProjectAccessPolicies' | i18n" (onCreateAccessPolicies)="handleCreateAccessPolicies($event)" (onDeleteAccessPolicy)="handleDeleteAccessPolicy($event)" (onUpdateAccessPolicy)="handleUpdateAccessPolicy($event)" diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-account.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-account.component.html index 2cda63c02f..00b5201a65 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-account.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-account.component.html @@ -5,7 +5,7 @@ > <bit-breadcrumbs slot="breadcrumbs"> <bit-breadcrumb [route]="['..']" icon="bwi-angle-left">{{ - "serviceAccounts" | i18n + "machineAccounts" | i18n }}</bit-breadcrumb> </bit-breadcrumbs> <sm-new-menu></sm-new-menu> 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 @@ </div> <bit-no-items *ngIf="serviceAccounts?.length == 0"> - <ng-container slot="title">{{ "serviceAccountsNoItemsTitle" | i18n }}</ng-container> - <ng-container slot="description">{{ "serviceAccountsNoItemsMessage" | i18n }}</ng-container> + <ng-container slot="title">{{ "machineAccountsNoItemsTitle" | i18n }}</ng-container> + <ng-container slot="description">{{ "machineAccountsNoItemsMessage" | i18n }}</ng-container> <button slot="button" type="button" @@ -13,7 +13,7 @@ (click)="newServiceAccountEvent.emit()" > <i class="bwi bwi-plus" aria-hidden="true"></i> - {{ "newServiceAccount" | i18n }} + {{ "newMachineAccount" | i18n }} </button> </bit-no-items> @@ -80,16 +80,16 @@ <bit-menu #serviceAccountMenu> <a type="button" bitMenuItem [routerLink]="serviceAccount.id"> <i class="bwi bwi-fw bwi-eye" aria-hidden="true"></i> - {{ "viewServiceAccount" | i18n }} + {{ "viewMachineAccount" | i18n }} </a> <button type="button" bitMenuItem (click)="editServiceAccountEvent.emit(serviceAccount.id)"> <i class="bwi bwi-fw bwi-pencil" aria-hidden="true"></i> - {{ "editServiceAccount" | i18n }} + {{ "editMachineAccount" | i18n }} </button> <button type="button" bitMenuItem (click)="delete(serviceAccount)"> <i class="bwi bwi-fw bwi-trash tw-text-danger" aria-hidden="true"></i> <span class="tw-text-danger"> - {{ "deleteServiceAccount" | i18n }} + {{ "deleteMachineAccount" | i18n }} </span> </button> </bit-menu> @@ -101,7 +101,7 @@ <button type="button" bitMenuItem (click)="bulkDeleteServiceAccounts()"> <i class="bwi bwi-fw bwi-trash tw-text-danger" aria-hidden="true"></i> <span class="tw-text-danger"> - {{ "deleteServiceAccounts" | i18n }} + {{ "deleteMachineAccounts" | i18n }} </span> </button> </bit-menu> 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 @@ <app-header> <bit-search - [placeholder]="'searchServiceAccounts' | i18n" + [placeholder]="'searchMachineAccounts' | i18n" [(ngModel)]="search" class="tw-w-80" ></bit-search> 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 @@ </button> <button type="button" bitMenuItem (click)="openServiceAccountDialog()"> <i class="bwi bwi-fw bwi-wrench" aria-hidden="true"></i> - {{ "serviceAccount" | i18n }} + {{ "machineAccount" | i18n }} </button> </bit-menu> 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..f9ddcdad78 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 @@ -58,7 +58,7 @@ const routes: Routes = [ path: "service-accounts", loadChildren: () => ServiceAccountsModule, data: { - titleId: "serviceAccounts", + titleId: "machineAccounts", }, }, { From 09169cac719431183261fd4eb68d8ab37248537e Mon Sep 17 00:00:00 2001 From: Victoria League <vleague@bitwarden.com> Date: Fri, 5 Apr 2024 10:58:32 -0400 Subject: [PATCH 118/351] [CL-254] Rename 500 colors to 600 to prep for UI redesign (#8623) * [CL-254] Rename 500 colors to 600 to prep for UI redesign --------- Co-authored-by: Will Martin <contact@willmartian.com> --- .../src/auth/delete-account.component.html | 2 +- .../src/app/admin-console/icons/devices.ts | 18 +++--- .../account/change-avatar.component.html | 4 +- .../account/danger-zone.component.html | 2 +- .../auth/settings/verify-email.component.html | 4 +- .../create-passkey-failed.icon.ts | 18 +++--- .../create-passkey.icon.ts | 16 +++--- .../vertical-step-content.component.html | 4 +- .../app/billing/shared/payment.component.ts | 2 +- .../components/selectable-avatar.component.ts | 4 +- .../product-switcher-content.component.html | 4 +- .../src/app/settings/low-kdf.component.html | 4 +- .../onboarding/onboarding.component.html | 2 +- .../app/tools/send/icons/expired-send.icon.ts | 10 ++-- .../src/app/tools/send/icons/no-send.icon.ts | 14 ++--- .../individual-vault/add-edit.component.html | 2 +- .../collection-access-restricted.component.ts | 4 +- .../auth/icons/create-passkey-failed.icon.ts | 18 +++--- .../src/auth/icons/create-passkey.icon.ts | 16 +++--- ...erification-biometrics-fingerprint.icon.ts | 12 ++-- libs/auth/src/icons/bitwarden-logo.ts | 4 +- libs/auth/src/icons/icon-lock.ts | 2 +- .../components/src/avatar/avatar.component.ts | 2 +- libs/components/src/avatar/avatar.mdx | 4 +- libs/components/src/badge/badge.directive.ts | 10 ++-- .../components/src/banner/banner.component.ts | 8 +-- .../src/button/button.component.spec.ts | 8 +-- .../components/src/button/button.component.ts | 16 +++--- .../src/callout/callout.component.ts | 8 +-- .../src/checkbox/checkbox.component.ts | 10 ++-- .../color-password.component.ts | 2 +- .../simple-configurable-dialog.component.ts | 2 +- .../dialog/simple-dialog/simple-dialog.mdx | 10 ++-- .../src/form-field/prefix.directive.ts | 2 +- libs/components/src/form/forms.mdx | 2 +- .../src/icon-button/icon-button.component.ts | 16 +++--- .../src/icon-button/icon-button.stories.ts | 4 +- libs/components/src/icon/icons/no-access.ts | 12 ++-- libs/components/src/icon/icons/search.ts | 18 +++--- libs/components/src/input/input.directive.ts | 2 +- libs/components/src/link/link.directive.ts | 6 +- libs/components/src/link/link.mdx | 2 +- libs/components/src/link/link.stories.ts | 6 +- .../src/menu/menu-divider.component.html | 2 +- libs/components/src/menu/menu.component.html | 2 +- .../src/multi-select/scss/bw.theme.scss | 2 +- .../components/src/popover/popover.stories.ts | 28 +++++----- .../src/progress/progress.component.ts | 8 +-- libs/components/src/progress/progress.mdx | 8 +-- .../src/radio-button/radio-input.component.ts | 6 +- libs/components/src/stories/colors.mdx | 12 ++-- libs/components/src/stories/migration.mdx | 22 ++++---- .../tabs/shared/tab-list-item.directive.ts | 4 +- .../src/toggle-group/toggle.component.ts | 10 ++-- libs/components/src/tw-theme.css | 56 +++++++++++-------- libs/components/tailwind.config.base.js | 22 ++++---- 56 files changed, 253 insertions(+), 245 deletions(-) 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 }} </bit-callout> <!-- Temporary border styles. TODO: Remove when app-user-verification is migrated to the CL. --> - <div class="tw-mb-2 tw-rounded tw-border tw-border-solid tw-border-info-500/25"> + <div class="tw-mb-2 tw-rounded tw-border tw-border-solid tw-border-info-600/25"> <app-user-verification ngDefaultControl formControlName="verification" 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` <svg width="201" height="201" viewBox="0 0 201 201" fill="none" xmlns="http://www.w3.org/2000/svg"> <g opacity=".49"> - <path class="tw-fill-secondary-500" fill-rule="evenodd" clip-rule="evenodd" d="M34.3628 82.0889H10.3628C7.04908 82.0889 4.36279 84.7752 4.36279 88.0889V148.089C4.36279 151.403 7.04909 154.089 10.3628 154.089H34.3628C37.6765 154.089 40.3628 151.403 40.3628 148.089V88.0889C40.3628 84.7752 37.6765 82.0889 34.3628 82.0889ZM10.3628 78.0889C4.83995 78.0889 0.362793 82.566 0.362793 88.0889V148.089C0.362793 153.612 4.83995 158.089 10.3628 158.089H34.3628C39.8856 158.089 44.3628 153.612 44.3628 148.089V88.0889C44.3628 82.566 39.8856 78.0889 34.3628 78.0889H10.3628Z" /> - <path class="tw-fill-secondary-500" fill-rule="evenodd" clip-rule="evenodd" d="M20.7329 86.8979C20.7329 86.3457 21.1806 85.8979 21.7329 85.8979H22.975C23.5273 85.8979 23.975 86.3457 23.975 86.8979C23.975 87.4502 23.5273 87.8979 22.975 87.8979H21.7329C21.1806 87.8979 20.7329 87.4502 20.7329 86.8979Z" /> - <path class="tw-fill-secondary-500" fill-rule="evenodd" clip-rule="evenodd" d="M68.3628 159.089C68.3628 158.537 68.8105 158.089 69.3628 158.089H127.363C127.915 158.089 128.363 158.537 128.363 159.089C128.363 159.641 127.915 160.089 127.363 160.089H69.3628C68.8105 160.089 68.3628 159.641 68.3628 159.089Z" /> - <path class="tw-fill-secondary-500" fill-rule="evenodd" clip-rule="evenodd" d="M88.103 159.089V141.325H90.103V159.089H88.103Z" /> - <path class="tw-fill-secondary-500" fill-rule="evenodd" clip-rule="evenodd" d="M108.073 159.089V141.325H110.073V159.089H108.073Z" /> - <path class="tw-fill-secondary-500" fill-rule="evenodd" clip-rule="evenodd" d="M27.3628 64.0889C27.3628 56.3569 33.6308 50.0889 41.3628 50.0889H157.363C165.095 50.0889 171.363 56.3569 171.363 64.0889V70.0889H167.363V64.0889C167.363 58.566 162.886 54.0889 157.363 54.0889H41.3628C35.8399 54.0889 31.3628 58.566 31.3628 64.0889V80.0889H27.3628V64.0889ZM42.3628 138.089H127.363V142.089H42.3628V138.089Z" /> - <path class="tw-fill-secondary-500" fill-rule="evenodd" clip-rule="evenodd" d="M35.3628 65.0889C35.3628 61.2229 38.4968 58.0889 42.3628 58.0889H156.363C160.229 58.0889 163.363 61.2229 163.363 65.0889V70.0889H161.363V65.0889C161.363 62.3274 159.124 60.0889 156.363 60.0889H42.3628C39.6014 60.0889 37.3628 62.3274 37.3628 65.0889V80.0889H35.3628V65.0889ZM42.3628 132.089H127.363V134.089H42.3628V132.089Z" /> - <path class="tw-fill-secondary-500" fill-rule="evenodd" clip-rule="evenodd" d="M125.363 78.0889C125.363 72.566 129.84 68.0889 135.363 68.0889H188.363C193.886 68.0889 198.363 72.566 198.363 78.0889V158.089C198.363 163.612 193.886 168.089 188.363 168.089H135.363C129.84 168.089 125.363 163.612 125.363 158.089V78.0889ZM135.363 72.0889C132.049 72.0889 129.363 74.7752 129.363 78.0889V158.089C129.363 161.403 132.049 164.089 135.363 164.089H188.363C191.676 164.089 194.363 161.403 194.363 158.089V78.0889C194.363 74.7752 191.677 72.0889 188.363 72.0889H135.363Z" /> - <path class="tw-fill-secondary-500" d="M164.363 159.089C164.363 160.193 163.467 161.089 162.363 161.089C161.258 161.089 160.363 160.193 160.363 159.089C160.363 157.984 161.258 157.089 162.363 157.089C163.467 157.089 164.363 157.984 164.363 159.089Z" /> + <path class="tw-fill-secondary-600" fill-rule="evenodd" clip-rule="evenodd" d="M34.3628 82.0889H10.3628C7.04908 82.0889 4.36279 84.7752 4.36279 88.0889V148.089C4.36279 151.403 7.04909 154.089 10.3628 154.089H34.3628C37.6765 154.089 40.3628 151.403 40.3628 148.089V88.0889C40.3628 84.7752 37.6765 82.0889 34.3628 82.0889ZM10.3628 78.0889C4.83995 78.0889 0.362793 82.566 0.362793 88.0889V148.089C0.362793 153.612 4.83995 158.089 10.3628 158.089H34.3628C39.8856 158.089 44.3628 153.612 44.3628 148.089V88.0889C44.3628 82.566 39.8856 78.0889 34.3628 78.0889H10.3628Z" /> + <path class="tw-fill-secondary-600" fill-rule="evenodd" clip-rule="evenodd" d="M20.7329 86.8979C20.7329 86.3457 21.1806 85.8979 21.7329 85.8979H22.975C23.5273 85.8979 23.975 86.3457 23.975 86.8979C23.975 87.4502 23.5273 87.8979 22.975 87.8979H21.7329C21.1806 87.8979 20.7329 87.4502 20.7329 86.8979Z" /> + <path class="tw-fill-secondary-600" fill-rule="evenodd" clip-rule="evenodd" d="M68.3628 159.089C68.3628 158.537 68.8105 158.089 69.3628 158.089H127.363C127.915 158.089 128.363 158.537 128.363 159.089C128.363 159.641 127.915 160.089 127.363 160.089H69.3628C68.8105 160.089 68.3628 159.641 68.3628 159.089Z" /> + <path class="tw-fill-secondary-600" fill-rule="evenodd" clip-rule="evenodd" d="M88.103 159.089V141.325H90.103V159.089H88.103Z" /> + <path class="tw-fill-secondary-600" fill-rule="evenodd" clip-rule="evenodd" d="M108.073 159.089V141.325H110.073V159.089H108.073Z" /> + <path class="tw-fill-secondary-600" fill-rule="evenodd" clip-rule="evenodd" d="M27.3628 64.0889C27.3628 56.3569 33.6308 50.0889 41.3628 50.0889H157.363C165.095 50.0889 171.363 56.3569 171.363 64.0889V70.0889H167.363V64.0889C167.363 58.566 162.886 54.0889 157.363 54.0889H41.3628C35.8399 54.0889 31.3628 58.566 31.3628 64.0889V80.0889H27.3628V64.0889ZM42.3628 138.089H127.363V142.089H42.3628V138.089Z" /> + <path class="tw-fill-secondary-600" fill-rule="evenodd" clip-rule="evenodd" d="M35.3628 65.0889C35.3628 61.2229 38.4968 58.0889 42.3628 58.0889H156.363C160.229 58.0889 163.363 61.2229 163.363 65.0889V70.0889H161.363V65.0889C161.363 62.3274 159.124 60.0889 156.363 60.0889H42.3628C39.6014 60.0889 37.3628 62.3274 37.3628 65.0889V80.0889H35.3628V65.0889ZM42.3628 132.089H127.363V134.089H42.3628V132.089Z" /> + <path class="tw-fill-secondary-600" fill-rule="evenodd" clip-rule="evenodd" d="M125.363 78.0889C125.363 72.566 129.84 68.0889 135.363 68.0889H188.363C193.886 68.0889 198.363 72.566 198.363 78.0889V158.089C198.363 163.612 193.886 168.089 188.363 168.089H135.363C129.84 168.089 125.363 163.612 125.363 158.089V78.0889ZM135.363 72.0889C132.049 72.0889 129.363 74.7752 129.363 78.0889V158.089C129.363 161.403 132.049 164.089 135.363 164.089H188.363C191.676 164.089 194.363 161.403 194.363 158.089V78.0889C194.363 74.7752 191.677 72.0889 188.363 72.0889H135.363Z" /> + <path class="tw-fill-secondary-600" d="M164.363 159.089C164.363 160.193 163.467 161.089 162.363 161.089C161.258 161.089 160.363 160.193 160.363 159.089C160.363 157.984 161.258 157.089 162.363 157.089C163.467 157.089 164.363 157.984 164.363 159.089Z" /> </g> </svg> `; 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" > <i diff --git a/apps/web/src/app/auth/settings/account/danger-zone.component.html b/apps/web/src/app/auth/settings/account/danger-zone.component.html index b6f5300cb2..14c3b7e0b7 100644 --- a/apps/web/src/app/auth/settings/account/danger-zone.component.html +++ b/apps/web/src/app/auth/settings/account/danger-zone.component.html @@ -1,6 +1,6 @@ <h1 bitTypography="h1" class="tw-mt-16 tw-pb-2.5 !tw-text-danger">{{ "dangerZone" | i18n }}</h1> -<div class="tw-rounded tw-border tw-border-solid tw-border-danger-500 tw-p-5"> +<div class="tw-rounded tw-border tw-border-solid tw-border-danger-600 tw-p-5"> <p>{{ "dangerZoneDesc" | i18n }}</p> <div class="tw-flex tw-flex-row tw-gap-2"> 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 @@ -<div class="tw-rounded tw-border tw-border-solid tw-border-warning-500 tw-bg-background"> - <div class="tw-bg-warning-500 tw-px-5 tw-py-2.5 tw-font-bold tw-uppercase tw-text-contrast"> +<div class="tw-rounded tw-border tw-border-solid tw-border-warning-600 tw-bg-background"> + <div class="tw-bg-warning-600 tw-px-5 tw-py-2.5 tw-font-bold tw-uppercase tw-text-contrast"> <i class="bwi bwi-envelope bwi-fw" aria-hidden="true"></i> {{ "verifyEmail" | i18n }} </div> <div class="tw-p-5"> 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` <svg xmlns="http://www.w3.org/2000/svg" width="163" height="115" fill="none"> - <path class="tw-fill-secondary-500" fill-rule="evenodd" d="M31 19.46H9v22h22v-22Zm-24-2v26h26v-26H7Z" + <path class="tw-fill-secondary-600" fill-rule="evenodd" d="M31 19.46H9v22h22v-22Zm-24-2v26h26v-26H7Z" clip-rule="evenodd" /> - <path class="tw-fill-secondary-500" fill-rule="evenodd" + <path class="tw-fill-secondary-600" fill-rule="evenodd" d="M0 43.46a4 4 0 0 1 4-4h32a4 4 0 0 1 4 4v7h-4v-7H4v16.747l1.705 2.149a4 4 0 0 1 .866 2.486v22.205a4 4 0 0 1-1 2.645L4 91.475v17.985h32V91.475l-1.572-1.783a4 4 0 0 1-1-2.645V64.842a4 4 0 0 1 .867-2.486L36 60.207V56.46h4v3.747a4 4 0 0 1-.867 2.487l-1.704 2.148v22.205L39 88.83a4 4 0 0 1 1 2.645v17.985a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V91.475a4 4 0 0 1 1-2.645l1.571-1.783V64.842L.867 62.694A4 4 0 0 1 0 60.207V43.46Z" clip-rule="evenodd" /> - <path class="tw-fill-secondary-500" fill-rule="evenodd" + <path class="tw-fill-secondary-600" fill-rule="evenodd" d="M19.74 63.96a.5.5 0 0 1 .355.147l2.852 2.866a.5.5 0 0 1 .146.353V77.56c2.585 1.188 4.407 3.814 4.407 6.865 0 4.183-3.357 7.534-7.5 7.534-4.144 0-7.5-3.376-7.5-7.534a7.546 7.546 0 0 1 4.478-6.894v-1.443a.5.5 0 0 1 .146-.353l1.275-1.281-1.322-1.33a.5.5 0 0 1 0-.705l.332-.334-.262-.263a.5.5 0 0 1-.005-.7l1.332-1.377-1.445-1.452a.5.5 0 0 1-.145-.352v-1.114a.5.5 0 0 1 .145-.352l2.357-2.369a.5.5 0 0 1 .355-.147Zm-1.856 3.075v.7l1.645 1.654a.5.5 0 0 1 .005.7l-1.332 1.377.267.268a.5.5 0 0 1 0 .705l-.333.334 1.323 1.329a.5.5 0 0 1 0 .705l-1.48 1.488v1.57a.5.5 0 0 1-.32.466 6.545 6.545 0 0 0-4.159 6.095c0 3.61 2.913 6.534 6.5 6.534 3.588 0 6.5-2.901 6.5-6.534 0-2.749-1.707-5.105-4.095-6.074a.5.5 0 0 1-.312-.463V67.532L19.74 65.17l-1.857 1.866ZM20 85.623a1.27 1.27 0 0 0-1.268 1.276c0 .702.56 1.276 1.268 1.276.712 0 1.268-.555 1.268-1.276A1.27 1.27 0 0 0 20 85.623Zm-2.268 1.276A2.27 2.27 0 0 1 20 84.623a2.27 2.27 0 0 1 2.268 2.276c0 1.269-1 2.276-2.268 2.276a2.27 2.27 0 0 1-2.268-2.276ZM57.623 114a1 1 0 0 1 1-1h63.048a1 1 0 0 1 0 2H58.623a1 1 0 0 1-1-1Z" clip-rule="evenodd" /> - <path class="tw-fill-secondary-500" fill-rule="evenodd" + <path class="tw-fill-secondary-600" fill-rule="evenodd" d="M78.022 114V95.654h2V114h-2ZM98.418 114V95.654h2V114h-2Z" clip-rule="evenodd" /> - <path class="tw-fill-secondary-500" fill-rule="evenodd" + <path class="tw-fill-secondary-600" fill-rule="evenodd" d="M16 14.46c0-7.732 6.268-14 14-14h119c7.732 0 14 6.268 14 14v68c0 7.732-6.268 14-14 14H39.5v-4H149c5.523 0 10-4.477 10-10v-68c0-5.523-4.477-10-10-10H30c-5.523 0-10 4.477-10 10v5h-4v-5Z" clip-rule="evenodd" /> - <path class="tw-fill-secondary-500" fill-rule="evenodd" + <path class="tw-fill-secondary-600" fill-rule="evenodd" d="M25 15.46a6 6 0 0 1 6-6h117a6 6 0 0 1 6 6v66a6 6 0 0 1-6 6H36.5v-2H148a4 4 0 0 0 4-4v-66a4 4 0 0 0-4-4H31a4 4 0 0 0-4 4v3h-2v-3Z" clip-rule="evenodd" /> - <path class="tw-fill-secondary-500" + <path class="tw-fill-secondary-600" d="M104.269 32.86a1.42 1.42 0 0 0-1.007-.4h-25.83c-.39 0-.722.132-1.007.4a1.26 1.26 0 0 0-.425.947v16.199c0 1.207.25 2.407.75 3.597a13.22 13.22 0 0 0 1.861 3.165c.74.919 1.62 1.817 2.646 2.69a30.93 30.93 0 0 0 2.834 2.172c.868.577 1.77 1.121 2.712 1.636.942.516 1.612.862 2.007 1.043.394.181.714.326.95.42.18.083.373.128.583.128.21 0 .403-.041.582-.128.241-.099.557-.239.956-.42.394-.181 1.064-.532 2.006-1.043a36.595 36.595 0 0 0 2.712-1.636c.867-.576 1.813-1.302 2.838-2.171a19.943 19.943 0 0 0 2.646-2.69 13.24 13.24 0 0 0 1.862-3.166 9.19 9.19 0 0 0 .749-3.597V33.812c.005-.367-.14-.684-.425-.952Zm-3.329 17.298c0 5.864-10.593 10.916-10.593 10.916V35.93h10.593v14.228Z" /> - <path class="tw-fill-secondary-500" fill-rule="evenodd" d="M18 24.46h-5v-2h5v2ZM27 24.46h-5v-2h5v2Z" + <path class="tw-fill-secondary-600" fill-rule="evenodd" d="M18 24.46h-5v-2h5v2ZM27 24.46h-5v-2h5v2Z" clip-rule="evenodd" /> - <path class="tw-fill-danger-500" + <path class="tw-fill-danger-600" d="M51.066 66.894a2.303 2.303 0 0 1-2.455-.5l-10.108-9.797L28.375 66.4l-.002.002a2.294 2.294 0 0 1-3.185.005 2.24 2.24 0 0 1-.506-2.496c.117-.27.286-.518.503-.728l10.062-9.737-9.945-9.623a2.258 2.258 0 0 1-.698-1.6c-.004-.314.06-.619.176-.894a2.254 2.254 0 0 1 1.257-1.222 2.305 2.305 0 0 1 1.723.014c.267.11.518.274.732.486l10.01 9.682 9.995-9.688.009-.008a2.292 2.292 0 0 1 3.159.026c.425.411.68.98.684 1.59a2.242 2.242 0 0 1-.655 1.6l-.01.01-9.926 9.627 10.008 9.7.029.027a2.237 2.237 0 0 1 .53 2.496l-.002.004a2.258 2.258 0 0 1-1.257 1.222Z" /> </svg> `; 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` <svg xmlns="http://www.w3.org/2000/svg" width="163" height="116" fill="none"> - <path class="tw-fill-secondary-500" fill-rule="evenodd" d="M31 19.58H9v22h22v-22Zm-24-2v26h26v-26H7Z" + <path class="tw-fill-secondary-600" fill-rule="evenodd" d="M31 19.58H9v22h22v-22Zm-24-2v26h26v-26H7Z" clip-rule="evenodd" /> - <path class="tw-fill-secondary-500" fill-rule="evenodd" + <path class="tw-fill-secondary-600" fill-rule="evenodd" d="M0 43.58a4 4 0 0 1 4-4h32a4 4 0 0 1 4 4v7h-4v-7H4v16.747l1.705 2.149a4 4 0 0 1 .866 2.486v22.204a4 4 0 0 1-1 2.646L4 91.595v17.985h32V91.595l-1.572-1.783a4 4 0 0 1-1-2.646V64.962a4 4 0 0 1 .867-2.486L36 60.327V56.58h4v3.747a4 4 0 0 1-.867 2.486l-1.704 2.149v22.204L39 88.95a4 4 0 0 1 1 2.646v17.985a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V91.595a4 4 0 0 1 1-2.646l1.571-1.783V64.962L.867 62.813A4 4 0 0 1 0 60.327V43.58Z" clip-rule="evenodd" /> - <path class="tw-fill-secondary-500" fill-rule="evenodd" + <path class="tw-fill-secondary-600" fill-rule="evenodd" d="M19.74 64.08a.5.5 0 0 1 .355.147l2.852 2.866a.5.5 0 0 1 .146.352V77.68c2.585 1.189 4.407 3.814 4.407 6.865 0 4.183-3.357 7.535-7.5 7.535-4.144 0-7.5-3.377-7.5-7.535a7.546 7.546 0 0 1 4.478-6.894V76.21a.5.5 0 0 1 .146-.353l1.275-1.282-1.322-1.329a.5.5 0 0 1 0-.705l.332-.334-.262-.263a.5.5 0 0 1-.005-.7l1.332-1.377-1.445-1.452a.5.5 0 0 1-.145-.353v-1.113a.5.5 0 0 1 .145-.353l2.357-2.368a.5.5 0 0 1 .355-.147Zm-1.856 3.074v.7l1.645 1.654a.5.5 0 0 1 .005.7l-1.332 1.377.267.268a.5.5 0 0 1 0 .706l-.333.334 1.323 1.329a.5.5 0 0 1 0 .705l-1.48 1.488v1.57a.5.5 0 0 1-.32.466 6.545 6.545 0 0 0-4.159 6.094c0 3.61 2.913 6.535 6.5 6.535 3.588 0 6.5-2.902 6.5-6.535 0-2.748-1.707-5.104-4.095-6.073a.5.5 0 0 1-.312-.463V67.651l-2.352-2.364-1.857 1.866ZM20 85.742a1.27 1.27 0 0 0-1.268 1.277c0 .701.56 1.276 1.268 1.276.712 0 1.268-.555 1.268-1.276A1.27 1.27 0 0 0 20 85.742Zm-2.268 1.277A2.27 2.27 0 0 1 20 84.742a2.27 2.27 0 0 1 2.268 2.277c0 1.268-1 2.276-2.268 2.276a2.27 2.27 0 0 1-2.268-2.276ZM41.796 42.844a1 1 0 0 1 1.413.058l5.526 6A1 1 0 0 1 48 50.58H27a1 1 0 1 1 0-2h18.72l-3.982-4.323a1 1 0 0 1 .058-1.413ZM33.315 62.315a1 1 0 0 1-1.413-.058l-5.526-6a1 1 0 0 1 .735-1.677h21a1 1 0 1 1 0 2h-18.72l3.982 4.322a1 1 0 0 1-.058 1.413ZM57.623 114.12a1 1 0 0 1 1-1h63.048a1 1 0 1 1 0 2H58.623a1 1 0 0 1-1-1Z" clip-rule="evenodd" /> - <path class="tw-fill-secondary-500" fill-rule="evenodd" + <path class="tw-fill-secondary-600" fill-rule="evenodd" d="M78.022 114.12V95.774h2v18.346h-2ZM98.418 114.12V95.774h2v18.346h-2Z" clip-rule="evenodd" /> - <path class="tw-fill-secondary-500" fill-rule="evenodd" + <path class="tw-fill-secondary-600" fill-rule="evenodd" d="M16 14.58c0-7.732 6.268-14 14-14h119c7.732 0 14 6.268 14 14v68c0 7.732-6.268 14-14 14H39.5v-4H149c5.523 0 10-4.478 10-10v-68c0-5.523-4.477-10-10-10H30c-5.523 0-10 4.477-10 10v5h-4v-5Z" clip-rule="evenodd" /> - <path class="tw-fill-secondary-500" fill-rule="evenodd" + <path class="tw-fill-secondary-600" fill-rule="evenodd" d="M25 15.58a6 6 0 0 1 6-6h117a6 6 0 0 1 6 6v66a6 6 0 0 1-6 6H36.5v-2H148a4 4 0 0 0 4-4v-66a4 4 0 0 0-4-4H31a4 4 0 0 0-4 4v3h-2v-3Z" clip-rule="evenodd" /> - <path class="tw-fill-secondary-500" + <path class="tw-fill-secondary-600" d="M104.269 32.98a1.42 1.42 0 0 0-1.007-.4h-25.83c-.39 0-.722.132-1.007.4a1.26 1.26 0 0 0-.425.947v16.199c0 1.207.25 2.406.75 3.597a13.222 13.222 0 0 0 1.861 3.165c.74.919 1.62 1.817 2.646 2.69a30.93 30.93 0 0 0 2.834 2.172c.868.577 1.77 1.121 2.712 1.636.942.515 1.612.861 2.007 1.043.394.18.714.325.95.42.18.082.373.128.583.128.21 0 .403-.042.582-.128.241-.099.557-.24.956-.42.394-.182 1.064-.532 2.006-1.043a36.56 36.56 0 0 0 2.712-1.636c.867-.577 1.813-1.302 2.838-2.172a19.943 19.943 0 0 0 2.646-2.69 13.24 13.24 0 0 0 1.862-3.165c.5-1.187.749-2.386.749-3.597V33.93c.005-.367-.14-.684-.425-.952Zm-3.329 17.298c0 5.864-10.593 10.916-10.593 10.916V36.049h10.593v14.23Z" /> - <path class="tw-fill-secondary-500" fill-rule="evenodd" d="M18 24.58h-5v-2h5v2ZM27 24.58h-5v-2h5v2Z" + <path class="tw-fill-secondary-600" fill-rule="evenodd" d="M18 24.58h-5v-2h5v2ZM27 24.58h-5v-2h5v2Z" clip-rule="evenodd" /> </svg> `; 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 }} </span> <span - class="tw-mr-3.5 tw-w-9 tw-rounded-full tw-bg-primary-500 tw-font-bold tw-leading-9 tw-text-contrast" + class="tw-mr-3.5 tw-w-9 tw-rounded-full tw-bg-primary-600 tw-font-bold tw-leading-9 tw-text-contrast" *ngIf="step.completed" > <i class="bwi bwi-fw bwi-check tw-p-1" aria-hidden="true"></i> 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/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/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" > <i class="bwi {{ product.icon }} tw-text-4xl !tw-m-0 !tw-mb-1"></i> 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 @@ -<div class="tw-rounded tw-border tw-border-solid tw-border-warning-500 tw-bg-background"> - <div class="tw-bg-warning-500 tw-px-5 tw-py-2.5 tw-font-bold tw-uppercase tw-text-contrast"> +<div class="tw-rounded tw-border tw-border-solid tw-border-warning-600 tw-bg-background"> + <div class="tw-bg-warning-600 tw-px-5 tw-py-2.5 tw-font-bold tw-uppercase tw-text-contrast"> <i class="bwi bwi-exclamation-triangle bwi-fw" aria-hidden="true"></i> {{ "lowKdfIterations" | i18n }} </div> 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 @@ <details #details class="tw-rounded-sm tw-bg-background-alt tw-text-main" (toggle)="toggle()" open> <summary class="tw-list-none tw-p-2 tw-px-4"> <div class="tw-flex tw-select-none tw-items-center tw-gap-4"> - <i class="bwi bwi-dashboard tw-text-3xl tw-text-primary-500" aria-hidden="true"></i> + <i class="bwi bwi-dashboard tw-text-3xl tw-text-primary-600" aria-hidden="true"></i> <div class="tw-text-lg">{{ title }}</div> <bit-progress class="tw-flex-1" [showText]="false" [barWidth]="barWidth"></bit-progress> <span *ngIf="tasks.length > 0; else spinner"> 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` <svg xmlns="http://www.w3.org/2000/svg" width="130" height="130" fill="none"> - <path class="tw-fill-secondary-500" fill-rule="evenodd" d="M22.75 29.695c0-4.991 4.074-9.037 9.1-9.037h14.3v2.582h-14.3c-3.59 0-6.5 2.89-6.5 6.455v68.428h-2.6V29.696Zm75.4 76.175V68.428h2.6v37.442c0 4.991-4.074 9.038-9.1 9.038h-53.3v-2.582h53.3c3.59 0 6.5-2.891 6.5-6.456Z" clip-rule="evenodd"/> - <path class="tw-fill-secondary-500" fill-rule="evenodd" d="M43.55 37.441c0-17.113 13.969-30.986 31.2-30.986s31.2 13.873 31.2 30.986c0 17.114-13.969 30.987-31.2 30.987s-31.2-13.873-31.2-30.986Zm31.2-33.568c-18.667 0-33.8 15.03-33.8 33.569S56.083 71.01 74.75 71.01c18.668 0 33.8-15.03 33.8-33.569S93.418 3.873 74.75 3.873Z" clip-rule="evenodd"/> - <path class="tw-fill-secondary-500" fill-rule="evenodd" d="M73.972 65.2c0 .357.291.646.65.646 15.968 0 28.925-12.71 28.925-28.404a.648.648 0 0 0-.65-.646.648.648 0 0 0-.65.646c0 14.967-12.36 27.113-27.625 27.113a.648.648 0 0 0-.65.645ZM46.347 38.087c.36 0 .65-.289.65-.645 0-14.968 12.361-27.113 27.625-27.113.36 0 .65-.29.65-.646a.648.648 0 0 0-.65-.646c-15.968 0-28.925 12.71-28.925 28.405 0 .356.291.645.65.645Z" clip-rule="evenodd"/> - <path class="tw-fill-secondary-500" fill-rule="evenodd" d="M123.729 81.869a1.926 1.926 0 0 1 0 2.739l-1.439 1.43a1.96 1.96 0 0 1-2.758 0L95.577 62.245a1.306 1.306 0 0 0-1.839 0 1.285 1.285 0 0 0 0 1.826l23.956 23.791a4.571 4.571 0 0 0 6.434 0l1.44-1.43a4.497 4.497 0 0 0 0-6.39l-23.956-23.791a1.306 1.306 0 0 0-1.838 0 1.285 1.285 0 0 0 0 1.825l23.955 23.792ZM34.45 36.797c0-.714.582-1.292 1.3-1.292h5.85c.718 0 1.3.578 1.3 1.291 0 .714-.582 1.292-1.3 1.292h-5.85c-.718 0-1.3-.578-1.3-1.291Zm0 10.973c0-.713.582-1.29 1.3-1.29h7.8c.718 0 1.3.578 1.3 1.29 0 .714-.582 1.292-1.3 1.292h-7.8c-.718 0-1.3-.578-1.3-1.291Zm0 10.975c0-.713.582-1.291 1.3-1.291H49.4c.718 0 1.3.578 1.3 1.29 0 .714-.582 1.292-1.3 1.292H35.75c-.718 0-1.3-.578-1.3-1.291Zm0 10.975c0-.714.582-1.292 1.3-1.292H72.8c.718 0 1.3.578 1.3 1.291s-.582 1.291-1.3 1.291H35.75c-.718 0-1.3-.578-1.3-1.29Zm0 10.973c0-.713.582-1.29 1.3-1.29h27.3c.718 0 1.3.577 1.3 1.29 0 .713-.582 1.291-1.3 1.291h-27.3c-.718 0-1.3-.578-1.3-1.29Zm6.5 10.975c0-.713.582-1.291 1.3-1.291H88.4c.718 0 1.3.578 1.3 1.291s-.582 1.291-1.3 1.291H42.25c-.718 0-1.3-.578-1.3-1.291Zm0 10.974c0-.713.582-1.291 1.3-1.291H88.4c.718 0 1.3.578 1.3 1.291s-.582 1.291-1.3 1.291H42.25c-.718 0-1.3-.578-1.3-1.291Z" clip-rule="evenodd"/> - <path class="tw-fill-secondary-500" fill-rule="evenodd" d="M43.664 86.742c.412.292.617.794.524 1.289l-6.366 33.964a1.305 1.305 0 0 1-1.745.968l-9.692-3.707-4.914 5.689c-.355.41-.928.557-1.438.37a1.292 1.292 0 0 1-.849-1.211v-8.444c0-.305.108-.599.306-.832l14.73-17.357a1.306 1.306 0 0 1 1.831-.156c.549.46.619 1.275.156 1.82L21.784 116.13v4.485l3.225-3.733c.358-.414.94-.56 1.454-.364l9.089 3.476 5.567-29.698-32.42 18.385 6.813 3.082c.653.296.941 1.061.643 1.71a1.303 1.303 0 0 1-1.722.64l-9.122-4.128a1.289 1.289 0 0 1-.106-2.296l37.06-21.017c.44-.249.986-.222 1.399.07Z" clip-rule="evenodd"/> + <path class="tw-fill-secondary-600" fill-rule="evenodd" d="M22.75 29.695c0-4.991 4.074-9.037 9.1-9.037h14.3v2.582h-14.3c-3.59 0-6.5 2.89-6.5 6.455v68.428h-2.6V29.696Zm75.4 76.175V68.428h2.6v37.442c0 4.991-4.074 9.038-9.1 9.038h-53.3v-2.582h53.3c3.59 0 6.5-2.891 6.5-6.456Z" clip-rule="evenodd"/> + <path class="tw-fill-secondary-600" fill-rule="evenodd" d="M43.55 37.441c0-17.113 13.969-30.986 31.2-30.986s31.2 13.873 31.2 30.986c0 17.114-13.969 30.987-31.2 30.987s-31.2-13.873-31.2-30.986Zm31.2-33.568c-18.667 0-33.8 15.03-33.8 33.569S56.083 71.01 74.75 71.01c18.668 0 33.8-15.03 33.8-33.569S93.418 3.873 74.75 3.873Z" clip-rule="evenodd"/> + <path class="tw-fill-secondary-600" fill-rule="evenodd" d="M73.972 65.2c0 .357.291.646.65.646 15.968 0 28.925-12.71 28.925-28.404a.648.648 0 0 0-.65-.646.648.648 0 0 0-.65.646c0 14.967-12.36 27.113-27.625 27.113a.648.648 0 0 0-.65.645ZM46.347 38.087c.36 0 .65-.289.65-.645 0-14.968 12.361-27.113 27.625-27.113.36 0 .65-.29.65-.646a.648.648 0 0 0-.65-.646c-15.968 0-28.925 12.71-28.925 28.405 0 .356.291.645.65.645Z" clip-rule="evenodd"/> + <path class="tw-fill-secondary-600" fill-rule="evenodd" d="M123.729 81.869a1.926 1.926 0 0 1 0 2.739l-1.439 1.43a1.96 1.96 0 0 1-2.758 0L95.577 62.245a1.306 1.306 0 0 0-1.839 0 1.285 1.285 0 0 0 0 1.826l23.956 23.791a4.571 4.571 0 0 0 6.434 0l1.44-1.43a4.497 4.497 0 0 0 0-6.39l-23.956-23.791a1.306 1.306 0 0 0-1.838 0 1.285 1.285 0 0 0 0 1.825l23.955 23.792ZM34.45 36.797c0-.714.582-1.292 1.3-1.292h5.85c.718 0 1.3.578 1.3 1.291 0 .714-.582 1.292-1.3 1.292h-5.85c-.718 0-1.3-.578-1.3-1.291Zm0 10.973c0-.713.582-1.29 1.3-1.29h7.8c.718 0 1.3.578 1.3 1.29 0 .714-.582 1.292-1.3 1.292h-7.8c-.718 0-1.3-.578-1.3-1.291Zm0 10.975c0-.713.582-1.291 1.3-1.291H49.4c.718 0 1.3.578 1.3 1.29 0 .714-.582 1.292-1.3 1.292H35.75c-.718 0-1.3-.578-1.3-1.291Zm0 10.975c0-.714.582-1.292 1.3-1.292H72.8c.718 0 1.3.578 1.3 1.291s-.582 1.291-1.3 1.291H35.75c-.718 0-1.3-.578-1.3-1.29Zm0 10.973c0-.713.582-1.29 1.3-1.29h27.3c.718 0 1.3.577 1.3 1.29 0 .713-.582 1.291-1.3 1.291h-27.3c-.718 0-1.3-.578-1.3-1.29Zm6.5 10.975c0-.713.582-1.291 1.3-1.291H88.4c.718 0 1.3.578 1.3 1.291s-.582 1.291-1.3 1.291H42.25c-.718 0-1.3-.578-1.3-1.291Zm0 10.974c0-.713.582-1.291 1.3-1.291H88.4c.718 0 1.3.578 1.3 1.291s-.582 1.291-1.3 1.291H42.25c-.718 0-1.3-.578-1.3-1.291Z" clip-rule="evenodd"/> + <path class="tw-fill-secondary-600" fill-rule="evenodd" d="M43.664 86.742c.412.292.617.794.524 1.289l-6.366 33.964a1.305 1.305 0 0 1-1.745.968l-9.692-3.707-4.914 5.689c-.355.41-.928.557-1.438.37a1.292 1.292 0 0 1-.849-1.211v-8.444c0-.305.108-.599.306-.832l14.73-17.357a1.306 1.306 0 0 1 1.831-.156c.549.46.619 1.275.156 1.82L21.784 116.13v4.485l3.225-3.733c.358-.414.94-.56 1.454-.364l9.089 3.476 5.567-29.698-32.42 18.385 6.813 3.082c.653.296.941 1.061.643 1.71a1.303 1.303 0 0 1-1.722.64l-9.122-4.128a1.289 1.289 0 0 1-.106-2.296l37.06-21.017c.44-.249.986-.222 1.399.07Z" clip-rule="evenodd"/> </svg> `; 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` <svg xmlns="http://www.w3.org/2000/svg" width="120" height="125" fill="none"> - <path class="tw-stroke-secondary-500" stroke-width="3" d="M13.425 11.994H5.99a4.311 4.311 0 0 0-4.311 4.312v62.09a4.311 4.311 0 0 0 4.311 4.311h40.09"/> - <path class="tw-stroke-secondary-500" stroke-width="3" d="M66.27 75.142h-49.9a3.234 3.234 0 0 1-3.233-3.234V9.818a3.234 3.234 0 0 1 3.234-3.233h35.764a3.233 3.233 0 0 1 2.293.953l14.134 14.216c.602.605.94 1.425.94 2.28v47.874a3.233 3.233 0 0 1-3.233 3.234Z"/> - <path class="tw-stroke-secondary-500" stroke-width="2" d="M47.021 35.586c0-3.818-2.728-6.915-6.095-6.915-3.367 0-6.096 3.097-6.096 6.915"/> - <path class="tw-stroke-secondary-500 tw-fill-secondary-100" stroke-width="2" d="M47.38 35.335H34.058a3.593 3.593 0 0 0-3.593 3.592v9.817a3.593 3.593 0 0 0 3.593 3.593H47.38a3.593 3.593 0 0 0 3.593-3.593v-9.817a3.593 3.593 0 0 0-3.593-3.592Z"/> - <path class="tw-stroke-secondary-500" stroke-linecap="round" stroke-width="2" d="M40.72 44.34v2.618"/> - <path class="tw-stroke-secondary-500" stroke-linecap="round" stroke-width="4" d="M40.72 42.7v-.373"/> - <path class="tw-stroke-secondary-500 tw-fill-secondary-100" stroke-width="3" d="M89.326 64.022s1.673-.73 2.252.572c.512 1.138-.822 2.033-.822 2.033L56.757 88.133a3.886 3.886 0 0 0-1.583 2.188l-4.732 16.705a2.665 2.665 0 0 0 .059 1.611 2.596 2.596 0 0 0 1.891 1.663c.331.07.673.071 1.004.004.402-.077.78-.25 1.102-.503l10.11-7.88a3.138 3.138 0 0 1 1.92-.663 3.08 3.08 0 0 1 1.905.662l13.926 10.948a2.556 2.556 0 0 0 3.162 0 2.71 2.71 0 0 0 .727-.879l31.777-61.762c.231-.448.33-.952.284-1.455a2.606 2.606 0 0 0-1.721-2.226 2.499 2.499 0 0 0-1.457-.06l-81.18 20.418c-.465.12-.888.364-1.223.708a2.672 2.672 0 0 0-.632 2.676c.146.46.417.865.78 1.174L46.2 83.1a4.463 4.463 0 0 0 2.565 1.572 4.489 4.489 0 0 0 2.984-.413l37.578-20.237Z"/> + <path class="tw-stroke-secondary-600" stroke-width="3" d="M13.425 11.994H5.99a4.311 4.311 0 0 0-4.311 4.312v62.09a4.311 4.311 0 0 0 4.311 4.311h40.09"/> + <path class="tw-stroke-secondary-600" stroke-width="3" d="M66.27 75.142h-49.9a3.234 3.234 0 0 1-3.233-3.234V9.818a3.234 3.234 0 0 1 3.234-3.233h35.764a3.233 3.233 0 0 1 2.293.953l14.134 14.216c.602.605.94 1.425.94 2.28v47.874a3.233 3.233 0 0 1-3.233 3.234Z"/> + <path class="tw-stroke-secondary-600" stroke-width="2" d="M47.021 35.586c0-3.818-2.728-6.915-6.095-6.915-3.367 0-6.096 3.097-6.096 6.915"/> + <path class="tw-stroke-secondary-600 tw-fill-secondary-100" stroke-width="2" d="M47.38 35.335H34.058a3.593 3.593 0 0 0-3.593 3.592v9.817a3.593 3.593 0 0 0 3.593 3.593H47.38a3.593 3.593 0 0 0 3.593-3.593v-9.817a3.593 3.593 0 0 0-3.593-3.592Z"/> + <path class="tw-stroke-secondary-600" stroke-linecap="round" stroke-width="2" d="M40.72 44.34v2.618"/> + <path class="tw-stroke-secondary-600" stroke-linecap="round" stroke-width="4" d="M40.72 42.7v-.373"/> + <path class="tw-stroke-secondary-600 tw-fill-secondary-100" stroke-width="3" d="M89.326 64.022s1.673-.73 2.252.572c.512 1.138-.822 2.033-.822 2.033L56.757 88.133a3.886 3.886 0 0 0-1.583 2.188l-4.732 16.705a2.665 2.665 0 0 0 .059 1.611 2.596 2.596 0 0 0 1.891 1.663c.331.07.673.071 1.004.004.402-.077.78-.25 1.102-.503l10.11-7.88a3.138 3.138 0 0 1 1.92-.663 3.08 3.08 0 0 1 1.905.662l13.926 10.948a2.556 2.556 0 0 0 3.162 0 2.71 2.71 0 0 0 .727-.879l31.777-61.762c.231-.448.33-.952.284-1.455a2.606 2.606 0 0 0-1.721-2.226 2.499 2.499 0 0 0-1.457-.06l-81.18 20.418c-.465.12-.888.364-1.223.708a2.672 2.672 0 0 0-.632 2.676c.146.46.417.865.78 1.174L46.2 83.1a4.463 4.463 0 0 0 2.565 1.572 4.489 4.489 0 0 0 2.984-.413l37.578-20.237Z"/> </svg> `; diff --git a/apps/web/src/app/vault/individual-vault/add-edit.component.html b/apps/web/src/app/vault/individual-vault/add-edit.component.html index 85075acfdd..db7a89003d 100644 --- a/apps/web/src/app/vault/individual-vault/add-edit.component.html +++ b/apps/web/src/app/vault/individual-vault/add-edit.component.html @@ -322,7 +322,7 @@ > <button type="button" - class="tw-items-center tw-border-none tw-bg-transparent tw-text-primary-500" + class="tw-items-center tw-border-none tw-bg-transparent tw-text-primary-600" appA11yTitle="{{ 'copyVerificationCode' | i18n }}" (click)="copy(totpCode, 'verificationCodeTotp', 'TOTP')" > diff --git a/apps/web/src/app/vault/org-vault/collection-access-restricted.component.ts b/apps/web/src/app/vault/org-vault/collection-access-restricted.component.ts index 1e2fb5690d..66018c7375 100644 --- a/apps/web/src/app/vault/org-vault/collection-access-restricted.component.ts +++ b/apps/web/src/app/vault/org-vault/collection-access-restricted.component.ts @@ -5,8 +5,8 @@ import { ButtonModule, NoItemsModule, svgIcon } from "@bitwarden/components"; import { SharedModule } from "../../shared"; const icon = svgIcon`<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" viewBox="10 -10 120 140" fill="none"> - <rect class="tw-stroke-secondary-500" width="134" height="86" x="3" y="31.485" stroke-width="6" rx="11"/> - <path class="tw-fill-secondary-500" d="M123.987 20.15H14.779a3.114 3.114 0 0 1-2.083-.95 3.036 3.036 0 0 1 0-4.208 3.125 3.125 0 0 1 2.083-.951h109.208c.792.043 1.536.38 2.083.95a3.035 3.035 0 0 1 0 4.208 3.115 3.115 0 0 1-2.083.95Zm-6.649-14.041h-95.91a3.114 3.114 0 0 1-2.082-.95 3.036 3.036 0 0 1-.848-2.105c0-.782.306-1.538.848-2.104A3.125 3.125 0 0 1 21.43 0h95.909c.791.043 1.535.38 2.082.95.547.57.849 1.322.849 2.104a3.05 3.05 0 0 1-.849 2.104 3.115 3.115 0 0 1-2.082.95ZM95.132 74.407A42.317 42.317 0 0 0 83.59 65.43l8.799-8.657a1.59 1.59 0 0 0 .004-2.27 1.641 1.641 0 0 0-2.298-.004l-9.64 9.479a28.017 28.017 0 0 0-10.483-2.13c-14.323 0-24.814 12.342-25.298 12.89a2.431 2.431 0 0 0-.675 1.64c-.01.612.215 1.203.626 1.66a43.981 43.981 0 0 0 11.873 9.485l-8.806 8.658a1.601 1.601 0 0 0-.499 1.138 1.602 1.602 0 0 0 1.008 1.5 1.651 1.651 0 0 0 1.255-.009c.199-.085.379-.205.528-.359l9.634-9.443a27.16 27.16 0 0 0 10.359 2.158c14.323 0 24.753-12.086 25.23-12.63a2.983 2.983 0 0 0-.078-4.128h.002ZM49.204 77.82a1.82 1.82 0 0 1-.43-.6 1.767 1.767 0 0 1-.152-.72 1.778 1.778 0 0 1 .582-1.32c3.857-3.564 11.782-9.686 20.77-9.676 2.564.037 5.105.508 7.508 1.395l-3.291 3.235a7.793 7.793 0 0 0-5.02-1.226 7.746 7.746 0 0 0-4.676 2.18 7.528 7.528 0 0 0-1 9.563l-4.199 4.143a43.135 43.135 0 0 1-10.092-6.974Zm26.059-1.318a5.19 5.19 0 0 1-1.557 3.68 5.326 5.326 0 0 1-3.733 1.521c-.82-.005-1.63-.2-2.359-.57l7.067-6.952c.377.718.575 1.513.582 2.321Zm-10.58 0a5.136 5.136 0 0 1 .673-2.555 5.204 5.204 0 0 1 1.862-1.897 5.302 5.302 0 0 1 5.172-.146l-7.096 6.977a5.06 5.06 0 0 1-.61-2.379Zm26.053 1.331c-3.857 3.56-11.779 9.677-20.763 9.677a22.723 22.723 0 0 1-7.454-1.369l3.292-3.226a7.793 7.793 0 0 0 4.995 1.192 7.734 7.734 0 0 0 4.642-2.176 7.524 7.524 0 0 0 1.033-9.506l4.224-4.168a43.258 43.258 0 0 1 10.02 6.945 1.788 1.788 0 0 1 .585 1.313 1.788 1.788 0 0 1-.577 1.318h.003Z"/> + <rect class="tw-stroke-secondary-600" width="134" height="86" x="3" y="31.485" stroke-width="6" rx="11"/> + <path class="tw-fill-secondary-600" d="M123.987 20.15H14.779a3.114 3.114 0 0 1-2.083-.95 3.036 3.036 0 0 1 0-4.208 3.125 3.125 0 0 1 2.083-.951h109.208c.792.043 1.536.38 2.083.95a3.035 3.035 0 0 1 0 4.208 3.115 3.115 0 0 1-2.083.95Zm-6.649-14.041h-95.91a3.114 3.114 0 0 1-2.082-.95 3.036 3.036 0 0 1-.848-2.105c0-.782.306-1.538.848-2.104A3.125 3.125 0 0 1 21.43 0h95.909c.791.043 1.535.38 2.082.95.547.57.849 1.322.849 2.104a3.05 3.05 0 0 1-.849 2.104 3.115 3.115 0 0 1-2.082.95ZM95.132 74.407A42.317 42.317 0 0 0 83.59 65.43l8.799-8.657a1.59 1.59 0 0 0 .004-2.27 1.641 1.641 0 0 0-2.298-.004l-9.64 9.479a28.017 28.017 0 0 0-10.483-2.13c-14.323 0-24.814 12.342-25.298 12.89a2.431 2.431 0 0 0-.675 1.64c-.01.612.215 1.203.626 1.66a43.981 43.981 0 0 0 11.873 9.485l-8.806 8.658a1.601 1.601 0 0 0-.499 1.138 1.602 1.602 0 0 0 1.008 1.5 1.651 1.651 0 0 0 1.255-.009c.199-.085.379-.205.528-.359l9.634-9.443a27.16 27.16 0 0 0 10.359 2.158c14.323 0 24.753-12.086 25.23-12.63a2.983 2.983 0 0 0-.078-4.128h.002ZM49.204 77.82a1.82 1.82 0 0 1-.43-.6 1.767 1.767 0 0 1-.152-.72 1.778 1.778 0 0 1 .582-1.32c3.857-3.564 11.782-9.686 20.77-9.676 2.564.037 5.105.508 7.508 1.395l-3.291 3.235a7.793 7.793 0 0 0-5.02-1.226 7.746 7.746 0 0 0-4.676 2.18 7.528 7.528 0 0 0-1 9.563l-4.199 4.143a43.135 43.135 0 0 1-10.092-6.974Zm26.059-1.318a5.19 5.19 0 0 1-1.557 3.68 5.326 5.326 0 0 1-3.733 1.521c-.82-.005-1.63-.2-2.359-.57l7.067-6.952c.377.718.575 1.513.582 2.321Zm-10.58 0a5.136 5.136 0 0 1 .673-2.555 5.204 5.204 0 0 1 1.862-1.897 5.302 5.302 0 0 1 5.172-.146l-7.096 6.977a5.06 5.06 0 0 1-.61-2.379Zm26.053 1.331c-3.857 3.56-11.779 9.677-20.763 9.677a22.723 22.723 0 0 1-7.454-1.369l3.292-3.226a7.793 7.793 0 0 0 4.995 1.192 7.734 7.734 0 0 0 4.642-2.176 7.524 7.524 0 0 0 1.033-9.506l4.224-4.168a43.258 43.258 0 0 1 10.02 6.945 1.788 1.788 0 0 1 .585 1.313 1.788 1.788 0 0 1-.577 1.318h.003Z"/> </svg>`; @Component({ 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` <svg xmlns="http://www.w3.org/2000/svg" width="163" height="115" fill="none"> - <path class="tw-fill-secondary-500" fill-rule="evenodd" d="M31 19.46H9v22h22v-22Zm-24-2v26h26v-26H7Z" + <path class="tw-fill-secondary-600" fill-rule="evenodd" d="M31 19.46H9v22h22v-22Zm-24-2v26h26v-26H7Z" clip-rule="evenodd" /> - <path class="tw-fill-secondary-500" fill-rule="evenodd" + <path class="tw-fill-secondary-600" fill-rule="evenodd" d="M0 43.46a4 4 0 0 1 4-4h32a4 4 0 0 1 4 4v7h-4v-7H4v16.747l1.705 2.149a4 4 0 0 1 .866 2.486v22.205a4 4 0 0 1-1 2.645L4 91.475v17.985h32V91.475l-1.572-1.783a4 4 0 0 1-1-2.645V64.842a4 4 0 0 1 .867-2.486L36 60.207V56.46h4v3.747a4 4 0 0 1-.867 2.487l-1.704 2.148v22.205L39 88.83a4 4 0 0 1 1 2.645v17.985a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V91.475a4 4 0 0 1 1-2.645l1.571-1.783V64.842L.867 62.694A4 4 0 0 1 0 60.207V43.46Z" clip-rule="evenodd" /> - <path class="tw-fill-secondary-500" fill-rule="evenodd" + <path class="tw-fill-secondary-600" fill-rule="evenodd" d="M19.74 63.96a.5.5 0 0 1 .355.147l2.852 2.866a.5.5 0 0 1 .146.353V77.56c2.585 1.188 4.407 3.814 4.407 6.865 0 4.183-3.357 7.534-7.5 7.534-4.144 0-7.5-3.376-7.5-7.534a7.546 7.546 0 0 1 4.478-6.894v-1.443a.5.5 0 0 1 .146-.353l1.275-1.281-1.322-1.33a.5.5 0 0 1 0-.705l.332-.334-.262-.263a.5.5 0 0 1-.005-.7l1.332-1.377-1.445-1.452a.5.5 0 0 1-.145-.352v-1.114a.5.5 0 0 1 .145-.352l2.357-2.369a.5.5 0 0 1 .355-.147Zm-1.856 3.075v.7l1.645 1.654a.5.5 0 0 1 .005.7l-1.332 1.377.267.268a.5.5 0 0 1 0 .705l-.333.334 1.323 1.329a.5.5 0 0 1 0 .705l-1.48 1.488v1.57a.5.5 0 0 1-.32.466 6.545 6.545 0 0 0-4.159 6.095c0 3.61 2.913 6.534 6.5 6.534 3.588 0 6.5-2.901 6.5-6.534 0-2.749-1.707-5.105-4.095-6.074a.5.5 0 0 1-.312-.463V67.532L19.74 65.17l-1.857 1.866ZM20 85.623a1.27 1.27 0 0 0-1.268 1.276c0 .702.56 1.276 1.268 1.276.712 0 1.268-.555 1.268-1.276A1.27 1.27 0 0 0 20 85.623Zm-2.268 1.276A2.27 2.27 0 0 1 20 84.623a2.27 2.27 0 0 1 2.268 2.276c0 1.269-1 2.276-2.268 2.276a2.27 2.27 0 0 1-2.268-2.276ZM57.623 114a1 1 0 0 1 1-1h63.048a1 1 0 0 1 0 2H58.623a1 1 0 0 1-1-1Z" clip-rule="evenodd" /> - <path class="tw-fill-secondary-500" fill-rule="evenodd" + <path class="tw-fill-secondary-600" fill-rule="evenodd" d="M78.022 114V95.654h2V114h-2ZM98.418 114V95.654h2V114h-2Z" clip-rule="evenodd" /> - <path class="tw-fill-secondary-500" fill-rule="evenodd" + <path class="tw-fill-secondary-600" fill-rule="evenodd" d="M16 14.46c0-7.732 6.268-14 14-14h119c7.732 0 14 6.268 14 14v68c0 7.732-6.268 14-14 14H39.5v-4H149c5.523 0 10-4.477 10-10v-68c0-5.523-4.477-10-10-10H30c-5.523 0-10 4.477-10 10v5h-4v-5Z" clip-rule="evenodd" /> - <path class="tw-fill-secondary-500" fill-rule="evenodd" + <path class="tw-fill-secondary-600" fill-rule="evenodd" d="M25 15.46a6 6 0 0 1 6-6h117a6 6 0 0 1 6 6v66a6 6 0 0 1-6 6H36.5v-2H148a4 4 0 0 0 4-4v-66a4 4 0 0 0-4-4H31a4 4 0 0 0-4 4v3h-2v-3Z" clip-rule="evenodd" /> - <path class="tw-fill-secondary-500" + <path class="tw-fill-secondary-600" d="M104.269 32.86a1.42 1.42 0 0 0-1.007-.4h-25.83c-.39 0-.722.132-1.007.4a1.26 1.26 0 0 0-.425.947v16.199c0 1.207.25 2.407.75 3.597a13.22 13.22 0 0 0 1.861 3.165c.74.919 1.62 1.817 2.646 2.69a30.93 30.93 0 0 0 2.834 2.172c.868.577 1.77 1.121 2.712 1.636.942.516 1.612.862 2.007 1.043.394.181.714.326.95.42.18.083.373.128.583.128.21 0 .403-.041.582-.128.241-.099.557-.239.956-.42.394-.181 1.064-.532 2.006-1.043a36.595 36.595 0 0 0 2.712-1.636c.867-.576 1.813-1.302 2.838-2.171a19.943 19.943 0 0 0 2.646-2.69 13.24 13.24 0 0 0 1.862-3.166 9.19 9.19 0 0 0 .749-3.597V33.812c.005-.367-.14-.684-.425-.952Zm-3.329 17.298c0 5.864-10.593 10.916-10.593 10.916V35.93h10.593v14.228Z" /> - <path class="tw-fill-secondary-500" fill-rule="evenodd" d="M18 24.46h-5v-2h5v2ZM27 24.46h-5v-2h5v2Z" + <path class="tw-fill-secondary-600" fill-rule="evenodd" d="M18 24.46h-5v-2h5v2ZM27 24.46h-5v-2h5v2Z" clip-rule="evenodd" /> - <path class="tw-fill-danger-500" + <path class="tw-fill-danger-600" d="M51.066 66.894a2.303 2.303 0 0 1-2.455-.5l-10.108-9.797L28.375 66.4l-.002.002a2.294 2.294 0 0 1-3.185.005 2.24 2.24 0 0 1-.506-2.496c.117-.27.286-.518.503-.728l10.062-9.737-9.945-9.623a2.258 2.258 0 0 1-.698-1.6c-.004-.314.06-.619.176-.894a2.254 2.254 0 0 1 1.257-1.222 2.305 2.305 0 0 1 1.723.014c.267.11.518.274.732.486l10.01 9.682 9.995-9.688.009-.008a2.292 2.292 0 0 1 3.159.026c.425.411.68.98.684 1.59a2.242 2.242 0 0 1-.655 1.6l-.01.01-9.926 9.627 10.008 9.7.029.027a2.237 2.237 0 0 1 .53 2.496l-.002.004a2.258 2.258 0 0 1-1.257 1.222Z" /> </svg> `; 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` <svg xmlns="http://www.w3.org/2000/svg" width="163" height="116" fill="none"> - <path class="tw-fill-secondary-500" fill-rule="evenodd" d="M31 19.58H9v22h22v-22Zm-24-2v26h26v-26H7Z" + <path class="tw-fill-secondary-600" fill-rule="evenodd" d="M31 19.58H9v22h22v-22Zm-24-2v26h26v-26H7Z" clip-rule="evenodd" /> - <path class="tw-fill-secondary-500" fill-rule="evenodd" + <path class="tw-fill-secondary-600" fill-rule="evenodd" d="M0 43.58a4 4 0 0 1 4-4h32a4 4 0 0 1 4 4v7h-4v-7H4v16.747l1.705 2.149a4 4 0 0 1 .866 2.486v22.204a4 4 0 0 1-1 2.646L4 91.595v17.985h32V91.595l-1.572-1.783a4 4 0 0 1-1-2.646V64.962a4 4 0 0 1 .867-2.486L36 60.327V56.58h4v3.747a4 4 0 0 1-.867 2.486l-1.704 2.149v22.204L39 88.95a4 4 0 0 1 1 2.646v17.985a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V91.595a4 4 0 0 1 1-2.646l1.571-1.783V64.962L.867 62.813A4 4 0 0 1 0 60.327V43.58Z" clip-rule="evenodd" /> - <path class="tw-fill-secondary-500" fill-rule="evenodd" + <path class="tw-fill-secondary-600" fill-rule="evenodd" d="M19.74 64.08a.5.5 0 0 1 .355.147l2.852 2.866a.5.5 0 0 1 .146.352V77.68c2.585 1.189 4.407 3.814 4.407 6.865 0 4.183-3.357 7.535-7.5 7.535-4.144 0-7.5-3.377-7.5-7.535a7.546 7.546 0 0 1 4.478-6.894V76.21a.5.5 0 0 1 .146-.353l1.275-1.282-1.322-1.329a.5.5 0 0 1 0-.705l.332-.334-.262-.263a.5.5 0 0 1-.005-.7l1.332-1.377-1.445-1.452a.5.5 0 0 1-.145-.353v-1.113a.5.5 0 0 1 .145-.353l2.357-2.368a.5.5 0 0 1 .355-.147Zm-1.856 3.074v.7l1.645 1.654a.5.5 0 0 1 .005.7l-1.332 1.377.267.268a.5.5 0 0 1 0 .706l-.333.334 1.323 1.329a.5.5 0 0 1 0 .705l-1.48 1.488v1.57a.5.5 0 0 1-.32.466 6.545 6.545 0 0 0-4.159 6.094c0 3.61 2.913 6.535 6.5 6.535 3.588 0 6.5-2.902 6.5-6.535 0-2.748-1.707-5.104-4.095-6.073a.5.5 0 0 1-.312-.463V67.651l-2.352-2.364-1.857 1.866ZM20 85.742a1.27 1.27 0 0 0-1.268 1.277c0 .701.56 1.276 1.268 1.276.712 0 1.268-.555 1.268-1.276A1.27 1.27 0 0 0 20 85.742Zm-2.268 1.277A2.27 2.27 0 0 1 20 84.742a2.27 2.27 0 0 1 2.268 2.277c0 1.268-1 2.276-2.268 2.276a2.27 2.27 0 0 1-2.268-2.276ZM41.796 42.844a1 1 0 0 1 1.413.058l5.526 6A1 1 0 0 1 48 50.58H27a1 1 0 1 1 0-2h18.72l-3.982-4.323a1 1 0 0 1 .058-1.413ZM33.315 62.315a1 1 0 0 1-1.413-.058l-5.526-6a1 1 0 0 1 .735-1.677h21a1 1 0 1 1 0 2h-18.72l3.982 4.322a1 1 0 0 1-.058 1.413ZM57.623 114.12a1 1 0 0 1 1-1h63.048a1 1 0 1 1 0 2H58.623a1 1 0 0 1-1-1Z" clip-rule="evenodd" /> - <path class="tw-fill-secondary-500" fill-rule="evenodd" + <path class="tw-fill-secondary-600" fill-rule="evenodd" d="M78.022 114.12V95.774h2v18.346h-2ZM98.418 114.12V95.774h2v18.346h-2Z" clip-rule="evenodd" /> - <path class="tw-fill-secondary-500" fill-rule="evenodd" + <path class="tw-fill-secondary-600" fill-rule="evenodd" d="M16 14.58c0-7.732 6.268-14 14-14h119c7.732 0 14 6.268 14 14v68c0 7.732-6.268 14-14 14H39.5v-4H149c5.523 0 10-4.478 10-10v-68c0-5.523-4.477-10-10-10H30c-5.523 0-10 4.477-10 10v5h-4v-5Z" clip-rule="evenodd" /> - <path class="tw-fill-secondary-500" fill-rule="evenodd" + <path class="tw-fill-secondary-600" fill-rule="evenodd" d="M25 15.58a6 6 0 0 1 6-6h117a6 6 0 0 1 6 6v66a6 6 0 0 1-6 6H36.5v-2H148a4 4 0 0 0 4-4v-66a4 4 0 0 0-4-4H31a4 4 0 0 0-4 4v3h-2v-3Z" clip-rule="evenodd" /> - <path class="tw-fill-secondary-500" + <path class="tw-fill-secondary-600" d="M104.269 32.98a1.42 1.42 0 0 0-1.007-.4h-25.83c-.39 0-.722.132-1.007.4a1.26 1.26 0 0 0-.425.947v16.199c0 1.207.25 2.406.75 3.597a13.222 13.222 0 0 0 1.861 3.165c.74.919 1.62 1.817 2.646 2.69a30.93 30.93 0 0 0 2.834 2.172c.868.577 1.77 1.121 2.712 1.636.942.515 1.612.861 2.007 1.043.394.18.714.325.95.42.18.082.373.128.583.128.21 0 .403-.042.582-.128.241-.099.557-.24.956-.42.394-.182 1.064-.532 2.006-1.043a36.56 36.56 0 0 0 2.712-1.636c.867-.577 1.813-1.302 2.838-2.172a19.943 19.943 0 0 0 2.646-2.69 13.24 13.24 0 0 0 1.862-3.165c.5-1.187.749-2.386.749-3.597V33.93c.005-.367-.14-.684-.425-.952Zm-3.329 17.298c0 5.864-10.593 10.916-10.593 10.916V36.049h10.593v14.23Z" /> - <path class="tw-fill-secondary-500" fill-rule="evenodd" d="M18 24.58h-5v-2h5v2ZM27 24.58h-5v-2h5v2Z" + <path class="tw-fill-secondary-600" fill-rule="evenodd" d="M18 24.58h-5v-2h5v2ZM27 24.58h-5v-2h5v2Z" clip-rule="evenodd" /> </svg> `; 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` <svg xmlns="http://www.w3.org/2000/svg" width="63" height="65" fill="none"> - <path class="tw-fill-secondary-500" fill="#89929F" fill-rule="evenodd" d="M6.539 13.582C12.113 5.786 21.228.7 31.529.7c15.02 0 27.512 10.808 30.203 25.086a2 2 0 1 1-3.93.74C55.457 14.093 44.578 4.7 31.528 4.7c-8.952 0-16.879 4.416-21.736 11.21a2 2 0 0 1-3.254-2.327Zm-.955 5.384A2 2 0 0 1 6.7 21.565a26.876 26.876 0 0 0-1.91 9.988v8.833a2 2 0 1 1-4 0v-8.833c0-4.05.778-7.923 2.195-11.472a2 2 0 0 1 2.599-1.115Zm54.685 10.587a2 2 0 0 1 2 2v8.244a2 2 0 0 1-4 0v-8.244a2 2 0 0 1 2-2Z" clip-rule="evenodd"/> - <path class="tw-fill-secondary-500" fill="#89929F" fill-rule="evenodd" d="M8.476 21.293c3.898-8.848 12.751-15.032 23.053-15.032a25.08 25.08 0 0 1 14.296 4.448A2 2 0 1 1 43.552 14a21.08 21.08 0 0 0-12.023-3.739c-8.66 0-16.11 5.196-19.392 12.645a2 2 0 1 1-3.661-1.613Zm39.328-6.481a2 2 0 0 1 2.82.211 25.062 25.062 0 0 1 6.082 16.4v19.629a2 2 0 1 1-4 0V31.423c0-5.27-1.925-10.085-5.114-13.79a2 2 0 0 1 .212-2.821ZM8.728 26.786A2 2 0 0 1 10.49 29c-.09.794-.137 1.603-.137 2.423v19.629a2 2 0 1 1-4 0V31.423c0-.972.055-1.931.163-2.876a2 2 0 0 1 2.213-1.76Z" clip-rule="evenodd"/> - <path class="tw-fill-secondary-500" fill="#89929F" fill-rule="evenodd" d="M12.223 31.097c0-10.648 8.647-19.273 19.306-19.273s19.306 8.625 19.306 19.273v25.321a2 2 0 1 1-4 0v-25.32c0-8.433-6.85-15.274-15.306-15.274s-15.305 6.841-15.305 15.273v9.913a2 2 0 1 1-4 0v-9.913Zm2 13.409a2 2 0 0 1 2 2v9.912a2 2 0 1 1-4 0v-9.912a2 2 0 0 1 2-2Z" clip-rule="evenodd"/> - <path class="tw-fill-secondary-500" fill="#89929F" fill-rule="evenodd" d="M24.698 19.044a13.418 13.418 0 0 1 6.831-1.857c7.411 0 13.434 5.984 13.434 13.385v7.851a2 2 0 1 1-4 0v-7.851c0-5.175-4.216-9.385-9.434-9.385a9.419 9.419 0 0 0-4.8 1.304 2 2 0 0 1-2.031-3.447Zm-1.76 3.755a2 2 0 0 1 .613 2.762 9.296 9.296 0 0 0-1.456 5.01v29.64a2 2 0 0 1-4 0v-29.64c0-2.63.763-5.087 2.081-7.158a2 2 0 0 1 2.761-.614Zm20.025 20.298a2 2 0 0 1 2 2v15.114a2 2 0 1 1-4 0V45.097a2 2 0 0 1 2-2Z" clip-rule="evenodd"/> - <path class="tw-fill-secondary-500" fill="#89929F" fill-rule="evenodd" d="M23.967 30.18c0-4.163 3.408-7.497 7.562-7.497s7.563 3.334 7.563 7.496v12.563a2 2 0 0 1-4 0V30.179c0-1.908-1.573-3.496-3.563-3.496-1.99 0-3.562 1.588-3.562 3.496v31.603a2 2 0 0 1-4 0V30.179ZM37.092 46.04a2 2 0 0 1 2 2v13.74a2 2 0 0 1-4 0v-13.74a2 2 0 0 1 2-2Z" clip-rule="evenodd"/> - <path class="tw-fill-secondary-500" fill="#89929F" fill-rule="evenodd" d="M31.546 28.375a2 2 0 0 1 2 2v4.908a2 2 0 1 1-4 0v-4.908a2 2 0 0 1 2-2Zm-.018 10.334a2 2 0 0 1 2.001 1.999l.017 22.25a2 2 0 1 1-4 .003l-.017-22.25a2 2 0 0 1 1.999-2.002Z" clip-rule="evenodd"/> + <path class="tw-fill-secondary-600" fill="#89929F" fill-rule="evenodd" d="M6.539 13.582C12.113 5.786 21.228.7 31.529.7c15.02 0 27.512 10.808 30.203 25.086a2 2 0 1 1-3.93.74C55.457 14.093 44.578 4.7 31.528 4.7c-8.952 0-16.879 4.416-21.736 11.21a2 2 0 0 1-3.254-2.327Zm-.955 5.384A2 2 0 0 1 6.7 21.565a26.876 26.876 0 0 0-1.91 9.988v8.833a2 2 0 1 1-4 0v-8.833c0-4.05.778-7.923 2.195-11.472a2 2 0 0 1 2.599-1.115Zm54.685 10.587a2 2 0 0 1 2 2v8.244a2 2 0 0 1-4 0v-8.244a2 2 0 0 1 2-2Z" clip-rule="evenodd"/> + <path class="tw-fill-secondary-600" fill="#89929F" fill-rule="evenodd" d="M8.476 21.293c3.898-8.848 12.751-15.032 23.053-15.032a25.08 25.08 0 0 1 14.296 4.448A2 2 0 1 1 43.552 14a21.08 21.08 0 0 0-12.023-3.739c-8.66 0-16.11 5.196-19.392 12.645a2 2 0 1 1-3.661-1.613Zm39.328-6.481a2 2 0 0 1 2.82.211 25.062 25.062 0 0 1 6.082 16.4v19.629a2 2 0 1 1-4 0V31.423c0-5.27-1.925-10.085-5.114-13.79a2 2 0 0 1 .212-2.821ZM8.728 26.786A2 2 0 0 1 10.49 29c-.09.794-.137 1.603-.137 2.423v19.629a2 2 0 1 1-4 0V31.423c0-.972.055-1.931.163-2.876a2 2 0 0 1 2.213-1.76Z" clip-rule="evenodd"/> + <path class="tw-fill-secondary-600" fill="#89929F" fill-rule="evenodd" d="M12.223 31.097c0-10.648 8.647-19.273 19.306-19.273s19.306 8.625 19.306 19.273v25.321a2 2 0 1 1-4 0v-25.32c0-8.433-6.85-15.274-15.306-15.274s-15.305 6.841-15.305 15.273v9.913a2 2 0 1 1-4 0v-9.913Zm2 13.409a2 2 0 0 1 2 2v9.912a2 2 0 1 1-4 0v-9.912a2 2 0 0 1 2-2Z" clip-rule="evenodd"/> + <path class="tw-fill-secondary-600" fill="#89929F" fill-rule="evenodd" d="M24.698 19.044a13.418 13.418 0 0 1 6.831-1.857c7.411 0 13.434 5.984 13.434 13.385v7.851a2 2 0 1 1-4 0v-7.851c0-5.175-4.216-9.385-9.434-9.385a9.419 9.419 0 0 0-4.8 1.304 2 2 0 0 1-2.031-3.447Zm-1.76 3.755a2 2 0 0 1 .613 2.762 9.296 9.296 0 0 0-1.456 5.01v29.64a2 2 0 0 1-4 0v-29.64c0-2.63.763-5.087 2.081-7.158a2 2 0 0 1 2.761-.614Zm20.025 20.298a2 2 0 0 1 2 2v15.114a2 2 0 1 1-4 0V45.097a2 2 0 0 1 2-2Z" clip-rule="evenodd"/> + <path class="tw-fill-secondary-600" fill="#89929F" fill-rule="evenodd" d="M23.967 30.18c0-4.163 3.408-7.497 7.562-7.497s7.563 3.334 7.563 7.496v12.563a2 2 0 0 1-4 0V30.179c0-1.908-1.573-3.496-3.563-3.496-1.99 0-3.562 1.588-3.562 3.496v31.603a2 2 0 0 1-4 0V30.179ZM37.092 46.04a2 2 0 0 1 2 2v13.74a2 2 0 0 1-4 0v-13.74a2 2 0 0 1 2-2Z" clip-rule="evenodd"/> + <path class="tw-fill-secondary-600" fill="#89929F" fill-rule="evenodd" d="M31.546 28.375a2 2 0 0 1 2 2v4.908a2 2 0 1 1-4 0v-4.908a2 2 0 0 1 2-2Zm-.018 10.334a2 2 0 0 1 2.001 1.999l.017 22.25a2 2 0 1 1-4 .003l-.017-22.25a2 2 0 0 1 1.999-2.002Z" clip-rule="evenodd"/> </svg> `; 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` <svg viewBox="0 0 290 45" fill="none" xmlns="http://www.w3.org/2000/svg"> <title>Bitwarden</title> - <path class="tw-fill-primary-500" fill-rule="evenodd" clip-rule="evenodd" d="M69.799 10.713c3.325 0 5.911 1.248 7.811 3.848 1.9 2.549 2.85 6.033 2.85 10.453 0 4.576-.95 8.113-2.902 10.61-1.953 2.547-4.592 3.743-7.918 3.743-3.325 0-5.858-1.144-7.758-3.536h-.528l-1.003 2.444a.976.976 0 0 1-.897.572H55.23a.94.94 0 0 1-.95-.936V1.352a.94.94 0 0 1 .95-.936h5.7a.94.94 0 0 1 .95.936v8.009c0 1.144-.105 2.964-.316 5.46h.317c1.741-2.704 4.433-4.108 7.917-4.108Zm-2.428 6.084c-1.847 0-3.273.572-4.17 1.717-.844 1.144-1.32 3.068-1.32 5.668v.832c0 2.964.423 5.097 1.32 6.345.897 1.248 2.322 1.924 4.275 1.924 1.531 0 2.85-.728 3.748-2.184.897-1.404 1.372-3.537 1.372-6.189 0-2.704-.475-4.732-1.372-6.084-.95-1.352-2.27-2.029-3.853-2.029ZM93.022 38.9h-5.7a.94.94 0 0 1-.95-.936V12.221a.94.94 0 0 1 .95-.936h5.7a.94.94 0 0 1 .95.936v25.69c.053.468-.422.988-.95.988Zm20.849-5.564c1.108 0 2.428-.208 4.011-.624a.632.632 0 0 1 .792.624v4.316a.64.64 0 0 1-.37.572c-1.794.728-4.064 1.092-6.597 1.092-3.062 0-5.278-.728-6.651-2.288-1.372-1.508-2.111-3.796-2.111-6.812V16.953h-3.008c-.37 0-.634-.26-.634-.624v-2.444c0-.052.053-.104.053-.156l4.17-2.444 2.058-5.408c.106-.26.317-.417.581-.417h3.8c.369 0 .633.26.633.625v5.252h7.548c.158 0 .317.156.317.312v4.68c0 .364-.264.624-.634.624h-7.178v13.21c0 1.04.317 1.872.897 2.34.528.572 1.373.832 2.323.832Zm35.521 5.564c-.739 0-1.319-.468-1.636-1.144l-5.595-16.797c-.369-1.196-.844-3.016-1.478-5.357h-.158l-.528 1.873-1.108 3.536-5.753 16.797c-.211.676-.845 1.092-1.584 1.092a1.628 1.628 0 0 1-1.583-1.196l-7.02-24.182c-.211-.728.369-1.508 1.214-1.508h.158c.528 0 1.003.364 1.161.884l4.117 14.717c1.003 3.849 1.689 6.657 2.006 8.53h.158c.95-3.85 1.689-6.397 2.164-7.698l5.331-15.393c.211-.624.792-1.04 1.531-1.04.686 0 1.267.416 1.478 1.04l4.961 15.29c1.214 3.9 1.953 6.396 2.217 7.696h.158c.159-1.04.792-3.952 2.006-8.633l3.958-14.509c.159-.52.634-.884 1.162-.884.791 0 1.372.728 1.161 1.508l-6.651 24.182c-.211.728-.844 1.196-1.636 1.196h-.211Zm31.352 0a.962.962 0 0 1-.95-.832l-.475-3.432h-.264c-1.372 1.716-2.745 2.964-4.223 3.692-1.425.728-3.166 1.04-5.119 1.04-2.692 0-4.751-.676-6.228-2.028-1.32-1.196-2.059-2.808-2.164-4.836-.212-2.704.95-5.305 3.166-6.813 2.27-1.456 5.437-2.34 9.712-2.34l5.173-.156v-1.768c0-2.6-.528-4.473-1.637-5.773-1.108-1.3-2.744-1.924-5.067-1.924-2.216 0-4.433.52-6.756 1.612-.58.26-1.266 0-1.53-.572s0-1.248.58-1.456c2.639-1.04 5.226-1.612 7.865-1.612 3.008 0 5.225.78 6.756 2.34 1.478 1.508 2.216 3.953 2.216 7.125v16.901c-.052.312-.527.832-1.055.832Zm-10.926-1.768c2.956 0 5.226-.832 6.862-2.444 1.689-1.612 2.533-3.952 2.533-6.813v-2.6l-4.75.208c-3.853.156-6.545.78-8.234 1.768-1.636.988-2.481 2.6-2.481 4.68 0 1.665.528 3.017 1.531 3.953 1.161.78 2.639 1.248 4.539 1.248Zm31.246-25.638c.792 0 1.584.052 2.481.156a1.176 1.176 0 0 1 1.003 1.352c-.106.624-.739.988-1.372.884-.792-.104-1.584-.208-2.375-.208-2.323 0-4.223.988-5.701 2.912-1.478 1.925-2.217 4.42-2.217 7.333v13.625c0 .676-.527 1.196-1.214 1.196-.686 0-1.213-.52-1.213-1.196V13.105c0-.572.475-1.04 1.055-1.04.581 0 1.056.416 1.056.988l.211 3.848h.158c1.109-1.976 2.323-3.38 3.589-4.16 1.214-.832 2.745-1.248 4.539-1.248Zm18.579 0c1.953 0 3.695.364 5.12 1.04 1.478.676 2.745 1.924 3.853 3.64h.158a122.343 122.343 0 0 1-.158-6.084V1.612c0-.676.528-1.196 1.214-1.196.686 0 1.214.52 1.214 1.196v36.351c0 .468-.37.832-.845.832a.852.852 0 0 1-.844-.78l-.528-3.38h-.211c-2.058 3.068-5.067 4.576-8.92 4.576-3.8 0-6.598-1.144-8.656-3.484-1.953-2.34-3.008-5.668-3.008-10.089 0-4.628.95-8.165 2.955-10.66 2.006-2.237 4.856-3.485 8.656-3.485Zm0 2.236c-3.008 0-5.225 1.04-6.756 3.12-1.478 2.029-2.216 4.993-2.216 8.945 0 7.593 3.008 11.39 9.025 11.39 3.114 0 5.331-.885 6.756-2.653 1.478-1.768 2.164-4.68 2.164-8.737v-.416c0-4.16-.686-7.124-2.164-8.893-1.372-1.872-3.642-2.756-6.809-2.756Zm31.616 25.638c-3.959 0-7.02-1.196-9.289-3.64-2.217-2.392-3.326-5.772-3.326-10.089 0-4.316 1.056-7.748 3.22-10.297 2.164-2.6 5.014-3.9 8.656-3.9 3.167 0 5.753 1.092 7.548 3.276 1.9 2.184 2.797 5.2 2.797 8.997v1.976h-19.634c.052 3.692.897 6.5 2.639 8.477 1.741 1.976 4.169 2.86 7.389 2.86 1.531 0 2.956-.104 4.117-.312.844-.156 1.847-.416 3.061-.832.686-.26 1.425.26 1.425.988 0 .416-.264.832-.686.988-1.267.52-2.481.832-3.589 1.04-1.32.364-2.745.468-4.328.468Zm-.739-25.69c-2.639 0-4.75.832-6.334 2.548-1.583 1.665-2.48 4.16-2.797 7.333h16.89c0-3.068-.686-5.564-2.059-7.28-1.372-1.717-3.272-2.6-5.7-2.6ZM288.733 38.9c-.686 0-1.214-.52-1.214-1.196V21.426c0-2.704-.58-4.68-1.689-5.877-1.214-1.196-2.955-1.872-5.383-1.872-3.273 0-5.648.78-7.126 2.444-1.478 1.613-2.322 4.265-2.322 7.853V37.6c0 .676-.528 1.196-1.214 1.196-.686 0-1.214-.52-1.214-1.196V13.105c0-.624.475-1.092 1.108-1.092.581 0 1.003.416 1.109.936l.316 2.704h.159c1.794-2.808 4.908-4.212 9.448-4.212 6.175 0 9.289 3.276 9.289 9.829V37.6c-.053.727-.633 1.3-1.267 1.3ZM90.225 0c-2.48 0-4.486 1.872-4.486 4.212v.416c0 2.289 2.058 4.213 4.486 4.213s4.486-1.924 4.486-4.213v-.364C94.711 1.872 92.653 0 90.225 0Z" /> - <path class="tw-fill-primary-500" d="M32.041 24.546V5.95H18.848v33.035c2.336-1.22 4.427-2.547 6.272-3.98 4.614-3.565 6.921-7.051 6.921-10.46Zm5.654-22.314v22.314c0 1.665-.329 3.317-.986 4.953-.658 1.637-1.473 3.09-2.445 4.359-.971 1.268-2.13 2.503-3.475 3.704-1.345 1.2-2.586 2.199-3.725 2.993a46.963 46.963 0 0 1-3.563 2.251c-1.237.707-2.116 1.187-2.636 1.439-.52.251-.938.445-1.252.58-.235.117-.49.175-.765.175s-.53-.058-.766-.174c-.314-.136-.731-.33-1.252-.581-.52-.252-1.398-.732-2.635-1.439a47.003 47.003 0 0 1-3.564-2.251c-1.138-.794-2.38-1.792-3.725-2.993-1.345-1.2-2.503-2.436-3.475-3.704-.972-1.27-1.787-2.722-2.444-4.359C.329 27.863 0 26.211 0 24.546V2.232c0-.504.187-.94.56-1.308A1.823 1.823 0 0 1 1.885.372H35.81c.511 0 .953.184 1.326.552.373.368.56.804.56 1.308Z" /> + <path class="tw-fill-primary-600" fill-rule="evenodd" clip-rule="evenodd" d="M69.799 10.713c3.325 0 5.911 1.248 7.811 3.848 1.9 2.549 2.85 6.033 2.85 10.453 0 4.576-.95 8.113-2.902 10.61-1.953 2.547-4.592 3.743-7.918 3.743-3.325 0-5.858-1.144-7.758-3.536h-.528l-1.003 2.444a.976.976 0 0 1-.897.572H55.23a.94.94 0 0 1-.95-.936V1.352a.94.94 0 0 1 .95-.936h5.7a.94.94 0 0 1 .95.936v8.009c0 1.144-.105 2.964-.316 5.46h.317c1.741-2.704 4.433-4.108 7.917-4.108Zm-2.428 6.084c-1.847 0-3.273.572-4.17 1.717-.844 1.144-1.32 3.068-1.32 5.668v.832c0 2.964.423 5.097 1.32 6.345.897 1.248 2.322 1.924 4.275 1.924 1.531 0 2.85-.728 3.748-2.184.897-1.404 1.372-3.537 1.372-6.189 0-2.704-.475-4.732-1.372-6.084-.95-1.352-2.27-2.029-3.853-2.029ZM93.022 38.9h-5.7a.94.94 0 0 1-.95-.936V12.221a.94.94 0 0 1 .95-.936h5.7a.94.94 0 0 1 .95.936v25.69c.053.468-.422.988-.95.988Zm20.849-5.564c1.108 0 2.428-.208 4.011-.624a.632.632 0 0 1 .792.624v4.316a.64.64 0 0 1-.37.572c-1.794.728-4.064 1.092-6.597 1.092-3.062 0-5.278-.728-6.651-2.288-1.372-1.508-2.111-3.796-2.111-6.812V16.953h-3.008c-.37 0-.634-.26-.634-.624v-2.444c0-.052.053-.104.053-.156l4.17-2.444 2.058-5.408c.106-.26.317-.417.581-.417h3.8c.369 0 .633.26.633.625v5.252h7.548c.158 0 .317.156.317.312v4.68c0 .364-.264.624-.634.624h-7.178v13.21c0 1.04.317 1.872.897 2.34.528.572 1.373.832 2.323.832Zm35.521 5.564c-.739 0-1.319-.468-1.636-1.144l-5.595-16.797c-.369-1.196-.844-3.016-1.478-5.357h-.158l-.528 1.873-1.108 3.536-5.753 16.797c-.211.676-.845 1.092-1.584 1.092a1.628 1.628 0 0 1-1.583-1.196l-7.02-24.182c-.211-.728.369-1.508 1.214-1.508h.158c.528 0 1.003.364 1.161.884l4.117 14.717c1.003 3.849 1.689 6.657 2.006 8.53h.158c.95-3.85 1.689-6.397 2.164-7.698l5.331-15.393c.211-.624.792-1.04 1.531-1.04.686 0 1.267.416 1.478 1.04l4.961 15.29c1.214 3.9 1.953 6.396 2.217 7.696h.158c.159-1.04.792-3.952 2.006-8.633l3.958-14.509c.159-.52.634-.884 1.162-.884.791 0 1.372.728 1.161 1.508l-6.651 24.182c-.211.728-.844 1.196-1.636 1.196h-.211Zm31.352 0a.962.962 0 0 1-.95-.832l-.475-3.432h-.264c-1.372 1.716-2.745 2.964-4.223 3.692-1.425.728-3.166 1.04-5.119 1.04-2.692 0-4.751-.676-6.228-2.028-1.32-1.196-2.059-2.808-2.164-4.836-.212-2.704.95-5.305 3.166-6.813 2.27-1.456 5.437-2.34 9.712-2.34l5.173-.156v-1.768c0-2.6-.528-4.473-1.637-5.773-1.108-1.3-2.744-1.924-5.067-1.924-2.216 0-4.433.52-6.756 1.612-.58.26-1.266 0-1.53-.572s0-1.248.58-1.456c2.639-1.04 5.226-1.612 7.865-1.612 3.008 0 5.225.78 6.756 2.34 1.478 1.508 2.216 3.953 2.216 7.125v16.901c-.052.312-.527.832-1.055.832Zm-10.926-1.768c2.956 0 5.226-.832 6.862-2.444 1.689-1.612 2.533-3.952 2.533-6.813v-2.6l-4.75.208c-3.853.156-6.545.78-8.234 1.768-1.636.988-2.481 2.6-2.481 4.68 0 1.665.528 3.017 1.531 3.953 1.161.78 2.639 1.248 4.539 1.248Zm31.246-25.638c.792 0 1.584.052 2.481.156a1.176 1.176 0 0 1 1.003 1.352c-.106.624-.739.988-1.372.884-.792-.104-1.584-.208-2.375-.208-2.323 0-4.223.988-5.701 2.912-1.478 1.925-2.217 4.42-2.217 7.333v13.625c0 .676-.527 1.196-1.214 1.196-.686 0-1.213-.52-1.213-1.196V13.105c0-.572.475-1.04 1.055-1.04.581 0 1.056.416 1.056.988l.211 3.848h.158c1.109-1.976 2.323-3.38 3.589-4.16 1.214-.832 2.745-1.248 4.539-1.248Zm18.579 0c1.953 0 3.695.364 5.12 1.04 1.478.676 2.745 1.924 3.853 3.64h.158a122.343 122.343 0 0 1-.158-6.084V1.612c0-.676.528-1.196 1.214-1.196.686 0 1.214.52 1.214 1.196v36.351c0 .468-.37.832-.845.832a.852.852 0 0 1-.844-.78l-.528-3.38h-.211c-2.058 3.068-5.067 4.576-8.92 4.576-3.8 0-6.598-1.144-8.656-3.484-1.953-2.34-3.008-5.668-3.008-10.089 0-4.628.95-8.165 2.955-10.66 2.006-2.237 4.856-3.485 8.656-3.485Zm0 2.236c-3.008 0-5.225 1.04-6.756 3.12-1.478 2.029-2.216 4.993-2.216 8.945 0 7.593 3.008 11.39 9.025 11.39 3.114 0 5.331-.885 6.756-2.653 1.478-1.768 2.164-4.68 2.164-8.737v-.416c0-4.16-.686-7.124-2.164-8.893-1.372-1.872-3.642-2.756-6.809-2.756Zm31.616 25.638c-3.959 0-7.02-1.196-9.289-3.64-2.217-2.392-3.326-5.772-3.326-10.089 0-4.316 1.056-7.748 3.22-10.297 2.164-2.6 5.014-3.9 8.656-3.9 3.167 0 5.753 1.092 7.548 3.276 1.9 2.184 2.797 5.2 2.797 8.997v1.976h-19.634c.052 3.692.897 6.5 2.639 8.477 1.741 1.976 4.169 2.86 7.389 2.86 1.531 0 2.956-.104 4.117-.312.844-.156 1.847-.416 3.061-.832.686-.26 1.425.26 1.425.988 0 .416-.264.832-.686.988-1.267.52-2.481.832-3.589 1.04-1.32.364-2.745.468-4.328.468Zm-.739-25.69c-2.639 0-4.75.832-6.334 2.548-1.583 1.665-2.48 4.16-2.797 7.333h16.89c0-3.068-.686-5.564-2.059-7.28-1.372-1.717-3.272-2.6-5.7-2.6ZM288.733 38.9c-.686 0-1.214-.52-1.214-1.196V21.426c0-2.704-.58-4.68-1.689-5.877-1.214-1.196-2.955-1.872-5.383-1.872-3.273 0-5.648.78-7.126 2.444-1.478 1.613-2.322 4.265-2.322 7.853V37.6c0 .676-.528 1.196-1.214 1.196-.686 0-1.214-.52-1.214-1.196V13.105c0-.624.475-1.092 1.108-1.092.581 0 1.003.416 1.109.936l.316 2.704h.159c1.794-2.808 4.908-4.212 9.448-4.212 6.175 0 9.289 3.276 9.289 9.829V37.6c-.053.727-.633 1.3-1.267 1.3ZM90.225 0c-2.48 0-4.486 1.872-4.486 4.212v.416c0 2.289 2.058 4.213 4.486 4.213s4.486-1.924 4.486-4.213v-.364C94.711 1.872 92.653 0 90.225 0Z" /> + <path class="tw-fill-primary-600" d="M32.041 24.546V5.95H18.848v33.035c2.336-1.22 4.427-2.547 6.272-3.98 4.614-3.565 6.921-7.051 6.921-10.46Zm5.654-22.314v22.314c0 1.665-.329 3.317-.986 4.953-.658 1.637-1.473 3.09-2.445 4.359-.971 1.268-2.13 2.503-3.475 3.704-1.345 1.2-2.586 2.199-3.725 2.993a46.963 46.963 0 0 1-3.563 2.251c-1.237.707-2.116 1.187-2.636 1.439-.52.251-.938.445-1.252.58-.235.117-.49.175-.765.175s-.53-.058-.766-.174c-.314-.136-.731-.33-1.252-.581-.52-.252-1.398-.732-2.635-1.439a47.003 47.003 0 0 1-3.564-2.251c-1.138-.794-2.38-1.792-3.725-2.993-1.345-1.2-2.503-2.436-3.475-3.704-.972-1.27-1.787-2.722-2.444-4.359C.329 27.863 0 26.211 0 24.546V2.232c0-.504.187-.94.56-1.308A1.823 1.823 0 0 1 1.885.372H35.81c.511 0 .953.184 1.326.552.373.368.56.804.56 1.308Z" /> </svg> `; 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` <svg width="65" height="80" fill="none" xmlns="http://www.w3.org/2000/svg"> - <path class="tw-fill-primary-500" d="M36.554 52.684a4.133 4.133 0 0 0-.545-2.085 4.088 4.088 0 0 0-1.514-1.518 4.022 4.022 0 0 0-4.114.072 4.094 4.094 0 0 0-1.461 1.57 4.153 4.153 0 0 0 .175 4.16c.393.616.94 1.113 1.588 1.44v6.736a1.864 1.864 0 0 0 .498 1.365c.17.18.376.328.603.425a1.781 1.781 0 0 0 1.437 0c.227-.097.432-.242.603-.425a1.864 1.864 0 0 0 .499-1.365v-6.745a4.05 4.05 0 0 0 1.62-1.498c.392-.64.604-1.377.611-2.132ZM57.86 25.527h-2.242c-.175 0-.35-.037-.514-.105a1.3 1.3 0 0 1-.434-.297 1.379 1.379 0 0 1-.39-.963v-1a23 23 0 0 0-5.455-15.32A22.46 22.46 0 0 0 34.673.101a21.633 21.633 0 0 0-8.998 1.032 21.777 21.777 0 0 0-7.813 4.637 22.118 22.118 0 0 0-5.286 7.446 22.376 22.376 0 0 0-1.855 8.975v1.62c0 .03-.118 1.705-1.555 1.73h-2.02A6.723 6.723 0 0 0 2.37 27.56 6.887 6.887 0 0 0 .4 32.403V73.12a6.905 6.905 0 0 0 1.97 4.847A6.76 6.76 0 0 0 7.146 80h50.713a6.746 6.746 0 0 0 4.77-2.03 6.925 6.925 0 0 0 1.971-4.845V32.403a6.91 6.91 0 0 0-1.965-4.85 6.793 6.793 0 0 0-2.19-1.493 6.676 6.676 0 0 0-2.588-.53l.002-.003Zm-42.2-3.335c-.007-2.55.549-5.07 1.625-7.373a17.085 17.085 0 0 1 4.606-5.945 16.8 16.8 0 0 1 6.684-3.358 16.71 16.71 0 0 1 7.462-.115c3.835.91 7.245 3.12 9.665 6.266a17.61 17.61 0 0 1 3.64 11.02v1.475c0 .18-.035.358-.102.523a1.349 1.349 0 0 1-1.244.842H17.722a1.876 1.876 0 0 1-.744-.085 1.894 1.894 0 0 1-1.119-.957 1.98 1.98 0 0 1-.204-.728v-1.565h.005ZM59.663 73.12c0 .487-.19.952-.529 1.3a1.796 1.796 0 0 1-1.279.545H7.146a1.826 1.826 0 0 1-1.807-1.845V32.403a1.85 1.85 0 0 1 .523-1.3c.168-.17.365-.308.585-.4.22-.093.454-.14.691-.143h50.719c.479.005.938.2 1.276.545.339.345.526.81.526 1.295v40.717l.003.003Z" /> + <path class="tw-fill-primary-600" d="M36.554 52.684a4.133 4.133 0 0 0-.545-2.085 4.088 4.088 0 0 0-1.514-1.518 4.022 4.022 0 0 0-4.114.072 4.094 4.094 0 0 0-1.461 1.57 4.153 4.153 0 0 0 .175 4.16c.393.616.94 1.113 1.588 1.44v6.736a1.864 1.864 0 0 0 .498 1.365c.17.18.376.328.603.425a1.781 1.781 0 0 0 1.437 0c.227-.097.432-.242.603-.425a1.864 1.864 0 0 0 .499-1.365v-6.745a4.05 4.05 0 0 0 1.62-1.498c.392-.64.604-1.377.611-2.132ZM57.86 25.527h-2.242c-.175 0-.35-.037-.514-.105a1.3 1.3 0 0 1-.434-.297 1.379 1.379 0 0 1-.39-.963v-1a23 23 0 0 0-5.455-15.32A22.46 22.46 0 0 0 34.673.101a21.633 21.633 0 0 0-8.998 1.032 21.777 21.777 0 0 0-7.813 4.637 22.118 22.118 0 0 0-5.286 7.446 22.376 22.376 0 0 0-1.855 8.975v1.62c0 .03-.118 1.705-1.555 1.73h-2.02A6.723 6.723 0 0 0 2.37 27.56 6.887 6.887 0 0 0 .4 32.403V73.12a6.905 6.905 0 0 0 1.97 4.847A6.76 6.76 0 0 0 7.146 80h50.713a6.746 6.746 0 0 0 4.77-2.03 6.925 6.925 0 0 0 1.971-4.845V32.403a6.91 6.91 0 0 0-1.965-4.85 6.793 6.793 0 0 0-2.19-1.493 6.676 6.676 0 0 0-2.588-.53l.002-.003Zm-42.2-3.335c-.007-2.55.549-5.07 1.625-7.373a17.085 17.085 0 0 1 4.606-5.945 16.8 16.8 0 0 1 6.684-3.358 16.71 16.71 0 0 1 7.462-.115c3.835.91 7.245 3.12 9.665 6.266a17.61 17.61 0 0 1 3.64 11.02v1.475c0 .18-.035.358-.102.523a1.349 1.349 0 0 1-1.244.842H17.722a1.876 1.876 0 0 1-.744-.085 1.894 1.894 0 0 1-1.119-.957 1.98 1.98 0 0 1-.204-.728v-1.565h.005ZM59.663 73.12c0 .487-.19.952-.529 1.3a1.796 1.796 0 0 1-1.279.545H7.146a1.826 1.826 0 0 1-1.807-1.845V32.403a1.85 1.85 0 0 1 .523-1.3c.168-.17.365-.308.585-.4.22-.093.454-.14.691-.143h50.719c.479.005.938.2 1.276.545.339.345.526.81.526 1.295v40.717l.003.003Z" /> </svg> `; 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/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 98048261cf..7d9fee3e8f 100644 --- a/libs/components/src/menu/menu-divider.component.html +++ b/libs/components/src/menu/menu-divider.component.html @@ -1,5 +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.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/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/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"), }, }, From cbf48decec39c84b8b0a8dd30f898386cae361df Mon Sep 17 00:00:00 2001 From: Shane Melton <smelton@bitwarden.com> Date: Fri, 5 Apr 2024 08:23:50 -0700 Subject: [PATCH 119/351] [PM-7292] Fix viewing/editing unassigned ciphers for admins (#8627) * [PM-7292] Introduce canEditUnassignedCiphers helper * [PM-7292] Use new canEditUnassignedCiphers helper * [PM-7292] Remove duplicate canUseAdminCollections helper --- apps/web/src/app/vault/org-vault/vault.component.ts | 12 +++++------- .../src/vault/components/add-edit.component.ts | 2 +- .../src/admin-console/models/domain/organization.ts | 5 +++-- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/apps/web/src/app/vault/org-vault/vault.component.ts b/apps/web/src/app/vault/org-vault/vault.component.ts index e4860f2dbc..d7cc70c583 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.ts +++ b/apps/web/src/app/vault/org-vault/vault.component.ts @@ -213,7 +213,7 @@ export class VaultComponent implements OnInit, OnDestroy { switchMap(async ([organization]) => { this.organization = organization; - if (!organization.canUseAdminCollections(this.flexibleCollectionsV1Enabled)) { + if (!organization.canEditAnyCollection(this.flexibleCollectionsV1Enabled)) { await this.syncService.fullSync(false); } @@ -407,8 +407,7 @@ export class VaultComponent implements OnInit, OnDestroy { ]).pipe( map(([filter, collection, organization]) => { return ( - (filter.collectionId === Unassigned && - !organization.canUseAdminCollections(this.flexibleCollectionsV1Enabled)) || + (filter.collectionId === Unassigned && !organization.canEditUnassignedCiphers()) || (!organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled) && collection != undefined && !collection.node.assigned) @@ -454,12 +453,11 @@ export class VaultComponent implements OnInit, OnDestroy { map(([filter, collection, organization]) => { return ( // Filtering by unassigned, show message if not admin - (filter.collectionId === Unassigned && - !organization.canUseAdminCollections(this.flexibleCollectionsV1Enabled)) || + (filter.collectionId === Unassigned && !organization.canEditUnassignedCiphers()) || // Filtering by a collection, so show message if user is not assigned (collection != undefined && !collection.node.assigned && - !organization.canUseAdminCollections(this.flexibleCollectionsV1Enabled)) + !organization.canEditAnyCollection(this.flexibleCollectionsV1Enabled)) ); }), shareReplay({ refCount: true, bufferSize: 1 }), @@ -482,7 +480,7 @@ export class VaultComponent implements OnInit, OnDestroy { (await firstValueFrom(allCipherMap$))[cipherId] != undefined; } else { canEditCipher = - organization.canUseAdminCollections(this.flexibleCollectionsV1Enabled) || + organization.canEditAnyCollection(this.flexibleCollectionsV1Enabled) || (await this.cipherService.get(cipherId)) != null; } diff --git a/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts index 36182ed9cf..6a0cfde350 100644 --- a/libs/angular/src/vault/components/add-edit.component.ts +++ b/libs/angular/src/vault/components/add-edit.component.ts @@ -662,7 +662,7 @@ export class AddEditComponent implements OnInit, OnDestroy { // 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?.canEditAllCiphers(this.flexibleCollectionsV1Enabled); + orgAdmin = this.organization?.canEditUnassignedCiphers(); } return this.cipher.id == null diff --git a/libs/common/src/admin-console/models/domain/organization.ts b/libs/common/src/admin-console/models/domain/organization.ts index 5850f4582e..bdf0b8fbbf 100644 --- a/libs/common/src/admin-console/models/domain/organization.ts +++ b/libs/common/src/admin-console/models/domain/organization.ts @@ -203,8 +203,9 @@ export class Organization { ); } - canUseAdminCollections(flexibleCollectionsV1Enabled: boolean) { - return this.canEditAnyCollection(flexibleCollectionsV1Enabled); + canEditUnassignedCiphers() { + // TODO: Update this to exclude Providers if provider access is restricted in AC-1707 + return this.isAdmin || this.permissions.editAnyCollection; } canEditAllCiphers(flexibleCollectionsV1Enabled: boolean) { From 574285abd008dc99fe8d413269312a0c5b085155 Mon Sep 17 00:00:00 2001 From: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> Date: Fri, 5 Apr 2024 11:34:25 -0500 Subject: [PATCH 120/351] [SM-1159] Use correct i18n key (#8630) --- .../projects/service-account-projects.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 772579426a..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 @@ -8,7 +8,7 @@ [label]="'projects' | i18n" [hint]="'newSaSelectAccess' | i18n" [columnTitle]="'projects' | i18n" - [emptyMessage]="'serviceAccountEmptyProjectAccessPolicies' | i18n" + [emptyMessage]="'serviceAccountEmptyProjectAccesspolicies' | i18n" (onCreateAccessPolicies)="handleCreateAccessPolicies($event)" (onDeleteAccessPolicy)="handleDeleteAccessPolicy($event)" (onUpdateAccessPolicy)="handleUpdateAccessPolicy($event)" From edf35a9ad1d1d4fe22b7462b80ed53a6f319ab7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=A8=20Audrey=20=E2=9C=A8?= <ajensen@bitwarden.com> Date: Fri, 5 Apr 2024 13:03:04 -0400 Subject: [PATCH 121/351] fix fencepost errors in padded data packer (#8631) Unit tests trim vertical pipes when appear in the data packer's JSON data, but electron is not as forgiving. It throws errors in this situation. This fixes the error by trimming the pipes before b64 decoding the result. --- .../tools/generator/state/padded-data-packer.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) 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 e2f5058b21..d1573e5cb7 100644 --- a/libs/common/src/tools/generator/state/padded-data-packer.ts +++ b/libs/common/src/tools/generator/state/padded-data-packer.ts @@ -58,11 +58,12 @@ export class PaddedDataPacker extends DataPackerAbstraction { /** {@link DataPackerAbstraction.unpack} */ unpack<Secret>(secret: string): Jsonify<Secret> { // 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); From 2ff990edd28ad9e6e6bc2c573fecb412033b5695 Mon Sep 17 00:00:00 2001 From: Addison Beck <github@addisonbeck.com> Date: Fri, 5 Apr 2024 13:10:24 -0500 Subject: [PATCH 122/351] Update policy service to clear its own state (#8564) --- .../browser/src/background/main.background.ts | 1 - apps/cli/src/bw.ts | 1 - apps/desktop/src/app/app.component.ts | 1 - apps/web/src/app/app.component.ts | 1 - .../policy/policy.service.abstraction.ts | 1 - .../services/policy/policy.service.spec.ts | 60 ------------------- .../services/policy/policy.service.ts | 9 +-- 7 files changed, 3 insertions(+), 71 deletions(-) diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 255538de52..235ccdf1aa 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1139,7 +1139,6 @@ export default class MainBackground { this.cipherService.clear(userId), this.folderService.clear(userId), this.collectionService.clear(userId), - this.policyService.clear(userId), this.passwordGenerationService.clear(userId), this.vaultTimeoutSettingsService.clear(userId), this.vaultFilterService.clear(), diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index 3815fc773b..08d43a88ef 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -702,7 +702,6 @@ 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), ]); diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index 4e74135c49..71b272b897 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -583,7 +583,6 @@ 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.biometricStateService.logout(userBeingLoggedOut as UserId); await this.providerService.save(null, userBeingLoggedOut as UserId); diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts index 32f4ee67e2..628875f04a 100644 --- a/apps/web/src/app/app.component.ts +++ b/apps/web/src/app/app.component.ts @@ -274,7 +274,6 @@ 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.biometricStateService.logout(userId as UserId), this.paymentMethodWarningService.clear(), 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<void>; replace: (policies: { [id: string]: PolicyData }) => Promise<void>; - clear: (userId?: string) => Promise<void>; } 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..a1633d29ff 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 @@ -102,66 +102,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<Policy> = { - 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 = { 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..0cbc7204de 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<PolicyData, PolicyId>(POLICIES_DISK, "policies", { +export const POLICIES = UserKeyDefinition.record<PolicyData, PolicyId>(POLICIES_DISK, "policies", { deserializer: (policyData) => policyData, + clearOn: ["logout"], }); export class PolicyService implements InternalPolicyServiceAbstraction { @@ -222,10 +223,6 @@ export class PolicyService implements InternalPolicyServiceAbstraction { await this.activeUserPolicyState.update(() => policies); } - async clear(userId?: UserId): Promise<void> { - 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 From 6df52262a94ba3c1a0e318417ede0388518244be Mon Sep 17 00:00:00 2001 From: Addison Beck <github@addisonbeck.com> Date: Fri, 5 Apr 2024 13:10:55 -0500 Subject: [PATCH 123/351] Clear provider state on logout (#8563) --- apps/browser/src/background/main.background.ts | 1 - apps/cli/src/bw.ts | 1 - apps/desktop/src/app/app.component.ts | 1 - libs/common/src/admin-console/services/provider.service.ts | 5 +++-- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 235ccdf1aa..31f0ae7279 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1143,7 +1143,6 @@ export default class MainBackground { this.vaultTimeoutSettingsService.clear(userId), this.vaultFilterService.clear(), this.biometricStateService.logout(userId), - this.providerService.save(null, userId), /* We intentionally do not clear: * - autofillSettingsService * - badgeSettingsService diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index 08d43a88ef..0e6571f775 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -703,7 +703,6 @@ export class Main { this.folderService.clear(userId), this.collectionService.clear(userId as UserId), this.passwordGenerationService.clear(), - this.providerService.save(null, userId as UserId), ]); await this.stateEventRunnerService.handleEvent("logout", userId as UserId); diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index 71b272b897..884296ea29 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -584,7 +584,6 @@ export class AppComponent implements OnInit, OnDestroy { await this.passwordGenerationService.clear(userBeingLoggedOut); await this.vaultTimeoutSettingsService.clear(userBeingLoggedOut); await this.biometricStateService.logout(userBeingLoggedOut as UserId); - await this.providerService.save(null, userBeingLoggedOut as UserId); await this.stateEventRunnerService.handleEvent("logout", userBeingLoggedOut as UserId); 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<ProviderData>(PROVIDERS_DISK, "providers", { +export const PROVIDERS = UserKeyDefinition.record<ProviderData>(PROVIDERS_DISK, "providers", { deserializer: (obj: ProviderData) => obj, + clearOn: ["logout"], }); function mapToSingleProvider(providerId: string) { From 8ae44b13ed4931fa4445a66acbf2d29be8933c9b Mon Sep 17 00:00:00 2001 From: Cesar Gonzalez <cesar.a.gonzalezcs@gmail.com> Date: Fri, 5 Apr 2024 16:33:03 -0500 Subject: [PATCH 124/351] [CL-255] Opening a menu by mouse click shows an outline on the first submenu item (#8629) --- libs/components/src/menu/menu-item.directive.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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", ]; From 216bbdb44c0d4e5f2977017518d37144a9750d12 Mon Sep 17 00:00:00 2001 From: Jonathan Prusik <jprusik@users.noreply.github.com> Date: Fri, 5 Apr 2024 17:36:52 -0400 Subject: [PATCH 125/351] fix notification bar content script using old server config state storage (#8618) --- .../abstractions/notification.background.ts | 2 ++ .../notification.background.spec.ts | 3 ++ .../background/notification.background.ts | 11 ++++++++ .../src/autofill/content/notification-bar.ts | 28 ++++++------------- .../browser/src/background/main.background.ts | 1 + 5 files changed, 25 insertions(+), 20 deletions(-) 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<boolean>; bgGetEnableAddedLoginPrompt: () => Promise<boolean>; bgGetExcludedDomains: () => Promise<NeverDomains>; + bgGetActiveUserServerConfig: () => Promise<ServerConfig>; getWebVaultUrlForNotification: () => Promise<string>; }; diff --git a/apps/browser/src/autofill/background/notification.background.spec.ts b/apps/browser/src/autofill/background/notification.background.spec.ts index 3b05cf57a9..fd15ea6e93 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"; @@ -54,6 +55,7 @@ describe("NotificationBackground", () => { const environmentService = mock<EnvironmentService>(); const logService = mock<LogService>(); const themeStateService = mock<ThemeStateService>(); + const configService = mock<ConfigService>(); beforeEach(() => { notificationBackground = new NotificationBackground( @@ -68,6 +70,7 @@ describe("NotificationBackground", () => { environmentService, logService, themeStateService, + configService, ); }); diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index c14531ee74..74e6147505 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<ServerConfig> { + 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. 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/background/main.background.ts b/apps/browser/src/background/main.background.ts index 31f0ae7279..f8a8e4fdb9 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -908,6 +908,7 @@ export default class MainBackground { this.environmentService, this.logService, themeStateService, + this.configService, ); this.overlayBackground = new OverlayBackground( this.cipherService, From 26226c4090edcc2f2ee876a4298e724aba8d48fe Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Mon, 8 Apr 2024 07:59:12 +1000 Subject: [PATCH 126/351] [AC-2356] Use safeProvider in web core services module (#8521) * Also add tests * Exclude type (compile-time) tests from jest config --- apps/web/src/app/core/core.module.ts | 203 ++++++++++-------- .../src/platform/utils/safe-provider.ts | 24 ++- .../platform/utils/safe-provider.type.spec.ts | 111 ++++++++++ libs/shared/jest.config.angular.js | 5 + 4 files changed, 251 insertions(+), 92 deletions(-) create mode 100644 libs/angular/src/platform/utils/safe-provider.type.spec.ts diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index bd514b1d18..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,6 +13,7 @@ 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"; @@ -58,97 +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, - }, - 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/libs/angular/src/platform/utils/safe-provider.ts b/libs/angular/src/platform/utils/safe-provider.ts index 7c19a280d6..e7547f9b82 100644 --- a/libs/angular/src/platform/utils/safe-provider.ts +++ b/libs/angular/src/platform/utils/safe-provider.ts @@ -85,9 +85,25 @@ type SafeConcreteProvider< 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<T extends { deps: any }> = Omit<T, "deps"> & { + useAngularDecorators: true; +}; + +/** + * Represents a type with a deps array that may optionally be overridden with useAngularDecorators + */ +type AllowAngularDecorators<T extends { deps: any }> = T | UseAngularDecorators<T>; + /** * 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). */ @@ -113,10 +129,10 @@ export const safeProvider = < DConcrete extends MapParametersToDeps<ConstructorParameters<IConcrete>>, >( provider: - | SafeClassProvider<AClass, IClass, DClass> + | AllowAngularDecorators<SafeClassProvider<AClass, IClass, DClass>> | SafeValueProvider<AValue, VValue> - | SafeFactoryProvider<AFactory, IFactory, DFactory> + | AllowAngularDecorators<SafeFactoryProvider<AFactory, IFactory, DFactory>> | SafeExistingProvider<AExisting, IExisting> - | SafeConcreteProvider<IConcrete, DConcrete> + | AllowAngularDecorators<SafeConcreteProvider<IConcrete, DConcrete>> | Constructor<unknown>, ): 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/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 From 29880606b433d56fd6cbc8b6d74fae0b82bbcb57 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 8 Apr 2024 06:00:55 +0000 Subject: [PATCH 127/351] Autosync the updated translations (#8625) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> Co-authored-by: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> --- apps/desktop/src/locales/af/messages.json | 6 +++ apps/desktop/src/locales/ar/messages.json | 6 +++ apps/desktop/src/locales/az/messages.json | 12 +++-- apps/desktop/src/locales/be/messages.json | 6 +++ apps/desktop/src/locales/bg/messages.json | 6 +++ apps/desktop/src/locales/bn/messages.json | 6 +++ apps/desktop/src/locales/bs/messages.json | 6 +++ apps/desktop/src/locales/ca/messages.json | 6 +++ apps/desktop/src/locales/cs/messages.json | 6 +++ apps/desktop/src/locales/cy/messages.json | 6 +++ apps/desktop/src/locales/da/messages.json | 6 +++ apps/desktop/src/locales/de/messages.json | 6 +++ apps/desktop/src/locales/el/messages.json | 52 +++++++++++--------- apps/desktop/src/locales/en_GB/messages.json | 6 +++ apps/desktop/src/locales/en_IN/messages.json | 6 +++ apps/desktop/src/locales/eo/messages.json | 6 +++ apps/desktop/src/locales/es/messages.json | 6 +++ apps/desktop/src/locales/et/messages.json | 6 +++ apps/desktop/src/locales/eu/messages.json | 6 +++ apps/desktop/src/locales/fa/messages.json | 6 +++ apps/desktop/src/locales/fi/messages.json | 12 +++-- apps/desktop/src/locales/fil/messages.json | 6 +++ apps/desktop/src/locales/fr/messages.json | 6 +++ apps/desktop/src/locales/gl/messages.json | 6 +++ apps/desktop/src/locales/he/messages.json | 6 +++ apps/desktop/src/locales/hi/messages.json | 6 +++ apps/desktop/src/locales/hr/messages.json | 6 +++ apps/desktop/src/locales/hu/messages.json | 6 +++ apps/desktop/src/locales/id/messages.json | 6 +++ apps/desktop/src/locales/it/messages.json | 6 +++ apps/desktop/src/locales/ja/messages.json | 6 +++ apps/desktop/src/locales/ka/messages.json | 6 +++ apps/desktop/src/locales/km/messages.json | 6 +++ apps/desktop/src/locales/kn/messages.json | 6 +++ apps/desktop/src/locales/ko/messages.json | 6 +++ apps/desktop/src/locales/lt/messages.json | 6 +++ apps/desktop/src/locales/lv/messages.json | 6 +++ apps/desktop/src/locales/me/messages.json | 6 +++ apps/desktop/src/locales/ml/messages.json | 6 +++ apps/desktop/src/locales/mr/messages.json | 6 +++ apps/desktop/src/locales/my/messages.json | 6 +++ apps/desktop/src/locales/nb/messages.json | 6 +++ apps/desktop/src/locales/ne/messages.json | 6 +++ apps/desktop/src/locales/nl/messages.json | 6 +++ apps/desktop/src/locales/nn/messages.json | 6 +++ apps/desktop/src/locales/or/messages.json | 6 +++ apps/desktop/src/locales/pl/messages.json | 6 +++ apps/desktop/src/locales/pt_BR/messages.json | 6 +++ apps/desktop/src/locales/pt_PT/messages.json | 6 +++ apps/desktop/src/locales/ro/messages.json | 6 +++ apps/desktop/src/locales/ru/messages.json | 6 +++ apps/desktop/src/locales/si/messages.json | 6 +++ apps/desktop/src/locales/sk/messages.json | 6 +++ apps/desktop/src/locales/sl/messages.json | 6 +++ apps/desktop/src/locales/sr/messages.json | 6 +++ apps/desktop/src/locales/sv/messages.json | 6 +++ apps/desktop/src/locales/te/messages.json | 6 +++ apps/desktop/src/locales/th/messages.json | 6 +++ apps/desktop/src/locales/tr/messages.json | 6 +++ apps/desktop/src/locales/uk/messages.json | 6 +++ apps/desktop/src/locales/vi/messages.json | 6 +++ apps/desktop/src/locales/zh_CN/messages.json | 6 +++ apps/desktop/src/locales/zh_TW/messages.json | 6 +++ 63 files changed, 407 insertions(+), 29 deletions(-) diff --git a/apps/desktop/src/locales/af/messages.json b/apps/desktop/src/locales/af/messages.json index b1deba9dd9..ee32c045c9 100644 --- a/apps/desktop/src/locales/af/messages.json +++ b/apps/desktop/src/locales/af/messages.json @@ -2696,5 +2696,11 @@ }, "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 b95501bbfd..104a9f7780 100644 --- a/apps/desktop/src/locales/ar/messages.json +++ b/apps/desktop/src/locales/ar/messages.json @@ -2696,5 +2696,11 @@ }, "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 a18d752620..f404c7f95a 100644 --- a/apps/desktop/src/locales/az/messages.json +++ b/apps/desktop/src/locales/az/messages.json @@ -2689,12 +2689,18 @@ "description": "Label indicating the most common import formats" }, "troubleshooting": { - "message": "Troubleshooting" + "message": "Problemlərin aradan qaldırılması" }, "disableHardwareAccelerationRestart": { - "message": "Disable hardware acceleration and restart" + "message": "Avadanlıq sürətləndirməni ləğv et və yenidən başlat" }, "enableHardwareAccelerationRestart": { - "message": "Enable hardware acceleration and restart" + "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 0529c407a4..2bc33f2b28 100644 --- a/apps/desktop/src/locales/be/messages.json +++ b/apps/desktop/src/locales/be/messages.json @@ -2696,5 +2696,11 @@ }, "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 3217f167ed..9f6d5bdd36 100644 --- a/apps/desktop/src/locales/bg/messages.json +++ b/apps/desktop/src/locales/bg/messages.json @@ -2696,5 +2696,11 @@ }, "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 9599fc6827..22893aadab 100644 --- a/apps/desktop/src/locales/bn/messages.json +++ b/apps/desktop/src/locales/bn/messages.json @@ -2696,5 +2696,11 @@ }, "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 6e6ca99bac..6adb5bbce3 100644 --- a/apps/desktop/src/locales/bs/messages.json +++ b/apps/desktop/src/locales/bs/messages.json @@ -2696,5 +2696,11 @@ }, "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 3cdedd0274..9f341f926f 100644 --- a/apps/desktop/src/locales/ca/messages.json +++ b/apps/desktop/src/locales/ca/messages.json @@ -2696,5 +2696,11 @@ }, "enableHardwareAccelerationRestart": { "message": "Activeu l'acceleració i reinicieu el maquinari" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" } } diff --git a/apps/desktop/src/locales/cs/messages.json b/apps/desktop/src/locales/cs/messages.json index efa1dccdc1..e7ba56e81c 100644 --- a/apps/desktop/src/locales/cs/messages.json +++ b/apps/desktop/src/locales/cs/messages.json @@ -2696,5 +2696,11 @@ }, "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 65ce77b340..e87b805d0b 100644 --- a/apps/desktop/src/locales/cy/messages.json +++ b/apps/desktop/src/locales/cy/messages.json @@ -2696,5 +2696,11 @@ }, "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 0da6c705ca..1f994cf8eb 100644 --- a/apps/desktop/src/locales/da/messages.json +++ b/apps/desktop/src/locales/da/messages.json @@ -2696,5 +2696,11 @@ }, "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 bacf158023..a07ad93b15 100644 --- a/apps/desktop/src/locales/de/messages.json +++ b/apps/desktop/src/locales/de/messages.json @@ -2696,5 +2696,11 @@ }, "enableHardwareAccelerationRestart": { "message": "Hardwarebeschleunigung aktivieren und neu starten" + }, + "removePasskey": { + "message": "Passkey löschen" + }, + "passkeyRemoved": { + "message": "Passkey gelöscht" } } diff --git a/apps/desktop/src/locales/el/messages.json b/apps/desktop/src/locales/el/messages.json index f5e18bdb85..63b1f21c2e 100644 --- a/apps/desktop/src/locales/el/messages.json +++ b/apps/desktop/src/locales/el/messages.json @@ -2550,22 +2550,22 @@ "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,34 +2603,34 @@ } }, "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": "Συμπερίληψη κοινόχρηστων φακέλων" @@ -2696,5 +2696,11 @@ }, "enableHardwareAccelerationRestart": { "message": "Ενεργοποίηση επιτάχυνσης υλικού και επανεκκίνηση" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" } } diff --git a/apps/desktop/src/locales/en_GB/messages.json b/apps/desktop/src/locales/en_GB/messages.json index 9b68b6de49..53958bca57 100644 --- a/apps/desktop/src/locales/en_GB/messages.json +++ b/apps/desktop/src/locales/en_GB/messages.json @@ -2696,5 +2696,11 @@ }, "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 3ffc46eba1..f6011c301f 100644 --- a/apps/desktop/src/locales/en_IN/messages.json +++ b/apps/desktop/src/locales/en_IN/messages.json @@ -2696,5 +2696,11 @@ }, "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 dcffc08ea4..772eb70985 100644 --- a/apps/desktop/src/locales/eo/messages.json +++ b/apps/desktop/src/locales/eo/messages.json @@ -2696,5 +2696,11 @@ }, "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 29c345d235..e3dcd0dc4c 100644 --- a/apps/desktop/src/locales/es/messages.json +++ b/apps/desktop/src/locales/es/messages.json @@ -2696,5 +2696,11 @@ }, "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 663cd873e5..2b54df2a91 100644 --- a/apps/desktop/src/locales/et/messages.json +++ b/apps/desktop/src/locales/et/messages.json @@ -2696,5 +2696,11 @@ }, "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 8970af1350..d66d5265e1 100644 --- a/apps/desktop/src/locales/eu/messages.json +++ b/apps/desktop/src/locales/eu/messages.json @@ -2696,5 +2696,11 @@ }, "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 a8a5758a14..c62bb99b2d 100644 --- a/apps/desktop/src/locales/fa/messages.json +++ b/apps/desktop/src/locales/fa/messages.json @@ -2696,5 +2696,11 @@ }, "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 5c069578fa..412e8fc20f 100644 --- a/apps/desktop/src/locales/fi/messages.json +++ b/apps/desktop/src/locales/fi/messages.json @@ -2689,12 +2689,18 @@ "description": "Label indicating the most common import formats" }, "troubleshooting": { - "message": "Troubleshooting" + "message": "Vianmääritys" }, "disableHardwareAccelerationRestart": { - "message": "Disable hardware acceleration and restart" + "message": "Poista laitteistokiihdytys käytöstä ja käynnistä sovellus uudelleen" }, "enableHardwareAccelerationRestart": { - "message": "Enable hardware acceleration and restart" + "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 6e6d0abaa6..170559fc64 100644 --- a/apps/desktop/src/locales/fil/messages.json +++ b/apps/desktop/src/locales/fil/messages.json @@ -2696,5 +2696,11 @@ }, "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 90dc4b0f77..20353d2d86 100644 --- a/apps/desktop/src/locales/fr/messages.json +++ b/apps/desktop/src/locales/fr/messages.json @@ -2696,5 +2696,11 @@ }, "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 11694b8c9c..f96260c005 100644 --- a/apps/desktop/src/locales/gl/messages.json +++ b/apps/desktop/src/locales/gl/messages.json @@ -2696,5 +2696,11 @@ }, "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 3a9517e8d3..cc5a0f011d 100644 --- a/apps/desktop/src/locales/he/messages.json +++ b/apps/desktop/src/locales/he/messages.json @@ -2696,5 +2696,11 @@ }, "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 86f17c1c83..57ef1d32ef 100644 --- a/apps/desktop/src/locales/hi/messages.json +++ b/apps/desktop/src/locales/hi/messages.json @@ -2696,5 +2696,11 @@ }, "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 5af3644eea..1e501cee78 100644 --- a/apps/desktop/src/locales/hr/messages.json +++ b/apps/desktop/src/locales/hr/messages.json @@ -2696,5 +2696,11 @@ }, "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 0b61e5f675..0b443c9a6b 100644 --- a/apps/desktop/src/locales/hu/messages.json +++ b/apps/desktop/src/locales/hu/messages.json @@ -2696,5 +2696,11 @@ }, "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 5290fd11de..008b6b369a 100644 --- a/apps/desktop/src/locales/id/messages.json +++ b/apps/desktop/src/locales/id/messages.json @@ -2696,5 +2696,11 @@ }, "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 2736a4a46a..a3a6f771fe 100644 --- a/apps/desktop/src/locales/it/messages.json +++ b/apps/desktop/src/locales/it/messages.json @@ -2696,5 +2696,11 @@ }, "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 a4b213a5fd..f0a95d4e35 100644 --- a/apps/desktop/src/locales/ja/messages.json +++ b/apps/desktop/src/locales/ja/messages.json @@ -2696,5 +2696,11 @@ }, "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 11694b8c9c..f96260c005 100644 --- a/apps/desktop/src/locales/ka/messages.json +++ b/apps/desktop/src/locales/ka/messages.json @@ -2696,5 +2696,11 @@ }, "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 11694b8c9c..f96260c005 100644 --- a/apps/desktop/src/locales/km/messages.json +++ b/apps/desktop/src/locales/km/messages.json @@ -2696,5 +2696,11 @@ }, "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 5f7ad11dcd..281d64cbc2 100644 --- a/apps/desktop/src/locales/kn/messages.json +++ b/apps/desktop/src/locales/kn/messages.json @@ -2696,5 +2696,11 @@ }, "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 c8e6719811..a09d53b1dd 100644 --- a/apps/desktop/src/locales/ko/messages.json +++ b/apps/desktop/src/locales/ko/messages.json @@ -2696,5 +2696,11 @@ }, "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 00186771fd..dbc2e13d1c 100644 --- a/apps/desktop/src/locales/lt/messages.json +++ b/apps/desktop/src/locales/lt/messages.json @@ -2696,5 +2696,11 @@ }, "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 f82b762d6c..0a3501dded 100644 --- a/apps/desktop/src/locales/lv/messages.json +++ b/apps/desktop/src/locales/lv/messages.json @@ -2696,5 +2696,11 @@ }, "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 533ec2ebba..d5e3bddf8e 100644 --- a/apps/desktop/src/locales/me/messages.json +++ b/apps/desktop/src/locales/me/messages.json @@ -2696,5 +2696,11 @@ }, "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 cba807216a..6b1137d232 100644 --- a/apps/desktop/src/locales/ml/messages.json +++ b/apps/desktop/src/locales/ml/messages.json @@ -2696,5 +2696,11 @@ }, "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 11694b8c9c..f96260c005 100644 --- a/apps/desktop/src/locales/mr/messages.json +++ b/apps/desktop/src/locales/mr/messages.json @@ -2696,5 +2696,11 @@ }, "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 05e3e7703e..2626e93c24 100644 --- a/apps/desktop/src/locales/my/messages.json +++ b/apps/desktop/src/locales/my/messages.json @@ -2696,5 +2696,11 @@ }, "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 a20a6a6267..8e8e2e2cbb 100644 --- a/apps/desktop/src/locales/nb/messages.json +++ b/apps/desktop/src/locales/nb/messages.json @@ -2696,5 +2696,11 @@ }, "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 40e3d11d88..e7d586023e 100644 --- a/apps/desktop/src/locales/ne/messages.json +++ b/apps/desktop/src/locales/ne/messages.json @@ -2696,5 +2696,11 @@ }, "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 0ae432ef45..9c4ba78036 100644 --- a/apps/desktop/src/locales/nl/messages.json +++ b/apps/desktop/src/locales/nl/messages.json @@ -2696,5 +2696,11 @@ }, "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 d3119f6a10..ea55378f5b 100644 --- a/apps/desktop/src/locales/nn/messages.json +++ b/apps/desktop/src/locales/nn/messages.json @@ -2696,5 +2696,11 @@ }, "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 20b704ef5e..c6c0c2fb0c 100644 --- a/apps/desktop/src/locales/or/messages.json +++ b/apps/desktop/src/locales/or/messages.json @@ -2696,5 +2696,11 @@ }, "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 60444211be..a0626a6c90 100644 --- a/apps/desktop/src/locales/pl/messages.json +++ b/apps/desktop/src/locales/pl/messages.json @@ -2696,5 +2696,11 @@ }, "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 4fb7c74889..c8f8316e6d 100644 --- a/apps/desktop/src/locales/pt_BR/messages.json +++ b/apps/desktop/src/locales/pt_BR/messages.json @@ -2696,5 +2696,11 @@ }, "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 7c325bc5f9..5fbd7636d1 100644 --- a/apps/desktop/src/locales/pt_PT/messages.json +++ b/apps/desktop/src/locales/pt_PT/messages.json @@ -2696,5 +2696,11 @@ }, "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 082a4f590b..7af8f7ec16 100644 --- a/apps/desktop/src/locales/ro/messages.json +++ b/apps/desktop/src/locales/ro/messages.json @@ -2696,5 +2696,11 @@ }, "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 5eaad37d0f..357a5757ce 100644 --- a/apps/desktop/src/locales/ru/messages.json +++ b/apps/desktop/src/locales/ru/messages.json @@ -2696,5 +2696,11 @@ }, "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 c72b2dd0e6..60e8aea93c 100644 --- a/apps/desktop/src/locales/si/messages.json +++ b/apps/desktop/src/locales/si/messages.json @@ -2696,5 +2696,11 @@ }, "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 b8777adeb1..240b883254 100644 --- a/apps/desktop/src/locales/sk/messages.json +++ b/apps/desktop/src/locales/sk/messages.json @@ -2696,5 +2696,11 @@ }, "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 fac763dec9..528274cf29 100644 --- a/apps/desktop/src/locales/sl/messages.json +++ b/apps/desktop/src/locales/sl/messages.json @@ -2696,5 +2696,11 @@ }, "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 73469b26b4..77b5f7221d 100644 --- a/apps/desktop/src/locales/sr/messages.json +++ b/apps/desktop/src/locales/sr/messages.json @@ -2696,5 +2696,11 @@ }, "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 c4c70123ac..4c41cee471 100644 --- a/apps/desktop/src/locales/sv/messages.json +++ b/apps/desktop/src/locales/sv/messages.json @@ -2696,5 +2696,11 @@ }, "enableHardwareAccelerationRestart": { "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" } } diff --git a/apps/desktop/src/locales/te/messages.json b/apps/desktop/src/locales/te/messages.json index 11694b8c9c..f96260c005 100644 --- a/apps/desktop/src/locales/te/messages.json +++ b/apps/desktop/src/locales/te/messages.json @@ -2696,5 +2696,11 @@ }, "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 a664696d5b..efbfc86b33 100644 --- a/apps/desktop/src/locales/th/messages.json +++ b/apps/desktop/src/locales/th/messages.json @@ -2696,5 +2696,11 @@ }, "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 ae3f217aa6..36d62ed2a5 100644 --- a/apps/desktop/src/locales/tr/messages.json +++ b/apps/desktop/src/locales/tr/messages.json @@ -2696,5 +2696,11 @@ }, "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 cc5273b1bd..377fd23b0b 100644 --- a/apps/desktop/src/locales/uk/messages.json +++ b/apps/desktop/src/locales/uk/messages.json @@ -2696,5 +2696,11 @@ }, "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 f81cb2778d..138773d40a 100644 --- a/apps/desktop/src/locales/vi/messages.json +++ b/apps/desktop/src/locales/vi/messages.json @@ -2696,5 +2696,11 @@ }, "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 617e8dd934..2c3401de6b 100644 --- a/apps/desktop/src/locales/zh_CN/messages.json +++ b/apps/desktop/src/locales/zh_CN/messages.json @@ -2696,5 +2696,11 @@ }, "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 1659344550..d124fc7d58 100644 --- a/apps/desktop/src/locales/zh_TW/messages.json +++ b/apps/desktop/src/locales/zh_TW/messages.json @@ -2696,5 +2696,11 @@ }, "enableHardwareAccelerationRestart": { "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" } } From 4069371b852c90570bb3491895efb524afa27dc5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 8 Apr 2024 06:05:41 +0000 Subject: [PATCH 128/351] Autosync the updated translations (#8624) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/browser/src/_locales/ar/messages.json | 6 ++++ apps/browser/src/_locales/az/messages.json | 8 ++++- apps/browser/src/_locales/be/messages.json | 6 ++++ apps/browser/src/_locales/bg/messages.json | 6 ++++ apps/browser/src/_locales/bn/messages.json | 6 ++++ apps/browser/src/_locales/bs/messages.json | 6 ++++ apps/browser/src/_locales/ca/messages.json | 6 ++++ apps/browser/src/_locales/cs/messages.json | 6 ++++ apps/browser/src/_locales/cy/messages.json | 6 ++++ apps/browser/src/_locales/da/messages.json | 6 ++++ apps/browser/src/_locales/de/messages.json | 6 ++++ apps/browser/src/_locales/el/messages.json | 6 ++++ apps/browser/src/_locales/en_GB/messages.json | 6 ++++ apps/browser/src/_locales/en_IN/messages.json | 6 ++++ apps/browser/src/_locales/es/messages.json | 6 ++++ apps/browser/src/_locales/et/messages.json | 6 ++++ apps/browser/src/_locales/eu/messages.json | 6 ++++ apps/browser/src/_locales/fa/messages.json | 6 ++++ apps/browser/src/_locales/fi/messages.json | 10 +++++-- apps/browser/src/_locales/fil/messages.json | 6 ++++ apps/browser/src/_locales/fr/messages.json | 6 ++++ apps/browser/src/_locales/gl/messages.json | 6 ++++ apps/browser/src/_locales/he/messages.json | 6 ++++ apps/browser/src/_locales/hi/messages.json | 6 ++++ apps/browser/src/_locales/hr/messages.json | 6 ++++ apps/browser/src/_locales/hu/messages.json | 6 ++++ apps/browser/src/_locales/id/messages.json | 6 ++++ apps/browser/src/_locales/it/messages.json | 6 ++++ apps/browser/src/_locales/ja/messages.json | 6 ++++ apps/browser/src/_locales/ka/messages.json | 6 ++++ apps/browser/src/_locales/km/messages.json | 6 ++++ apps/browser/src/_locales/kn/messages.json | 6 ++++ apps/browser/src/_locales/ko/messages.json | 6 ++++ apps/browser/src/_locales/lt/messages.json | 6 ++++ apps/browser/src/_locales/lv/messages.json | 6 ++++ apps/browser/src/_locales/ml/messages.json | 6 ++++ apps/browser/src/_locales/mr/messages.json | 6 ++++ apps/browser/src/_locales/my/messages.json | 6 ++++ apps/browser/src/_locales/nb/messages.json | 6 ++++ apps/browser/src/_locales/ne/messages.json | 6 ++++ apps/browser/src/_locales/nl/messages.json | 6 ++++ apps/browser/src/_locales/nn/messages.json | 6 ++++ apps/browser/src/_locales/or/messages.json | 6 ++++ apps/browser/src/_locales/pl/messages.json | 6 ++++ apps/browser/src/_locales/pt_BR/messages.json | 6 ++++ apps/browser/src/_locales/pt_PT/messages.json | 6 ++++ apps/browser/src/_locales/ro/messages.json | 6 ++++ apps/browser/src/_locales/ru/messages.json | 6 ++++ apps/browser/src/_locales/si/messages.json | 6 ++++ apps/browser/src/_locales/sk/messages.json | 6 ++++ apps/browser/src/_locales/sl/messages.json | 6 ++++ apps/browser/src/_locales/sr/messages.json | 6 ++++ apps/browser/src/_locales/sv/messages.json | 6 ++++ apps/browser/src/_locales/te/messages.json | 6 ++++ apps/browser/src/_locales/th/messages.json | 6 ++++ apps/browser/src/_locales/tr/messages.json | 6 ++++ apps/browser/src/_locales/uk/messages.json | 6 ++++ apps/browser/src/_locales/vi/messages.json | 6 ++++ apps/browser/src/_locales/zh_CN/messages.json | 6 ++++ apps/browser/src/_locales/zh_TW/messages.json | 30 +++++++++++-------- 60 files changed, 375 insertions(+), 15 deletions(-) diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index 5e3a0152a5..6000df04bb 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -2999,5 +2999,11 @@ "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" } } diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index 5d17d567fc..f7479ccf18 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,11 @@ "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" } } diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index f05102d29f..0b11a5e3e6 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -2999,5 +2999,11 @@ "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" } } diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index e96a48b3f0..7ffecb5d1f 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -2999,5 +2999,11 @@ "saveCipherAttemptFailed": { "message": "Грешка при запазването на идентификационните данни. Вижте конзолата за подробности.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Премахване на секретния ключ" + }, + "passkeyRemoved": { + "message": "Секретният ключ е премахнат" } } diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index cfaed770c1..63cb122765 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -2999,5 +2999,11 @@ "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" } } diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index 9260f5c902..e0a0633dda 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -2999,5 +2999,11 @@ "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" } } diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index b77edf5611..1572f54c80 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -2999,5 +2999,11 @@ "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": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" } } diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index bc2d7e8fd8..e818527b51 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -2999,5 +2999,11 @@ "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" } } diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index 665470b512..410c8dbe80 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -2999,5 +2999,11 @@ "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" } } diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index ab4c4e5b6e..4c686aa5ce 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -2999,5 +2999,11 @@ "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" } } diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index 0406953936..502f5a8833 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -2999,5 +2999,11 @@ "saveCipherAttemptFailed": { "message": "Fehler beim Speichern der Zugangsdaten. Details in der Konsole.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Passkey löschen" + }, + "passkeyRemoved": { + "message": "Passkey gelöscht" } } diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index 64014298e2..a698cf2ec6 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -2999,5 +2999,11 @@ "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" } } diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index 26af3b5f71..284d05d7bc 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -2999,5 +2999,11 @@ "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" } } diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index ddbc3f41c9..77d4b05427 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -2999,5 +2999,11 @@ "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" } } diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index b55c64c9f7..da9d26eb6d 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -2999,5 +2999,11 @@ "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" } } diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index b7e8b2419e..fc108fcab5 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -2999,5 +2999,11 @@ "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" } } diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index ac30dc4b28..e574d8e2e7 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -2999,5 +2999,11 @@ "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" } } diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index 373fc5a8d0..fc702246d5 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -2999,5 +2999,11 @@ "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" } } diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index 343c22d5d0..591f12421c 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -2989,15 +2989,21 @@ "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" } } diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index 2a09430c27..d1cd0687e8 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -2999,5 +2999,11 @@ "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" } } diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index 73e00ba489..adfb462a66 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -2999,5 +2999,11 @@ "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" } } diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index 4a37361f29..0f2cab77d8 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -2999,5 +2999,11 @@ "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" } } diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index d902be6af0..f9c352a683 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -2999,5 +2999,11 @@ "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" } } diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index 767edcfb95..84bc3461ae 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -2999,5 +2999,11 @@ "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" } } diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index b79477d83c..c98aae3a3b 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -2999,5 +2999,11 @@ "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" } } diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index fe4292d9c0..c720d99c71 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -2999,5 +2999,11 @@ "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." } } diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index 44f6be8cef..ecc11725e7 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -2999,5 +2999,11 @@ "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" } } diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index ac6bcc9cb5..fb87081121 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -2999,5 +2999,11 @@ "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" } } diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index 990775c084..a247ee29cb 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -2999,5 +2999,11 @@ "saveCipherAttemptFailed": { "message": "資格情報の保存中にエラーが発生しました。詳細はコンソールを確認してください。", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "パスキーを削除" + }, + "passkeyRemoved": { + "message": "パスキーを削除しました" } } diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index fdcd46bc3c..2559f4a109 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -2999,5 +2999,11 @@ "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" } } diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index 4a37361f29..0f2cab77d8 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -2999,5 +2999,11 @@ "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" } } diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index 20fb2ea458..01997a462c 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -2999,5 +2999,11 @@ "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" } } diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index 394534d6b0..a99dd11d2f 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -2999,5 +2999,11 @@ "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" } } diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index fe73a8a28a..9537241f0d 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -2999,5 +2999,11 @@ "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" } } diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index 83f3de2556..492141ff59 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -2999,5 +2999,11 @@ "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" } } diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index c2d1006694..b87a8c8ee6 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -2999,5 +2999,11 @@ "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" } } diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index 7ddbe00732..c3859f9764 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -2999,5 +2999,11 @@ "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" } } diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index 4a37361f29..0f2cab77d8 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -2999,5 +2999,11 @@ "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" } } diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index d7a8345a23..5256eba72d 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -2999,5 +2999,11 @@ "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" } } diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index 4a37361f29..0f2cab77d8 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -2999,5 +2999,11 @@ "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" } } diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index c28b99b7c2..e189f1774f 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -2999,5 +2999,11 @@ "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" } } diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index 4a37361f29..0f2cab77d8 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -2999,5 +2999,11 @@ "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" } } diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index 4a37361f29..0f2cab77d8 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -2999,5 +2999,11 @@ "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" } } diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index e4c3b7b171..4fa5fcb859 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -2999,5 +2999,11 @@ "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" } } diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index 3445a3ff5f..a21308af6a 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -2999,5 +2999,11 @@ "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" } } diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index 117c5be6b4..bd42d39535 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -2999,5 +2999,11 @@ "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" } } diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index 4851add018..49ca701a6f 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -2999,5 +2999,11 @@ "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" } } diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index fb4e2abaac..229ab31816 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -2999,5 +2999,11 @@ "saveCipherAttemptFailed": { "message": "Ошибка сохранения учетных данных. Проверьте консоль для получения подробной информации.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Удалить passkey" + }, + "passkeyRemoved": { + "message": "Passkey удален" } } diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index ca466ef7bb..9857b8ca97 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -2999,5 +2999,11 @@ "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" } } diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index 382dc82245..b6f984a04c 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -2999,5 +2999,11 @@ "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ý" } } diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index e11a56acde..a8547066e6 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -2999,5 +2999,11 @@ "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" } } diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index d598263e91..67ef9eb856 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -2999,5 +2999,11 @@ "saveCipherAttemptFailed": { "message": "Грешка при чувању акредитива. Проверите конзолу за детаље.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Уклонити приступачни кључ" + }, + "passkeyRemoved": { + "message": "Приступачни кључ је уклоњен" } } diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index 082fbac350..e37b914b28 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -2999,5 +2999,11 @@ "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" } } diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index 4a37361f29..0f2cab77d8 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -2999,5 +2999,11 @@ "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" } } diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index 011b7983d4..0ee21fb3ec 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -2999,5 +2999,11 @@ "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" } } diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index 391fef8ddc..d9da7727cc 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -2999,5 +2999,11 @@ "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" } } diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index 98dabb597d..64d3f62a78 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -2999,5 +2999,11 @@ "saveCipherAttemptFailed": { "message": "Помилка збереження облікових даних. Перегляньте подробиці в консолі.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Вилучити ключ доступу" + }, + "passkeyRemoved": { + "message": "Ключ доступу вилучено" } } diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index 3a7cf5a794..7aa43a4491 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -2999,5 +2999,11 @@ "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" } } diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index 1c269640c8..1e31baee60 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -2999,5 +2999,11 @@ "saveCipherAttemptFailed": { "message": "保存凭据时出错。检查控制台以获取详细信息。", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "移除通行密钥" + }, + "passkeyRemoved": { + "message": "通行密钥已移除" } } diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index fdd6f4639b..c47bf538b8 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,11 @@ "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" } } From 759e48728ed4ed71e2c131d09fe6a7413a796ad6 Mon Sep 17 00:00:00 2001 From: Oscar Hinton <Hinton@users.noreply.github.com> Date: Mon, 8 Apr 2024 13:18:39 +0200 Subject: [PATCH 129/351] Remove unused broadcaster service reference (#8420) --- apps/browser/src/auth/popup/two-factor.component.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/apps/browser/src/auth/popup/two-factor.component.ts b/apps/browser/src/auth/popup/two-factor.component.ts index dd541f63f8..9bac336695 100644 --- a/apps/browser/src/auth/popup/two-factor.component.ts +++ b/apps/browser/src/auth/popup/two-factor.component.ts @@ -15,7 +15,6 @@ import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/ 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 { 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"; @@ -32,8 +31,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,7 +47,6 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { platformUtilsService: PlatformUtilsService, private syncService: SyncService, environmentService: EnvironmentService, - private broadcasterService: BroadcasterService, stateService: StateService, route: ActivatedRoute, private messagingService: MessagingService, @@ -175,8 +171,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"); } From 1308b326fde194615d8e2fb41faa032598e762a1 Mon Sep 17 00:00:00 2001 From: Matt Gibson <mgibson@bitwarden.com> Date: Mon, 8 Apr 2024 07:26:22 -0500 Subject: [PATCH 130/351] Tools/specify-clearon-conditions (#8596) * Specify user clear events for event upload * Specify generator clear events * Specify clear events for user send data * Specify generic clear on logout for encrypted secret state * Allow `clearOn`event to be passed into secret state * Match current data persistence rules * Clear ui memory on lock + logout --- libs/common/src/platform/state/index.ts | 2 +- .../src/platform/state/user-key-definition.ts | 2 +- .../src/services/event/key-definitions.ts | 5 ++- .../src/tools/generator/key-definitions.ts | 39 ++++++++++++------- .../state/secret-key-definition.spec.ts | 8 ++-- .../generator/state/secret-key-definition.ts | 14 ++++--- .../username/forwarder-generator-strategy.ts | 5 ++- .../tools/send/services/key-definitions.ts | 24 ++++++++---- 8 files changed, 64 insertions(+), 35 deletions(-) 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/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<T> = KeyDefinitionOptions<T> & { +export type UserKeyDefinitionOptions<T> = KeyDefinitionOptions<T> & { clearOn: ClearEvent[]; }; 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<EventData[]> = KeyDefinition.array<EventData>( +export const EVENT_COLLECTION = UserKeyDefinition.array<EventData>( EVENT_COLLECTION_DISK, "events", { deserializer: (s) => EventData.fromJSON(s), + clearOn: ["logout"], }, ); diff --git a/libs/common/src/tools/generator/key-definitions.ts b/libs/common/src/tools/generator/key-definitions.ts index 2f35169612..074df48468 100644 --- a/libs/common/src/tools/generator/key-definitions.ts +++ b/libs/common/src/tools/generator/key-definitions.ts @@ -1,4 +1,4 @@ -import { GENERATOR_DISK, GENERATOR_MEMORY, 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"; @@ -17,110 +17,122 @@ import { import { SubaddressGenerationOptions } from "./username/subaddress-generator-options"; /** plaintext password generation options */ -export const GENERATOR_SETTINGS = new KeyDefinition<GeneratorNavigation>( +export const GENERATOR_SETTINGS = new UserKeyDefinition<GeneratorNavigation>( GENERATOR_MEMORY, "generatorSettings", { deserializer: (value) => value, + clearOn: ["lock", "logout"], }, ); /** plaintext password generation options */ -export const PASSWORD_SETTINGS = new KeyDefinition<PasswordGenerationOptions>( +export const PASSWORD_SETTINGS = new UserKeyDefinition<PasswordGenerationOptions>( GENERATOR_DISK, "passwordGeneratorSettings", { deserializer: (value) => value, + clearOn: [], }, ); /** plaintext passphrase generation options */ -export const PASSPHRASE_SETTINGS = new KeyDefinition<PassphraseGenerationOptions>( +export const PASSPHRASE_SETTINGS = new UserKeyDefinition<PassphraseGenerationOptions>( GENERATOR_DISK, "passphraseGeneratorSettings", { deserializer: (value) => value, + clearOn: [], }, ); /** plaintext username generation options */ -export const EFF_USERNAME_SETTINGS = new KeyDefinition<EffUsernameGenerationOptions>( +export const EFF_USERNAME_SETTINGS = new UserKeyDefinition<EffUsernameGenerationOptions>( GENERATOR_DISK, "effUsernameGeneratorSettings", { deserializer: (value) => value, + clearOn: [], }, ); /** plaintext configuration for a domain catch-all address. */ -export const CATCHALL_SETTINGS = new KeyDefinition<CatchallGenerationOptions>( +export const CATCHALL_SETTINGS = new UserKeyDefinition<CatchallGenerationOptions>( GENERATOR_DISK, "catchallGeneratorSettings", { deserializer: (value) => value, + clearOn: [], }, ); /** plaintext configuration for an email subaddress. */ -export const SUBADDRESS_SETTINGS = new KeyDefinition<SubaddressGenerationOptions>( +export const SUBADDRESS_SETTINGS = new UserKeyDefinition<SubaddressGenerationOptions>( GENERATOR_DISK, "subaddressGeneratorSettings", { deserializer: (value) => value, + clearOn: [], }, ); /** backing store configuration for {@link Forwarders.AddyIo} */ -export const ADDY_IO_FORWARDER = new KeyDefinition<SelfHostedApiOptions & EmailDomainOptions>( +export const ADDY_IO_FORWARDER = new UserKeyDefinition<SelfHostedApiOptions & EmailDomainOptions>( GENERATOR_DISK, "addyIoForwarder", { deserializer: (value) => value, + clearOn: [], }, ); /** backing store configuration for {@link Forwarders.DuckDuckGo} */ -export const DUCK_DUCK_GO_FORWARDER = new KeyDefinition<ApiOptions>( +export const DUCK_DUCK_GO_FORWARDER = new UserKeyDefinition<ApiOptions>( GENERATOR_DISK, "duckDuckGoForwarder", { deserializer: (value) => value, + clearOn: [], }, ); /** backing store configuration for {@link Forwarders.FastMail} */ -export const FASTMAIL_FORWARDER = new KeyDefinition<ApiOptions & EmailPrefixOptions>( +export const FASTMAIL_FORWARDER = new UserKeyDefinition<ApiOptions & EmailPrefixOptions>( GENERATOR_DISK, "fastmailForwarder", { deserializer: (value) => value, + clearOn: [], }, ); /** backing store configuration for {@link Forwarders.FireFoxRelay} */ -export const FIREFOX_RELAY_FORWARDER = new KeyDefinition<ApiOptions>( +export const FIREFOX_RELAY_FORWARDER = new UserKeyDefinition<ApiOptions>( GENERATOR_DISK, "firefoxRelayForwarder", { deserializer: (value) => value, + clearOn: [], }, ); /** backing store configuration for {@link Forwarders.ForwardEmail} */ -export const FORWARD_EMAIL_FORWARDER = new KeyDefinition<ApiOptions & EmailDomainOptions>( +export const FORWARD_EMAIL_FORWARDER = new UserKeyDefinition<ApiOptions & EmailDomainOptions>( GENERATOR_DISK, "forwardEmailForwarder", { deserializer: (value) => value, + clearOn: [], }, ); /** backing store configuration for {@link forwarders.SimpleLogin} */ -export const SIMPLE_LOGIN_FORWARDER = new KeyDefinition<SelfHostedApiOptions>( +export const SIMPLE_LOGIN_FORWARDER = new UserKeyDefinition<SelfHostedApiOptions>( GENERATOR_DISK, "simpleLoginForwarder", { deserializer: (value) => value, + clearOn: [], }, ); @@ -131,5 +143,6 @@ export const GENERATOR_HISTORY = SecretKeyDefinition.array( SecretClassifier.allSecret<GeneratedCredential>(), { deserializer: GeneratedCredential.fromJSON, + clearOn: ["logout"], }, ); 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 7352631ff6..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,16 +1,17 @@ -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 = { + const expectedOptions: UserKeyDefinitionOptions<any> = { deserializer: (v: any) => v, cleanupDelayMs: 100, + clearOn: ["logout", "lock"], }; const definition = SecretKeyDefinition.value( GENERATOR_DISK, @@ -26,6 +27,7 @@ describe("SecretKeyDefinition", () => { 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); }); 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 0de59be624..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,4 +1,4 @@ -import { KeyDefinition, 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"; @@ -11,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. @@ -21,12 +21,14 @@ export class SecretKeyDefinition<Outer, Id, Inner extends object, Disclosed, Sec /** Converts the secret key to the `KeyDefinition` used for secret storage. */ toEncryptedStateKey() { - const secretKey = new KeyDefinition<ClassifiedFormat<Id, Disclosed>[]>( + 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, }, ); @@ -45,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, @@ -69,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, @@ -93,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/username/forwarder-generator-strategy.ts b/libs/common/src/tools/generator/username/forwarder-generator-strategy.ts index 086e347669..b4205b9fc9 100644 --- a/libs/common/src/tools/generator/username/forwarder-generator-strategy.ts +++ b/libs/common/src/tools/generator/username/forwarder-generator-strategy.ts @@ -3,7 +3,7 @@ import { Observable, map, pipe } from "rxjs"; import { PolicyType } from "../../../admin-console/enums"; 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"; @@ -56,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">` @@ -83,7 +84,7 @@ export abstract class ForwarderGeneratorStrategy< abstract defaults$: (userId: UserId) => Observable<Options>; /** Determine where forwarder configuration is stored */ - protected abstract readonly key: KeyDefinition<Options>; + protected abstract readonly key: UserKeyDefinition<Options>; /** {@link GeneratorStrategy.toEvaluator} */ toEvaluator = () => { diff --git a/libs/common/src/tools/send/services/key-definitions.ts b/libs/common/src/tools/send/services/key-definitions.ts index b117c52268..f1a6b3d6c6 100644 --- a/libs/common/src/tools/send/services/key-definitions.ts +++ b/libs/common/src/tools/send/services/key-definitions.ts @@ -1,13 +1,23 @@ -import { KeyDefinition, SEND_DISK, SEND_MEMORY } from "../../../platform/state"; +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 = KeyDefinition.record<SendData>(SEND_DISK, "sendUserEncrypted", { - deserializer: (obj: SendData) => obj, -}); +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 KeyDefinition<SendView[]>(SEND_MEMORY, "sendUserDecrypted", { - deserializer: (obj) => obj, -}); +export const SEND_USER_DECRYPTED = new UserKeyDefinition<SendView[]>( + SEND_MEMORY, + "sendUserDecrypted", + { + deserializer: (obj) => obj, + clearOn: ["lock"], + }, +); From 3d052242dfa97412056376f982c754505e00ecda Mon Sep 17 00:00:00 2001 From: Tom <144813356+ttalty@users.noreply.github.com> Date: Mon, 8 Apr 2024 10:30:39 -0400 Subject: [PATCH 131/351] [PM-5578] [PM-5579] [PM-5580] [PM-5581] Send Browser State Provider (#8232) * Replacing state service with state provider * Documentation indicating the differences between the 2 states used. * Creating key definition, updating comments, and modifying test cases * Adding the key definitions tests * Documenting the observables * Fixing the test issue with the awaitAsync import * Removing browser state service stuff for merge fix * no need to redefine interface members * Renaming to DefaultBrowserStateService --- .../notification.background.spec.ts | 4 +- .../background/overlay.background.spec.ts | 4 +- .../browser/src/background/main.background.ts | 4 +- .../state-service.factory.ts | 8 +-- .../browser-session.decorator.spec.ts | 6 +- .../abstractions/browser-state.service.ts | 16 +---- .../services/browser-state.service.spec.ts | 44 ++++--------- ...ce.ts => default-browser-state.service.ts} | 48 +------------- apps/browser/src/popup/app.component.ts | 6 +- .../src/popup/services/services.module.ts | 10 ++- .../popup/send/send-groupings.component.ts | 4 +- .../tools/popup/send/send-type.component.ts | 4 +- .../browser-send-state.service.spec.ts | 61 +++++++++++++++++ .../services/browser-send-state.service.ts | 65 +++++++++++++++++++ .../popup/services/key-definitions.spec.ts | 40 ++++++++++++ .../tools/popup/services/key-definitions.ts | 23 +++++++ .../src/platform/state/state-definitions.ts | 1 + 17 files changed, 236 insertions(+), 112 deletions(-) rename apps/browser/src/platform/services/{browser-state.service.ts => default-browser-state.service.ts} (74%) create mode 100644 apps/browser/src/tools/popup/services/browser-send-state.service.spec.ts create mode 100644 apps/browser/src/tools/popup/services/browser-send-state.service.ts create mode 100644 apps/browser/src/tools/popup/services/key-definitions.spec.ts create mode 100644 apps/browser/src/tools/popup/services/key-definitions.ts diff --git a/apps/browser/src/autofill/background/notification.background.spec.ts b/apps/browser/src/autofill/background/notification.background.spec.ts index fd15ea6e93..93750ece07 100644 --- a/apps/browser/src/autofill/background/notification.background.spec.ts +++ b/apps/browser/src/autofill/background/notification.background.spec.ts @@ -17,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"; @@ -49,7 +49,7 @@ describe("NotificationBackground", () => { const authService = mock<AuthService>(); const policyService = mock<PolicyService>(); const folderService = mock<FolderService>(); - const stateService = mock<BrowserStateService>(); + const stateService = mock<DefaultBrowserStateService>(); const userNotificationSettingsService = mock<UserNotificationSettingsService>(); const domainSettingsService = mock<DomainSettingsService>(); const environmentService = mock<EnvironmentService>(); diff --git a/apps/browser/src/autofill/background/overlay.background.spec.ts b/apps/browser/src/autofill/background/overlay.background.spec.ts index c06df6603b..2599c1825e 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<BrowserStateService>(); + const stateService = mock<DefaultBrowserStateService>(); const autofillSettingsService = mock<AutofillSettingsService>(); const i18nService = mock<I18nService>(); const platformUtilsService = mock<BrowserPlatformUtilsService>(); diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index f8a8e4fdb9..a7fadc6d6f 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -211,7 +211,7 @@ import BrowserLocalStorageService from "../platform/services/browser-local-stora import BrowserMemoryStorageService from "../platform/services/browser-memory-storage.service"; import BrowserMessagingPrivateModeBackgroundService from "../platform/services/browser-messaging-private-mode-background.service"; import BrowserMessagingService from "../platform/services/browser-messaging.service"; -import { BrowserStateService } from "../platform/services/browser-state.service"; +import { DefaultBrowserStateService } from "../platform/services/default-browser-state.service"; import I18nService from "../platform/services/i18n.service"; import { LocalBackedSessionStorageService } from "../platform/services/local-backed-session-storage.service"; import { BackgroundPlatformUtilsService } from "../platform/services/platform-utils/background-platform-utils.service"; @@ -466,7 +466,7 @@ export default class MainBackground { new MigrationBuilderService(), ); - this.stateService = new BrowserStateService( + this.stateService = new DefaultBrowserStateService( this.storageService, this.secureStorageService, this.memoryStorageService, diff --git a/apps/browser/src/platform/background/service-factories/state-service.factory.ts b/apps/browser/src/platform/background/service-factories/state-service.factory.ts index 20a9ac074a..5567e00990 100644 --- a/apps/browser/src/platform/background/service-factories/state-service.factory.ts +++ b/apps/browser/src/platform/background/service-factories/state-service.factory.ts @@ -10,7 +10,7 @@ import { TokenServiceInitOptions, } from "../../../auth/background/service-factories/token-service.factory"; import { Account } from "../../../models/account"; -import { BrowserStateService } from "../../services/browser-state.service"; +import { DefaultBrowserStateService } from "../../services/default-browser-state.service"; import { environmentServiceFactory, @@ -46,15 +46,15 @@ export type StateServiceInitOptions = StateServiceFactoryOptions & MigrationRunnerInitOptions; export async function stateServiceFactory( - cache: { stateService?: BrowserStateService } & CachedServices, + cache: { stateService?: DefaultBrowserStateService } & CachedServices, opts: StateServiceInitOptions, -): Promise<BrowserStateService> { +): Promise<DefaultBrowserStateService> { const service = await factory( cache, "stateService", opts, async () => - new BrowserStateService( + new DefaultBrowserStateService( await diskStorageServiceFactory(cache, opts), await secureStorageServiceFactory(cache, opts), await memoryStorageServiceFactory(cache, opts), diff --git a/apps/browser/src/platform/decorators/session-sync-observable/browser-session.decorator.spec.ts b/apps/browser/src/platform/decorators/session-sync-observable/browser-session.decorator.spec.ts index 4b0226d54e..2092f6992b 100644 --- a/apps/browser/src/platform/decorators/session-sync-observable/browser-session.decorator.spec.ts +++ b/apps/browser/src/platform/decorators/session-sync-observable/browser-session.decorator.spec.ts @@ -3,7 +3,7 @@ import { BehaviorSubject } from "rxjs"; import { AbstractMemoryStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; -import { BrowserStateService } from "../../services/browser-state.service"; +import { DefaultBrowserStateService } from "../../services/default-browser-state.service"; import { browserSession } from "./browser-session.decorator"; import { SessionStorable } from "./session-storable"; @@ -25,7 +25,7 @@ describe("browserSession decorator", () => { }); it("should create if StateService is a constructor argument", () => { - const stateService = Object.create(BrowserStateService.prototype, { + const stateService = Object.create(DefaultBrowserStateService.prototype, { memoryStorageService: { value: Object.create(MemoryStorageService.prototype, { type: { value: MemoryStorageService.TYPE }, @@ -35,7 +35,7 @@ describe("browserSession decorator", () => { @browserSession class TestClass { - constructor(private stateService: BrowserStateService) {} + constructor(private stateService: DefaultBrowserStateService) {} } expect(new TestClass(stateService)).toBeDefined(); diff --git a/apps/browser/src/platform/services/abstractions/browser-state.service.ts b/apps/browser/src/platform/services/abstractions/browser-state.service.ts index 82ec54975a..c8e2c502e7 100644 --- a/apps/browser/src/platform/services/abstractions/browser-state.service.ts +++ b/apps/browser/src/platform/services/abstractions/browser-state.service.ts @@ -1,19 +1,5 @@ import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; -import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options"; import { Account } from "../../../models/account"; -import { BrowserComponentState } from "../../../models/browserComponentState"; -import { BrowserSendComponentState } from "../../../models/browserSendComponentState"; -export abstract class BrowserStateService extends BaseStateServiceAbstraction<Account> { - getBrowserSendComponentState: (options?: StorageOptions) => Promise<BrowserSendComponentState>; - setBrowserSendComponentState: ( - value: BrowserSendComponentState, - options?: StorageOptions, - ) => Promise<void>; - getBrowserSendTypeComponentState: (options?: StorageOptions) => Promise<BrowserComponentState>; - setBrowserSendTypeComponentState: ( - value: BrowserComponentState, - options?: StorageOptions, - ) => Promise<void>; -} +export abstract class BrowserStateService extends BaseStateServiceAbstraction<Account> {} diff --git a/apps/browser/src/platform/services/browser-state.service.spec.ts b/apps/browser/src/platform/services/browser-state.service.spec.ts index 7e75b9b707..8f43998321 100644 --- a/apps/browser/src/platform/services/browser-state.service.spec.ts +++ b/apps/browser/src/platform/services/browser-state.service.spec.ts @@ -1,4 +1,5 @@ import { mock, MockProxy } from "jest-mock-extended"; +import { firstValueFrom } from "rxjs"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; @@ -12,15 +13,11 @@ import { GlobalState } from "@bitwarden/common/platform/models/domain/global-sta import { State } from "@bitwarden/common/platform/models/domain/state"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; import { mockAccountServiceWith } from "@bitwarden/common/spec"; -import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; -import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; import { UserId } from "@bitwarden/common/types/guid"; import { Account } from "../../models/account"; -import { BrowserComponentState } from "../../models/browserComponentState"; -import { BrowserSendComponentState } from "../../models/browserSendComponentState"; -import { BrowserStateService } from "./browser-state.service"; +import { DefaultBrowserStateService } from "./default-browser-state.service"; // disable session syncing to just test class jest.mock("../decorators/session-sync-observable/"); @@ -39,7 +36,7 @@ describe("Browser State Service", () => { const userId = "userId" as UserId; const accountService = mockAccountServiceWith(userId); - let sut: BrowserStateService; + let sut: DefaultBrowserStateService; beforeEach(() => { secureStorageService = mock(); @@ -71,7 +68,7 @@ describe("Browser State Service", () => { const stateGetter = (key: string) => Promise.resolve(state); memoryStorageService.get.mockImplementation(stateGetter); - sut = new BrowserStateService( + sut = new DefaultBrowserStateService( diskStorageService, secureStorageService, memoryStorageService, @@ -85,32 +82,17 @@ describe("Browser State Service", () => { ); }); - describe("getBrowserSendComponentState", () => { - it("should return a BrowserSendComponentState", async () => { - const sendState = new BrowserSendComponentState(); - sendState.sends = [new SendView(), new SendView()]; - sendState.typeCounts = new Map<SendType, number>([ - [SendType.File, 3], - [SendType.Text, 5], - ]); - state.accounts[userId].send = sendState; - (global as any)["watch"] = state; + describe("add Account", () => { + it("should add account", async () => { + const newUserId = "newUserId" as UserId; + const newAcct = new Account({ + profile: { userId: newUserId }, + }); - const actual = await sut.getBrowserSendComponentState(); - expect(actual).toBeInstanceOf(BrowserSendComponentState); - expect(actual).toMatchObject(sendState); - }); - }); + await sut.addAccount(newAcct); - describe("getBrowserSendTypeComponentState", () => { - it("should return a BrowserComponentState", async () => { - const componentState = new BrowserComponentState(); - componentState.scrollY = 0; - componentState.searchText = "test"; - state.accounts[userId].sendType = componentState; - - const actual = await sut.getBrowserSendTypeComponentState(); - expect(actual).toStrictEqual(componentState); + const accts = await firstValueFrom(sut.accounts$); + expect(accts[newUserId]).toBeDefined(); }); }); }); diff --git a/apps/browser/src/platform/services/browser-state.service.ts b/apps/browser/src/platform/services/default-browser-state.service.ts similarity index 74% rename from apps/browser/src/platform/services/browser-state.service.ts rename to apps/browser/src/platform/services/default-browser-state.service.ts index ea410ee83a..f1f306dbc0 100644 --- a/apps/browser/src/platform/services/browser-state.service.ts +++ b/apps/browser/src/platform/services/default-browser-state.service.ts @@ -15,17 +15,15 @@ import { MigrationRunner } from "@bitwarden/common/platform/services/migration-r import { StateService as BaseStateService } from "@bitwarden/common/platform/services/state.service"; import { Account } from "../../models/account"; -import { BrowserComponentState } from "../../models/browserComponentState"; -import { BrowserSendComponentState } from "../../models/browserSendComponentState"; import { BrowserApi } from "../browser/browser-api"; import { browserSession, sessionSync } from "../decorators/session-sync-observable"; -import { BrowserStateService as StateServiceAbstraction } from "./abstractions/browser-state.service"; +import { BrowserStateService } from "./abstractions/browser-state.service"; @browserSession -export class BrowserStateService +export class DefaultBrowserStateService extends BaseStateService<GlobalState, Account> - implements StateServiceAbstraction + implements BrowserStateService { @sessionSync({ initializer: Account.fromJSON as any, // TODO: Remove this any when all any types are removed from Account @@ -115,46 +113,6 @@ export class BrowserStateService ); } - async getBrowserSendComponentState(options?: StorageOptions): Promise<BrowserSendComponentState> { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) - )?.send; - } - - async setBrowserSendComponentState( - value: BrowserSendComponentState, - options?: StorageOptions, - ): Promise<void> { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - account.send = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - } - - async getBrowserSendTypeComponentState(options?: StorageOptions): Promise<BrowserComponentState> { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) - )?.sendType; - } - - async setBrowserSendTypeComponentState( - value: BrowserComponentState, - options?: StorageOptions, - ): Promise<void> { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - account.sendType = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - } - // Overriding the base class to prevent deleting the cache on save. We register a storage listener // to delete the cache in the constructor above. protected override async saveAccountToDisk( diff --git a/apps/browser/src/popup/app.component.ts b/apps/browser/src/popup/app.component.ts index e0d898481b..03ac1612f1 100644 --- a/apps/browser/src/popup/app.component.ts +++ b/apps/browser/src/popup/app.component.ts @@ -12,6 +12,7 @@ import { BrowserApi } from "../platform/browser/browser-api"; import { ZonedMessageListenerService } from "../platform/browser/zoned-message-listener.service"; import { BrowserStateService } from "../platform/services/abstractions/browser-state.service"; import { ForegroundPlatformUtilsService } from "../platform/services/platform-utils/foreground-platform-utils.service"; +import { BrowserSendStateService } from "../tools/popup/services/browser-send-state.service"; import { VaultBrowserStateService } from "../vault/services/vault-browser-state.service"; import { routerTransition } from "./app-routing.animations"; @@ -38,6 +39,7 @@ export class AppComponent implements OnInit, OnDestroy { private i18nService: I18nService, private router: Router, private stateService: BrowserStateService, + private browserSendStateService: BrowserSendStateService, private vaultBrowserStateService: VaultBrowserStateService, private changeDetectorRef: ChangeDetectorRef, private ngZone: NgZone, @@ -231,8 +233,8 @@ export class AppComponent implements OnInit, OnDestroy { await Promise.all([ this.vaultBrowserStateService.setBrowserGroupingsComponentState(null), this.vaultBrowserStateService.setBrowserVaultItemsComponentState(null), - this.stateService.setBrowserSendComponentState(null), - this.stateService.setBrowserSendTypeComponentState(null), + this.browserSendStateService.setBrowserSendComponentState(null), + this.browserSendStateService.setBrowserSendTypeComponentState(null), ]); } } diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index e68873e31c..037246d3c4 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -99,11 +99,12 @@ import { BrowserEnvironmentService } from "../../platform/services/browser-envir import BrowserLocalStorageService from "../../platform/services/browser-local-storage.service"; import BrowserMessagingPrivateModePopupService from "../../platform/services/browser-messaging-private-mode-popup.service"; import BrowserMessagingService from "../../platform/services/browser-messaging.service"; -import { BrowserStateService } from "../../platform/services/browser-state.service"; +import { DefaultBrowserStateService } from "../../platform/services/default-browser-state.service"; import I18nService from "../../platform/services/i18n.service"; import { ForegroundPlatformUtilsService } from "../../platform/services/platform-utils/foreground-platform-utils.service"; import { ForegroundDerivedStateProvider } from "../../platform/state/foreground-derived-state.provider"; import { ForegroundMemoryStorageService } from "../../platform/storage/foreground-memory-storage.service"; +import { BrowserSendStateService } from "../../tools/popup/services/browser-send-state.service"; import { FilePopoutUtilsService } from "../../tools/popup/services/file-popout-utils.service"; import { VaultBrowserStateService } from "../../vault/services/vault-browser-state.service"; import { VaultFilterService } from "../../vault/services/vault-filter.service"; @@ -412,7 +413,7 @@ const safeProviders: SafeProvider[] = [ tokenService: TokenService, migrationRunner: MigrationRunner, ) => { - return new BrowserStateService( + return new DefaultBrowserStateService( storageService, secureStorageService, memoryStorageService, @@ -487,6 +488,11 @@ const safeProviders: SafeProvider[] = [ useClass: UserNotificationSettingsService, deps: [StateProvider], }), + safeProvider({ + provide: BrowserSendStateService, + useClass: BrowserSendStateService, + deps: [StateProvider], + }), ]; @NgModule({ 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..9b3ecc7163 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"; @@ -43,7 +43,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, 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..3dafc0934a --- /dev/null +++ b/apps/browser/src/tools/popup/services/browser-send-state.service.spec.ts @@ -0,0 +1,61 @@ +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 { SendType } from "@bitwarden/common/tools/send/enums/send-type"; +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"; + state.typeCounts = new Map<SendType, number>().set(SendType.File, 1); + + 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..b814ee5bc9 --- /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<BrowserSendComponentState>; + + /** Observable that contains the current state for active user Sends that only includes the search text + * and scroll position + */ + browserSendTypeComponentState$: Observable<BrowserComponentState>; + + private activeUserBrowserSendComponentState: ActiveUserState<BrowserSendComponentState>; + private activeUserBrowserSendTypeComponentState: ActiveUserState<BrowserComponentState>; + + 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<BrowserSendComponentState> { + return await firstValueFrom(this.browserSendComponentState$); + } + + /** Set the active user's browser send component state + * @param { BrowserSendComponentState } value sets the sends and type counts along with the scroll position and search text for + * the send component on the browser + */ + async setBrowserSendComponentState(value: BrowserSendComponentState): Promise<void> { + 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<BrowserComponentState> { + 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<void> { + 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..3ba574efa3 --- /dev/null +++ b/apps/browser/src/tools/popup/services/key-definitions.spec.ts @@ -0,0 +1,40 @@ +import { Jsonify } from "type-fest"; + +import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; + +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 = { + typeCounts: new Map<SendType, number>(), + }; + + const result = keyDef.deserializer( + JSON.parse(JSON.stringify(expectedState)) as Jsonify<BrowserSendComponentState>, + ); + + 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<BrowserSendComponentState>( + BROWSER_SEND_MEMORY, + "browser_send_component", + { + deserializer: (obj: Jsonify<BrowserSendComponentState>) => + BrowserSendComponentState.fromJSON(obj), + }, +); + +export const BROWSER_SEND_TYPE_COMPONENT = new KeyDefinition<BrowserComponentState>( + BROWSER_SEND_MEMORY, + "browser_send_type_component", + { + deserializer: (obj: Jsonify<BrowserComponentState>) => BrowserComponentState.fromJSON(obj), + }, +); diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index 979321c1e3..d9265cf10c 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -102,6 +102,7 @@ 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", From 5f1a3acc1768f838a5d18eca3a2505a4ef0cc37e Mon Sep 17 00:00:00 2001 From: Bitwarden DevOps <106330231+bitwarden-devops-bot@users.noreply.github.com> Date: Mon, 8 Apr 2024 11:26:10 -0400 Subject: [PATCH 132/351] Bumped web version to (#8642) --- apps/web/package.json | 2 +- package-lock.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/package.json b/apps/web/package.json index 5b049dcb9d..dbb8337a83 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/web-vault", - "version": "2024.3.1", + "version": "2024.4.0", "scripts": { "build:oss": "webpack", "build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js", diff --git a/package-lock.json b/package-lock.json index 4ec09e2b7c..540bc145e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -247,7 +247,7 @@ }, "apps/web": { "name": "@bitwarden/web-vault", - "version": "2024.3.1" + "version": "2024.4.0" }, "libs/admin-console": { "name": "@bitwarden/admin-console", From 5c30be2927619e71ab2fa932d160fdb17ac967a4 Mon Sep 17 00:00:00 2001 From: Oscar Hinton <Hinton@users.noreply.github.com> Date: Mon, 8 Apr 2024 17:38:24 +0200 Subject: [PATCH 133/351] Increase max-old-space for bit:dev:watch (#8628) --- apps/web/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/package.json b/apps/web/package.json index dbb8337a83..99828bb543 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -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", From d1a0a20daa7551523abf8a479fae51a44fabd03d Mon Sep 17 00:00:00 2001 From: Matt Gibson <mgibson@bitwarden.com> Date: Mon, 8 Apr 2024 10:41:45 -0500 Subject: [PATCH 134/351] [PM-7341] Force serialization of data in chrome storage api (#8621) * Force serialization of data in chrome storage api * Test chrome api storage serialization * Update apps/browser/src/platform/services/abstractions/abstract-chrome-storage-api.service.ts Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com> --------- Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com> --- .../abstract-chrome-storage-api.service.ts | 32 ++++-- .../chrome-storage-api.service.spec.ts | 99 +++++++++++++++++++ 2 files changed, 122 insertions(+), 9 deletions(-) create mode 100644 apps/browser/src/platform/services/abstractions/chrome-storage-api.service.spec.ts diff --git a/apps/browser/src/platform/services/abstractions/abstract-chrome-storage-api.service.ts b/apps/browser/src/platform/services/abstractions/abstract-chrome-storage-api.service.ts index 22ce8d4564..a5681d65c0 100644 --- a/apps/browser/src/platform/services/abstractions/abstract-chrome-storage-api.service.ts +++ b/apps/browser/src/platform/services/abstractions/abstract-chrome-storage-api.service.ts @@ -8,6 +8,23 @@ import { import { fromChromeEvent } from "../../browser/from-chrome-event"; +export const serializationIndicator = "__json__"; + +export const objToStore = (obj: any) => { + if (obj == null) { + return null; + } + + if (obj instanceof Set) { + obj = Array.from(obj); + } + + return { + [serializationIndicator]: true, + value: JSON.stringify(obj), + }; +}; + export default abstract class AbstractChromeStorageService implements AbstractStorageService, ObservableStorageService { @@ -44,7 +61,11 @@ export default abstract class AbstractChromeStorageService return new Promise((resolve) => { this.chromeStorageApi.get(key, (obj: any) => { if (obj != null && obj[key] != null) { - resolve(obj[key] as T); + let value = obj[key]; + if (value[serializationIndicator] && typeof value.value === "string") { + value = JSON.parse(value.value); + } + resolve(value as T); return; } resolve(null); @@ -57,14 +78,7 @@ export default abstract class AbstractChromeStorageService } async save(key: string, obj: any): Promise<void> { - if (obj == null) { - // Fix safari not liking null in set - return this.remove(key); - } - - if (obj instanceof Set) { - obj = Array.from(obj); - } + obj = objToStore(obj); const keyedObj = { [key]: obj }; return new Promise<void>((resolve) => { diff --git a/apps/browser/src/platform/services/abstractions/chrome-storage-api.service.spec.ts b/apps/browser/src/platform/services/abstractions/chrome-storage-api.service.spec.ts new file mode 100644 index 0000000000..bb89dc8a6a --- /dev/null +++ b/apps/browser/src/platform/services/abstractions/chrome-storage-api.service.spec.ts @@ -0,0 +1,99 @@ +import AbstractChromeStorageService, { + objToStore, + serializationIndicator, +} from "./abstract-chrome-storage-api.service"; + +class TestChromeStorageApiService extends AbstractChromeStorageService {} + +describe("objectToStore", () => { + it("converts an object to a tagged string", () => { + const obj = { key: "value" }; + const result = objToStore(obj); + expect(result).toEqual({ + [serializationIndicator]: true, + value: JSON.stringify(obj), + }); + }); + + it("converts a set to an array prior to serialization", () => { + const obj = new Set(["value"]); + const result = objToStore(obj); + expect(result).toEqual({ + [serializationIndicator]: true, + value: JSON.stringify(Array.from(obj)), + }); + }); + + it("does nothing to null", () => { + expect(objToStore(null)).toEqual(null); + }); +}); + +describe("ChromeStorageApiService", () => { + let service: TestChromeStorageApiService; + let store: Record<any, any>; + + beforeEach(() => { + store = {}; + + service = new TestChromeStorageApiService(chrome.storage.local); + }); + + describe("save", () => { + let setMock: jest.Mock; + + beforeEach(() => { + // setup save + setMock = chrome.storage.local.set as jest.Mock; + setMock.mockImplementation((data, callback) => { + Object.assign(store, data); + callback(); + }); + }); + + it("uses `objToStore` to prepare a value for set", async () => { + const key = "key"; + const value = { key: "value" }; + await service.save(key, value); + expect(setMock).toHaveBeenCalledWith( + { + [key]: objToStore(value), + }, + expect.any(Function), + ); + }); + }); + + describe("get", () => { + let getMock: jest.Mock; + + beforeEach(() => { + // setup get + getMock = chrome.storage.local.get as jest.Mock; + getMock.mockImplementation((key, callback) => { + callback({ [key]: store[key] }); + }); + }); + + it("returns a stored value when it is serialized", async () => { + const key = "key"; + const value = { key: "value" }; + store[key] = objToStore(value); + const result = await service.get(key); + expect(result).toEqual(value); + }); + + it("returns a stored value when it is not serialized", async () => { + const key = "key"; + const value = "value"; + store[key] = value; + const result = await service.get(key); + expect(result).toEqual(value); + }); + + it("returns null when the key does not exist", async () => { + const result = await service.get("key"); + expect(result).toBeNull(); + }); + }); +}); From 7064b595da33bfcf1b943310791f6f9ff325d4bb Mon Sep 17 00:00:00 2001 From: Oscar Hinton <Hinton@users.noreply.github.com> Date: Mon, 8 Apr 2024 17:46:24 +0200 Subject: [PATCH 135/351] [SM-1031] Remove SecretsManager & showDDG compile flags (#8610) Remove old compile flags which should no longer be required, and may even cause issues. secretsManager: false hides the app switcher which is now used for more than just secrets manager. --- .storybook/main.ts | 4 +--- apps/browser/src/platform/flags.ts | 4 ++-- apps/cli/src/platform/flags.ts | 4 ++-- apps/desktop/config/development.json | 1 - apps/desktop/config/production.json | 1 - .../src/app/accounts/settings.component.ts | 3 +-- apps/desktop/src/platform/flags.ts | 8 +++----- apps/web/config/base.json | 1 - apps/web/config/cloud.json | 1 - apps/web/config/development.json | 1 - apps/web/config/euprd.json | 1 - apps/web/config/euqa.json | 1 - apps/web/config/qa.json | 1 - apps/web/config/selfhosted.json | 1 - apps/web/config/usdev.json | 1 - .../organizations/members/people.component.ts | 5 +---- .../product-switcher.component.html | 18 ++++++++---------- .../product-switcher.component.ts | 5 ----- apps/web/src/utils/flags.ts | 5 ++--- .../app/secrets-manager/sm-routing.module.ts | 5 ++--- libs/common/src/platform/misc/flags.ts | 4 ++-- 21 files changed, 24 insertions(+), 51 deletions(-) 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/src/platform/flags.ts b/apps/browser/src/platform/flags.ts index 71a20edc5e..36aa698a7b 100644 --- a/apps/browser/src/platform/flags.ts +++ b/apps/browser/src/platform/flags.ts @@ -11,13 +11,13 @@ import { GroupPolicyEnvironment } from "../admin-console/types/group-policy-envi import { BrowserApi } from "./browser/browser-api"; // 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 = { accountSwitching?: boolean; } & 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 = { storeSessionDecrypted?: boolean; managedEnvironment?: GroupPolicyEnvironment; 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/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/src/app/accounts/settings.component.ts b/apps/desktop/src/app/accounts/settings.component.ts index a613328878..07fcc5d3b8 100644 --- a/apps/desktop/src/app/accounts/settings.component.ts +++ b/apps/desktop/src/app/accounts/settings.component.ts @@ -24,7 +24,6 @@ import { DialogService } from "@bitwarden/components"; import { SetPinComponent } from "../../auth/components/set-pin.component"; import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service"; -import { flagEnabled } from "../../platform/flags"; import { DesktopSettingsService } from "../../platform/services/desktop-settings.service"; @Component({ @@ -146,7 +145,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 }, 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/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/src/app/admin-console/organizations/members/people.component.ts b/apps/web/src/app/admin-console/organizations/members/people.component.ts index 8a303ddfe5..0da0ab79f0 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 @@ -52,7 +52,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"; @@ -148,9 +147,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) => { 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 @@ -<ng-template [ngIf]="isEnabled"> - <button - type="button" - bitIconButton="bwi bwi-fw bwi-filter" - [bitMenuTriggerFor]="content?.menu" - [buttonType]="buttonType" - [attr.aria-label]="'switchProducts' | i18n" - ></button> - <product-switcher-content #content></product-switcher-content> -</ng-template> +<button + type="button" + bitIconButton="bwi bwi-fw bwi-filter" + [bitMenuTriggerFor]="content?.menu" + [buttonType]="buttonType" + [attr.aria-label]="'switchProducts' | i18n" +></button> +<product-switcher-content #content></product-switcher-content> 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/utils/flags.ts b/apps/web/src/utils/flags.ts index 902159147e..9d3c25d5cc 100644 --- a/apps/web/src/utils/flags.ts +++ b/apps/web/src/utils/flags.ts @@ -7,14 +7,13 @@ 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 = { - secretsManager?: boolean; showPasswordless?: boolean; } & 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/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 f9ddcdad78..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: [ { @@ -86,7 +85,7 @@ const routes: Routes = [ ], }, ], - }), + }, ]; @NgModule({ 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; }; From 0c291bf79b1b4007bd3a5a2204cf3679b848395e Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Mon, 8 Apr 2024 13:24:27 -0500 Subject: [PATCH 136/351] [AC-2086] Limit admin access - Collection Modal (#8335) * feat: add view collection string, update button text, refs AC-2086 * feat: remove canEdit from Restricted Collection Access component, refs AC-2086 * feat: add view collection clicked flow, refs AC-2086 * fix: revert accidental svg icon changes, refs AC-2086 * feat: add input for access selector to hide multi select, refs AC-2086 * feat: apply readonly/disabled changes to access dialog, refs AC-2086 * fix: messages file conflict, refs AC-2086 * feat: apply disabled state to access selector, refs AC-2086 * fix: formatting, refs AC-2086 * fix: permission mode read only relocate, refs AC-2086 * fix: conform readonly casing, refs AC-2086 --- .../access-selector.component.html | 6 +- .../access-selector.component.ts | 8 ++ .../collection-dialog.component.html | 39 +++++--- .../collection-dialog.component.ts | 93 +++++++++++++++++-- .../collection-access-restricted.component.ts | 11 +-- .../app/vault/org-vault/vault.component.html | 6 +- .../app/vault/org-vault/vault.component.ts | 13 ++- apps/web/src/locales/en/messages.json | 6 ++ 8 files changed, 145 insertions(+), 37 deletions(-) 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 @@ </select> </bit-form-field> - <bit-form-field class="tw-grow"> + <bit-form-field class="tw-grow" *ngIf="!disabled"> <bit-label>{{ selectorLabelText }}</bit-label> <bit-multi-select class="tw-w-full" @@ -120,7 +120,7 @@ </div> <div - *ngIf="item.readonly" + *ngIf="item.readonly || disabled" class="tw-max-w-40 tw-overflow-hidden tw-overflow-ellipsis tw-whitespace-nowrap tw-font-bold tw-text-muted" [title]="permissionLabelId(item.readonlyPermission) | i18n" > @@ -139,7 +139,7 @@ <td bitCell class="tw-text-right"> <button - *ngIf="!item.readonly" + *ngIf="!disabled && !item.readonly" type="button" bitIconButton="bwi-close" buttonType="muted" diff --git a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.ts b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.ts index 0c87f87752..51f5db1ca7 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.ts +++ b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.ts @@ -121,6 +121,13 @@ export class AccessSelectorComponent implements ControlValueAccessor, OnInit, On protected permissionList: Permission[]; protected initialPermission = CollectionPermission.View; + /** + * When disabled, the access selector will make the assumption that a readonly state is desired. + * The PermissionMode will be set to Readonly + * The Multi-Select control will be hidden + * The delete action on each row item will be hidden + * The readonly permission label/property needs to configured on the access item views being passed into the component + */ disabled: boolean; /** @@ -225,6 +232,7 @@ export class AccessSelectorComponent implements ControlValueAccessor, OnInit, On // Keep the internal FormGroup in sync if (this.disabled) { + this.permissionMode = PermissionMode.Readonly; this.formGroup.disable(); } else { this.formGroup.enable(); 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 @@ </bit-tab> <bit-tab label="{{ 'access' | i18n }}"> <div class="tw-mb-3" *ngIf="organization.flexibleCollections"> - <span *ngIf="organization.useGroups">{{ "grantCollectionAccess" | i18n }}</span> - <span *ngIf="!organization.useGroups">{{ - "grantCollectionAccessMembersOnly" | i18n - }}</span> - <span - *ngIf=" - (flexibleCollectionsV1Enabled$ | async) && - organization.allowAdminAccessToAllCollectionItems - " - >{{ " " + ("adminCollectionAccess" | i18n) }}</span - > + <ng-container *ngIf="dialogReadonly"> + <span>{{ "readOnlyCollectionAccess" | i18n }}</span> + </ng-container> + <ng-container *ngIf="!dialogReadonly"> + <span *ngIf="organization.useGroups">{{ "grantCollectionAccess" | i18n }}</span> + <span *ngIf="!organization.useGroups">{{ + "grantCollectionAccessMembersOnly" | i18n + }}</span> + <span + *ngIf=" + (flexibleCollectionsV1Enabled$ | async) && + organization.allowAdminAccessToAllCollectionItems + " + >{{ " " + ("adminCollectionAccess" | i18n) }}</span + > + </ng-container> </div> <div class="tw-mb-3 tw-text-danger" @@ -85,7 +90,7 @@ </div> <bit-access-selector *ngIf="organization.useGroups" - [permissionMode]="PermissionMode.Edit" + [permissionMode]="dialogReadonly ? PermissionMode.Readonly : PermissionMode.Edit" formControlName="access" [items]="accessItems" [columnHeader]="'groupSlashMemberColumnHeader' | i18n" @@ -96,7 +101,7 @@ ></bit-access-selector> <bit-access-selector *ngIf="!organization.useGroups" - [permissionMode]="PermissionMode.Edit" + [permissionMode]="dialogReadonly ? PermissionMode.Readonly : PermissionMode.Edit" formControlName="access" [items]="accessItems" [columnHeader]="'memberColumnHeader' | i18n" @@ -108,7 +113,13 @@ </bit-tab-group> </div> <ng-container bitDialogFooter> - <button type="submit" bitButton bitFormButton buttonType="primary" [disabled]="loading"> + <button + type="submit" + bitButton + bitFormButton + buttonType="primary" + [disabled]="loading || dialogReadonly" + > {{ "save" | i18n }} </button> <button diff --git a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts index 722ab972fc..64150245cb 100644 --- a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts +++ b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts @@ -16,6 +16,7 @@ import { first } from "rxjs/operators"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; +import { OrganizationUserUserDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-user/responses/organization-user.response"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -27,7 +28,11 @@ import { CollectionResponse } from "@bitwarden/common/vault/models/response/coll import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; import { BitValidators, DialogService } from "@bitwarden/components"; -import { GroupService, GroupView } from "../../../admin-console/organizations/core"; +import { + GroupService, + GroupView, + CollectionAccessSelectionView, +} from "../../../admin-console/organizations/core"; import { PermissionMode } from "../../../admin-console/organizations/shared/components/access-selector/access-selector.component"; import { AccessItemType, @@ -36,8 +41,6 @@ import { CollectionPermission, convertToPermission, convertToSelectionView, - mapGroupToAccessItemView, - mapUserToAccessItemView, } from "../../../admin-console/organizations/shared/components/access-selector/access-selector.models"; import { CollectionAdminService } from "../../core/collection-admin.service"; import { CollectionAdminView } from "../../core/views/collection-admin.view"; @@ -54,6 +57,7 @@ export interface CollectionDialogParams { parentCollectionId?: string; showOrgSelector?: boolean; collectionIds?: string[]; + readonly?: boolean; } export interface CollectionDialogResult { @@ -158,7 +162,8 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { ? from(this.collectionAdminService.get(orgId, this.params.collectionId)) : of(null), groups: groups$, - users: this.organizationUserService.getAllUsers(orgId), + // Collection(s) needed to map readonlypermission for (potential) access selector disabled state + users: this.organizationUserService.getAllUsers(orgId, { includeCollections: true }), collection: this.params.collectionId ? this.collectionService.get(this.params.collectionId) : of(null), @@ -177,8 +182,8 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { }) => { this.organization = organization; this.accessItems = [].concat( - groups.map(mapGroupToAccessItemView), - users.data.map(mapUserToAccessItemView), + groups.map((group) => mapGroupToAccessItemView(group, this.collectionId)), + users.data.map((user) => mapUserToAccessItemView(user, this.collectionId)), ); // Force change detection to update the access selector's items @@ -209,7 +214,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { access: accessSelections, }); this.collection.manage = collection?.manage ?? false; // Get manage flag from sync data collection - this.showDeleteButton = this.collection.canDelete(organization); + this.showDeleteButton = !this.dialogReadonly && this.collection.canDelete(organization); } else { this.nestOptions = collections; const parent = collections.find((c) => c.id === this.params.parentCollectionId); @@ -244,6 +249,8 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { } this.formGroup.controls.access.updateValueAndValidity(); + this.handleFormGroupReadonly(this.dialogReadonly); + this.loading = false; }, ); @@ -257,11 +264,20 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { return this.params.collectionId != undefined; } + protected get dialogReadonly() { + return this.params.readonly === true; + } + protected async cancel() { this.close(CollectionDialogAction.Canceled); } protected submit = async () => { + // Saving a collection is prohibited while in read only mode + if (this.dialogReadonly) { + return; + } + this.formGroup.markAllAsTouched(); if (this.formGroup.invalid) { @@ -316,6 +332,11 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { }; protected delete = async () => { + // Deleting a collection is prohibited while in read only mode + if (this.dialogReadonly) { + return; + } + const confirmed = await this.dialogService.openSimpleDialog({ title: this.collection?.name, content: { key: "deleteCollectionConfirmation" }, @@ -342,6 +363,20 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { this.destroy$.complete(); } + private handleFormGroupReadonly(readonly: boolean) { + if (readonly) { + this.formGroup.controls.name.disable(); + this.formGroup.controls.externalId.disable(); + this.formGroup.controls.parent.disable(); + this.formGroup.controls.access.disable(); + } else { + this.formGroup.controls.name.enable(); + this.formGroup.controls.externalId.enable(); + this.formGroup.controls.parent.enable(); + this.formGroup.controls.access.enable(); + } + } + private close(action: CollectionDialogAction, collection?: CollectionResponse | CollectionView) { this.dialogRef.close({ action, collection } as CollectionDialogResult); } @@ -383,6 +418,50 @@ function validateCanManagePermission(control: AbstractControl) { return hasManagePermission ? null : { managePermissionRequired: true }; } +/** + * + * @param group Current group being used to translate object into AccessItemView + * @param collectionId Current collection being viewed/edited + * @returns AccessItemView customized to set a readonlyPermission to be displayed if the access selector is in a disabled state + */ +function mapGroupToAccessItemView(group: GroupView, collectionId: string): AccessItemView { + return { + id: group.id, + type: AccessItemType.Group, + listName: group.name, + labelName: group.name, + accessAllItems: group.accessAll, + readonly: group.accessAll, + readonlyPermission: convertToPermission(group.collections.find((gc) => gc.id == collectionId)), + }; +} + +/** + * + * @param user Current user being used to translate object into AccessItemView + * @param collectionId Current collection being viewed/edited + * @returns AccessItemView customized to set a readonlyPermission to be displayed if the access selector is in a disabled state + */ +function mapUserToAccessItemView( + user: OrganizationUserUserDetailsResponse, + collectionId: string, +): AccessItemView { + return { + id: user.id, + type: AccessItemType.Member, + email: user.email, + role: user.type, + listName: user.name?.length > 0 ? `${user.name} (${user.email})` : user.email, + labelName: user.name ?? user.email, + status: user.status, + accessAllItems: user.accessAll, + readonly: user.accessAll, + readonlyPermission: convertToPermission( + new CollectionAccessSelectionView(user.collections.find((uc) => uc.id == collectionId)), + ), + }; +} + /** * Strongly typed helper to open a CollectionDialog * @param dialogService Instance of the dialog service that will be used to open the dialog diff --git a/apps/web/src/app/vault/org-vault/collection-access-restricted.component.ts b/apps/web/src/app/vault/org-vault/collection-access-restricted.component.ts index 66018c7375..337d73b315 100644 --- a/apps/web/src/app/vault/org-vault/collection-access-restricted.component.ts +++ b/apps/web/src/app/vault/org-vault/collection-access-restricted.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, Input, Output } from "@angular/core"; +import { Component, EventEmitter, Output } from "@angular/core"; import { ButtonModule, NoItemsModule, svgIcon } from "@bitwarden/components"; @@ -16,21 +16,18 @@ const icon = svgIcon`<svg xmlns="http://www.w3.org/2000/svg" width="120" height= template: `<bit-no-items [icon]="icon" class="tw-mt-2 tw-block"> <span slot="title" class="tw-mt-4 tw-block">{{ "collectionAccessRestricted" | i18n }}</span> <button - *ngIf="canEdit" slot="button" bitButton - (click)="editInfoClicked.emit()" + (click)="viewCollectionClicked.emit()" buttonType="secondary" type="button" > - <i aria-hidden="true" class="bwi bwi-pencil-square"></i> {{ "editInfo" | i18n }} + <i aria-hidden="true" class="bwi bwi-pencil-square"></i> {{ "viewCollection" | i18n }} </button> </bit-no-items>`, }) export class CollectionAccessRestrictedComponent { protected icon = icon; - @Input() canEdit = false; - - @Output() editInfoClicked = new EventEmitter<void>(); + @Output() viewCollectionClicked = new EventEmitter<void>(); } diff --git a/apps/web/src/app/vault/org-vault/vault.component.html b/apps/web/src/app/vault/org-vault/vault.component.html index 391f412f1e..bcbd56630c 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.html +++ b/apps/web/src/app/vault/org-vault/vault.component.html @@ -99,11 +99,9 @@ </bit-no-items> <collection-access-restricted *ngIf="showCollectionAccessRestricted" - [canEdit]=" - selectedCollection != null && - selectedCollection.node.canEdit(organization, flexibleCollectionsV1Enabled) + (viewCollectionClicked)=" + editCollection(selectedCollection.node, CollectionDialogTabType.Info, true) " - (editInfoClicked)="editCollection(selectedCollection.node, CollectionDialogTabType.Info)" > </collection-access-restricted> </ng-container> diff --git a/apps/web/src/app/vault/org-vault/vault.component.ts b/apps/web/src/app/vault/org-vault/vault.component.ts index d7cc70c583..cb01951fcc 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.ts +++ b/apps/web/src/app/vault/org-vault/vault.component.ts @@ -1058,9 +1058,18 @@ export class VaultComponent implements OnInit, OnDestroy { } } - async editCollection(c: CollectionView, tab: CollectionDialogTabType): Promise<void> { + async editCollection( + c: CollectionView, + tab: CollectionDialogTabType, + readonly: boolean = false, + ): Promise<void> { const dialog = openCollectionDialog(this.dialogService, { - data: { collectionId: c?.id, organizationId: this.organization?.id, initialTab: tab }, + data: { + collectionId: c?.id, + organizationId: this.organization?.id, + initialTab: tab, + readonly: readonly, + }, }); const result = await lastValueFrom(dialog.closed); diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 1604057f70..307c5be70c 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -7501,6 +7501,9 @@ "collectionAccessRestricted": { "message": "Collection access is restricted" }, + "readOnlyCollectionAccess": { + "message": "You do not have access to manage this collection." + }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." }, @@ -7603,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "viewCollection": { + "message": "View collection" + }, "restrictedGroupAccess": { "message": "You cannot add yourself to groups." }, From 18ae698f8d562f4cc2ead7c438894969ebbb5a2a Mon Sep 17 00:00:00 2001 From: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com> Date: Mon, 8 Apr 2024 14:42:49 -0400 Subject: [PATCH 137/351] SM changes (#8531) --- .../organizations/organization-plans.component.ts | 10 +++++++--- .../organization-subscription-cloud.component.ts | 3 +++ .../providers/clients/clients.component.ts | 1 + libs/common/src/billing/enums/plan-type.enum.ts | 15 ++++++++++----- .../services/organization-billing.service.ts | 1 + 5 files changed, 22 insertions(+), 8 deletions(-) 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..17eddbd33d 100644 --- a/apps/web/src/app/billing/organizations/organization-plans.component.ts +++ b/apps/web/src/app/billing/organizations/organization-plans.component.ts @@ -47,7 +47,11 @@ interface OnSuccessArgs { organizationId: string; } -const Allowed2020PlanTypes = [ +const AllowedLegacyPlanTypes = [ + PlanType.TeamsMonthly2023, + PlanType.TeamsAnnually2023, + PlanType.EnterpriseAnnually2023, + PlanType.EnterpriseMonthly2023, PlanType.TeamsMonthly2020, PlanType.TeamsAnnually2020, PlanType.EnterpriseAnnually2020, @@ -278,7 +282,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 +297,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); 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 0810f79b8e..2173d4c0ca 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 @@ -241,6 +241,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 || @@ -254,6 +256,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) { diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts index 20e98ce084..72cce7aac3 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts @@ -29,6 +29,7 @@ const DisallowedPlanTypes = [ PlanType.Free, PlanType.FamiliesAnnually2019, PlanType.FamiliesAnnually, + PlanType.TeamsStarter2023, PlanType.TeamsStarter, ]; 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/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: From c73372310b7688881d4cc521d705df58f4e7a3e3 Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Mon, 8 Apr 2024 14:32:14 -0500 Subject: [PATCH 138/351] fix: remove TXT generation, separate save/verify steps, refs AC-2350 (#8540) --- .../domain-add-edit-dialog.component.html | 9 ++++++--- .../domain-add-edit-dialog.component.ts | 17 +++-------------- .../requests/organization-domain.request.ts | 4 +--- 3 files changed, 10 insertions(+), 20 deletions(-) diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.html b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.html index fcbfcb74e6..0d0ca04f92 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.html @@ -26,7 +26,7 @@ <bit-hint>{{ "domainNameInputHint" | i18n }}</bit-hint> </bit-form-field> - <bit-form-field> + <bit-form-field *ngIf="data?.orgDomain"> <bit-label>{{ "dnsTxtRecord" | i18n }}</bit-label> <input bitInput formControlName="txt" /> <bit-hint>{{ "dnsTxtRecordInputHint" | i18n }}</bit-hint> @@ -42,7 +42,7 @@ </bit-form-field> <bit-callout - *ngIf="!data?.orgDomain?.verifiedDate" + *ngIf="data?.orgDomain && !data?.orgDomain?.verifiedDate" type="info" title="{{ 'automaticDomainVerification' | i18n }}" > @@ -51,7 +51,10 @@ </div> <ng-container bitDialogFooter> <button type="submit" bitButton bitFormButton buttonType="primary"> - <span *ngIf="!data?.orgDomain?.verifiedDate">{{ "verifyDomain" | i18n }}</span> + <span *ngIf="!data?.orgDomain">{{ "next" | i18n }}</span> + <span *ngIf="data?.orgDomain && !data?.orgDomain?.verifiedDate">{{ + "verifyDomain" | i18n + }}</span> <span *ngIf="data?.orgDomain?.verifiedDate">{{ "reverifyDomain" | i18n }}</span> </button> <button bitButton buttonType="secondary" (click)="dialogRef.close()" type="button"> diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.ts b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.ts index 3fff5ad97d..52e46915e9 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component.ts @@ -13,7 +13,6 @@ import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitw 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 { Utils } from "@bitwarden/common/platform/misc/utils"; import { DialogService } from "@bitwarden/components"; import { domainNameValidator } from "./validators/domain-name.validator"; @@ -90,17 +89,6 @@ export class DomainAddEditDialogComponent implements OnInit, OnDestroy { // Edit this.domainForm.patchValue(this.data.orgDomain); this.domainForm.disable(); - } else { - // Add - - // Figuring out the proper length of our DNS TXT Record value was fun. - // DNS-Based Service Discovery RFC: https://www.ietf.org/rfc/rfc6763.txt; see section 6.1 - // Google uses 43 chars for their TXT record value: https://support.google.com/a/answer/2716802 - // So, chose a magic # of 33 bytes to achieve at least that once converted to base 64 (47 char length). - const generatedTxt = `bw=${Utils.fromBufferToB64( - await this.cryptoFunctionService.randomBytes(33), - )}`; - this.txtCtrl.setValue(generatedTxt); } this.setupFormListeners(); @@ -121,6 +109,7 @@ export class DomainAddEditDialogComponent implements OnInit, OnDestroy { // End Form methods // Async Form Actions + // Creates a new domain record. The DNS TXT Record will be generated server-side and returned in the response. saveDomain = async (): Promise<void> => { if (this.domainForm.invalid) { this.platformUtilsService.showToast("error", null, this.i18nService.t("domainFormInvalid")); @@ -130,14 +119,14 @@ export class DomainAddEditDialogComponent implements OnInit, OnDestroy { this.domainNameCtrl.disable(); const request: OrganizationDomainRequest = new OrganizationDomainRequest( - this.txtCtrl.value, this.domainNameCtrl.value, ); try { this.data.orgDomain = await this.orgDomainApiService.post(this.data.organizationId, request); + // Patch the DNS TXT Record that was generated server-side + this.domainForm.controls.txt.patchValue(this.data.orgDomain.txt); this.platformUtilsService.showToast("success", null, this.i18nService.t("domainSaved")); - await this.verifyDomain(); } catch (e) { this.handleDomainSaveError(e); } 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; } } From b05679e9bcea57572a0882f9c55dca08a0581677 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 9 Apr 2024 11:27:36 +1000 Subject: [PATCH 139/351] [deps] AC: Update sass to v1.74.1 (#8476) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 540bc145e4..d19f27e535 100644 --- a/package-lock.json +++ b/package-lock.json @@ -168,7 +168,7 @@ "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", @@ -33467,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", diff --git a/package.json b/package.json index 1c36865cd6..203da2d625 100644 --- a/package.json +++ b/package.json @@ -129,7 +129,7 @@ "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", From 8b00897638f17de7ef92ab8a6401d517043b2e34 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 9 Apr 2024 09:53:46 +0200 Subject: [PATCH 140/351] [deps] Platform: Update Rust crate thiserror to v1.0.58 (#8187) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- apps/desktop/desktop_native/Cargo.lock | 8 ++++---- apps/desktop/desktop_native/Cargo.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 7646b63001..b921cab37b 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -1237,18 +1237,18 @@ dependencies = [ [[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", diff --git a/apps/desktop/desktop_native/Cargo.toml b/apps/desktop/desktop_native/Cargo.toml index a1625020e5..4b2bc2e905 100644 --- a/apps/desktop/desktop_native/Cargo.toml +++ b/apps/desktop/desktop_native/Cargo.toml @@ -24,7 +24,7 @@ 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] From 2145a37fd4b7b6c6eb74b047fb6c1f29ddb012cb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 9 Apr 2024 09:27:55 +0000 Subject: [PATCH 141/351] Autosync the updated translations (#8645) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/desktop/src/locales/fi/messages.json | 4 +- apps/desktop/src/locales/zh_TW/messages.json | 60 ++++++++++---------- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/apps/desktop/src/locales/fi/messages.json b/apps/desktop/src/locales/fi/messages.json index 412e8fc20f..206588f3c3 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" @@ -2689,7 +2689,7 @@ "description": "Label indicating the most common import formats" }, "troubleshooting": { - "message": "Vianmääritys" + "message": "Vianselvitys" }, "disableHardwareAccelerationRestart": { "message": "Poista laitteistokiihdytys käytöstä ja käynnistä sovellus uudelleen" diff --git a/apps/desktop/src/locales/zh_TW/messages.json b/apps/desktop/src/locales/zh_TW/messages.json index d124fc7d58..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,22 +2685,22 @@ "message": "將與您的 LastPass 帳戶關聯的 YubiKey 插入電腦的 USB 連接埠,然後觸摸其按鈕。" }, "commonImportFormats": { - "message": "Common formats", + "message": "常見格式", "description": "Label indicating the most common import formats" }, "troubleshooting": { - "message": "Troubleshooting" + "message": "疑難排解" }, "disableHardwareAccelerationRestart": { - "message": "Disable hardware acceleration and restart" + "message": "停用硬體加速並重新啟動" }, "enableHardwareAccelerationRestart": { - "message": "Enable hardware acceleration and restart" + "message": "啟用硬體加速並重新啟動" }, "removePasskey": { - "message": "Remove passkey" + "message": "移除金鑰" }, "passkeyRemoved": { - "message": "Passkey removed" + "message": "金鑰已移除" } } From aefea43fffe62d64ff2b2bfeef9605297ab54cc3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 9 Apr 2024 09:29:09 +0000 Subject: [PATCH 142/351] Autosync the updated translations (#8646) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/browser/src/_locales/eu/messages.json | 108 ++++++++++----------- apps/browser/src/_locales/fi/messages.json | 4 +- 2 files changed, 56 insertions(+), 56 deletions(-) diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index e574d8e2e7..2d8286307b 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" diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index 591f12421c..13c6d119f9 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)" From c02723d6a6ef1b61644f39babdeeec436632391f Mon Sep 17 00:00:00 2001 From: Matt Gibson <mgibson@bitwarden.com> Date: Tue, 9 Apr 2024 10:17:00 -0500 Subject: [PATCH 143/351] Specify clearOn options for platform services (#8584) * Use UserKeys in biometric state * Remove global clear todo. Answer is never * User UserKeys in crypto state * Clear userkey on both lock and logout via User Key Definitions * Use UserKeyDefinitions in environment service * Rely on userKeyDefinition to clear org keys * Rely on userKeyDefinition to clear provider keys * Rely on userKeyDefinition to clear user keys * Rely on userKeyDefinitions to clear user asym key pair --- libs/common/spec/fake-account-service.ts | 8 +- .../platform/abstractions/crypto.service.ts | 26 +- .../biometrics/biometric.state.spec.ts | 12 +- .../platform/biometrics/biometric.state.ts | 17 +- .../services/config/default-config.service.ts | 1 - .../platform/services/crypto.service.spec.ts | 263 +++--------------- .../src/platform/services/crypto.service.ts | 101 +++---- .../default-environment.service.spec.ts | 7 +- .../services/default-environment.service.ts | 49 +++- .../services/key-state/org-keys.state.ts | 5 +- .../services/key-state/provider-keys.state.ts | 5 +- .../services/key-state/user-key.state.ts | 14 +- .../src/platform/state/derive-definition.ts | 22 +- .../vault-timeout.service.spec.ts | 1 - .../vault-timeout/vault-timeout.service.ts | 3 - 15 files changed, 169 insertions(+), 365 deletions(-) diff --git a/libs/common/spec/fake-account-service.ts b/libs/common/spec/fake-account-service.ts index 2f33d9cf02..bd35d901c2 100644 --- a/libs/common/spec/fake-account-service.ts +++ b/libs/common/spec/fake-account-service.ts @@ -32,12 +32,8 @@ export class FakeAccountService implements AccountService { get activeUserId() { return this._activeUserId; } - get accounts$() { - return this.accountsSubject.asObservable(); - } - get activeAccount$() { - return this.activeAccountSubject.asObservable(); - } + accounts$ = this.accountsSubject.asObservable(); + activeAccount$ = this.activeAccountSubject.asObservable(); accountLock$: Observable<UserId>; accountLogout$: Observable<UserId>; diff --git a/libs/common/src/platform/abstractions/crypto.service.ts b/libs/common/src/platform/abstractions/crypto.service.ts index 85b2bfe82e..ed451fd896 100644 --- a/libs/common/src/platform/abstractions/crypto.service.ts +++ b/libs/common/src/platform/abstractions/crypto.service.ts @@ -26,7 +26,7 @@ export abstract class CryptoService { * 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 */ @@ -93,13 +93,6 @@ export abstract class CryptoService { * @returns A new user key and the master key protected version of it */ abstract 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 - */ - abstract clearUserKey(clearSecretStorage?: boolean, userId?: string): Promise<void>; /** * Clears the user's stored version of the user key * @param keySuffix The desired version of the key to clear @@ -238,12 +231,6 @@ export abstract class CryptoService { abstract makeDataEncKey<T extends UserKey | OrgKey>( 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 - */ - abstract clearOrgKeys(memoryOnly?: boolean, userId?: string): Promise<void>; /** * Stores the encrypted provider keys and clears any decrypted * provider keys currently in memory @@ -260,11 +247,6 @@ export abstract class CryptoService { * @returns A record of the provider Ids to their symmetric keys */ abstract getProviderKeys(): Promise<Record<ProviderId, ProviderKey>>; - /** - * @param memoryOnly Clear only the in-memory keys - * @param userId The desired user - */ - abstract clearProviderKeys(memoryOnly?: boolean, userId?: string): Promise<void>; /** * Returns the public key from memory. If not available, extracts it * from the private key and stores it in memory @@ -304,12 +286,6 @@ export abstract class CryptoService { * @returns A new keypair: [publicKey in Base64, encrypted privateKey] */ abstract 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 - */ - abstract clearKeyPair(memoryOnly?: boolean, userId?: string): Promise<void[]>; /** * @param pin The user's pin * @param salt The user's salt 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>, EncryptedString] | [KeyDefinition<boolean>, boolean] + ...args: + | [UserKeyDefinition<EncryptedString>, EncryptedString] + | [UserKeyDefinition<boolean>, boolean] + | [KeyDefinition<boolean>, boolean] ) => { - function testDeserialization<T>(keyDefinition: KeyDefinition<T>, state: T) { + function testDeserialization<T>( + keyDefinition: UserKeyDefinition<T> | KeyDefinition<T>, + 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<boolean>( +export const BIOMETRIC_UNLOCK_ENABLED = new UserKeyDefinition<boolean>( BIOMETRIC_SETTINGS_DISK, "biometricUnlockEnabled", { deserializer: (obj) => obj, + clearOn: [], }, ); @@ -18,11 +19,12 @@ export const BIOMETRIC_UNLOCK_ENABLED = new KeyDefinition<boolean>( * * A true setting controls whether {@link ENCRYPTED_CLIENT_KEY_HALF} is set. */ -export const REQUIRE_PASSWORD_ON_START = new KeyDefinition<boolean>( +export const REQUIRE_PASSWORD_ON_START = new UserKeyDefinition<boolean>( BIOMETRIC_SETTINGS_DISK, "requirePasswordOnStart", { deserializer: (value) => value, + clearOn: [], }, ); @@ -33,11 +35,12 @@ export const REQUIRE_PASSWORD_ON_START = new KeyDefinition<boolean>( * 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<EncryptedString>( +export const ENCRYPTED_CLIENT_KEY_HALF = new UserKeyDefinition<EncryptedString>( BIOMETRIC_SETTINGS_DISK, "clientKeyHalf", { deserializer: (obj) => obj, + clearOn: ["logout"], }, ); @@ -45,11 +48,12 @@ export const ENCRYPTED_CLIENT_KEY_HALF = new KeyDefinition<EncryptedString>( * 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<boolean>( +export const DISMISSED_REQUIRE_PASSWORD_ON_START_CALLOUT = new UserKeyDefinition<boolean>( BIOMETRIC_SETTINGS_DISK, "dismissedBiometricRequirePasswordOnStartCallout", { deserializer: (obj) => obj, + clearOn: [], }, ); @@ -68,11 +72,12 @@ export const PROMPT_CANCELLED = KeyDefinition.record<boolean, UserId>( /** * Stores whether the user has elected to automatically prompt for biometric unlock on application start. */ -export const PROMPT_AUTOMATICALLY = new KeyDefinition<boolean>( +export const PROMPT_AUTOMATICALLY = new UserKeyDefinition<boolean>( BIOMETRIC_SETTINGS_DISK, "promptAutomatically", { deserializer: (obj) => obj, + clearOn: [], }, ); diff --git a/libs/common/src/platform/services/config/default-config.service.ts b/libs/common/src/platform/services/config/default-config.service.ts index 9532b903d3..e124deccf8 100644 --- a/libs/common/src/platform/services/config/default-config.service.ts +++ b/libs/common/src/platform/services/config/default-config.service.ts @@ -32,7 +32,6 @@ export const USER_SERVER_CONFIG = new UserKeyDefinition<ServerConfig>(CONFIG_DIS clearOn: ["logout"], }); -// TODO MDG: When to clean these up? export const GLOBAL_SERVER_CONFIGURATIONS = KeyDefinition.record<ServerConfig, ApiUrl>( CONFIG_DISK, "byServer", diff --git a/libs/common/src/platform/services/crypto.service.spec.ts b/libs/common/src/platform/services/crypto.service.spec.ts index 9160664aa5..c17d3f97d2 100644 --- a/libs/common/src/platform/services/crypto.service.spec.ts +++ b/libs/common/src/platform/services/crypto.service.spec.ts @@ -1,5 +1,5 @@ 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"; @@ -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"; @@ -336,231 +337,22 @@ describe("cryptoService", () => { }); }); - describe("clearUserKey", () => { - it.each([mockUserId, null])("should clear the User Key for id %2", async (userId) => { - await cryptoService.clearUserKey(false, userId); - - expect(stateProvider.mock.setUserState).toHaveBeenCalledWith(USER_KEY, null, userId); - }); - - it("should update status to locked", async () => { - await cryptoService.clearUserKey(false, mockUserId); - - expect(accountService.mock.setMaxAccountStatus).toHaveBeenCalledWith( - mockUserId, - AuthenticationStatus.Locked, + describe("clearKeys", () => { + it("resolves active user id when called with no user id", async () => { + let callCount = 0; + accountService.activeAccount$ = accountService.activeAccountSubject.pipe( + tap(() => callCount++), ); - }); - 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); + await cryptoService.clearKeys(null); + expect(callCount).toBe(1); - 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"); + // revert to the original state + accountService.activeAccount$ = accountService.activeAccountSubject.asObservable(); }); it("sets the maximum account status of the active user id to locked when user id is not specified", async () => { - await cryptoService.clearUserKey(false); + await cryptoService.clearKeys(); expect(accountService.mock.setMaxAccountStatus).toHaveBeenCalledWith( mockUserId, AuthenticationStatus.Locked, @@ -568,17 +360,36 @@ describe("cryptoService", () => { }); 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); + const userId = "someOtherUser" as UserId; + await cryptoService.clearKeys(userId); expect(accountService.mock.setMaxAccountStatus).toHaveBeenCalledWith( - "someOtherUser" as UserId, + 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); + describe.each([ + USER_ENCRYPTED_ORGANIZATION_KEYS, + USER_ENCRYPTED_PROVIDER_KEYS, + USER_ENCRYPTED_PRIVATE_KEY, + USER_KEY, + ])("key removal", (key: UserKeyDefinition<unknown>) => { + it(`clears ${key.key} for active user when unspecified`, async () => { + await cryptoService.clearKeys(null); + + const encryptedOrgKeyState = stateProvider.singleUser.getFake(mockUserId, key); + expect(encryptedOrgKeyState.nextMock).toHaveBeenCalledTimes(1); + expect(encryptedOrgKeyState.nextMock).toHaveBeenCalledWith(null); + }); + + it(`clears ${key.key} for the specified user when specified`, async () => { + const userId = "someOtherUser" as UserId; + await cryptoService.clearKeys(userId); + + 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 dd3c497470..df7528b13c 100644 --- a/libs/common/src/platform/services/crypto.service.ts +++ b/libs/common/src/platform/services/crypto.service.ts @@ -144,7 +144,7 @@ export class CryptoService implements CryptoServiceAbstraction { async setUserKey(key: UserKey, userId?: UserId): Promise<void> { 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); @@ -242,13 +242,19 @@ export class CryptoService implements CryptoServiceAbstraction { return this.buildProtectedSymmetricKey(masterKey, newUserKey.key); } - async clearUserKey(clearStoredKeys = true, userId?: UserId): Promise<void> { - // 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 + */ + async clearUserKey(userId: UserId): Promise<void> { + 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.accountService.setMaxAccountStatus(userId, AuthenticationStatus.Locked); + await this.clearAllStoredUserKeys(userId); } async clearStoredUserKey(keySuffix: KeySuffixOptions, userId?: UserId): Promise<void> { @@ -480,25 +486,12 @@ export class CryptoService implements CryptoServiceAbstraction { return this.buildProtectedSymmetricKey(key, newSymKey.key); } - async clearOrgKeys(memoryOnly?: boolean, userId?: UserId): Promise<void> { - 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<void> { + 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<void> { @@ -526,25 +519,12 @@ export class CryptoService implements CryptoServiceAbstraction { return await firstValueFrom(this.activeUserProviderKeys$); } - async clearProviderKeys(memoryOnly?: boolean, userId?: UserId): Promise<void> { - 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<void> { + 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<Uint8Array> { @@ -597,26 +577,17 @@ export class CryptoService implements CryptoServiceAbstraction { return [publicB64, privateEnc]; } - async clearKeyPair(memoryOnly?: boolean, userId?: UserId): Promise<void[]> { - 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<void[]> { + 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<PinKey> { @@ -681,11 +652,17 @@ export class CryptoService implements CryptoServiceAbstraction { } async clearKeys(userId?: UserId): Promise<any> { - await this.clearUserKey(true, userId); + userId ||= (await firstValueFrom(this.accountService.activeAccount$))?.id; + + if (userId == null) { + throw new Error("Cannot clear keys, no user Id resolved."); + } + + await this.clearUserKey(userId); await this.clearMasterKeyHash(userId); - await this.clearOrgKeys(false, userId); - await this.clearProviderKeys(false, userId); - await this.clearKeyPair(false, 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); } 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..a70ab3d179 100644 --- a/libs/common/src/platform/services/default-environment.service.spec.ts +++ b/libs/common/src/platform/services/default-environment.service.spec.ts @@ -7,9 +7,10 @@ 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 @@ -55,7 +56,7 @@ describe("EnvironmentService", () => { }; 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 +67,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<EnvironmentState>( +export const GLOBAL_ENVIRONMENT_KEY = new KeyDefinition<EnvironmentState>( ENVIRONMENT_DISK, "environment", { @@ -48,9 +49,31 @@ export const ENVIRONMENT_KEY = new KeyDefinition<EnvironmentState>( }, ); -export const CLOUD_REGION_KEY = new KeyDefinition<CloudRegion>(ENVIRONMENT_MEMORY, "cloudRegion", { - deserializer: (b) => b, -}); +export const USER_ENVIRONMENT_KEY = new UserKeyDefinition<EnvironmentState>( + ENVIRONMENT_DISK, + "environment", + { + deserializer: EnvironmentState.fromJSON, + clearOn: ["logout"], + }, +); + +export const GLOBAL_CLOUD_REGION_KEY = new KeyDefinition<CloudRegion>( + ENVIRONMENT_MEMORY, + "cloudRegion", + { + deserializer: (b) => b, + }, +); + +export const USER_CLOUD_REGION_KEY = new UserKeyDefinition<CloudRegion>( + 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<EncryptedString, ProviderId>( +export const USER_ENCRYPTED_PROVIDER_KEYS = UserKeyDefinition.record<EncryptedString, ProviderId>( 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..abb26b92da 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,25 @@ 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 { + KeyDefinition, + CRYPTO_DISK, + DeriveDefinition, + CRYPTO_MEMORY, + UserKeyDefinition, +} from "../../state"; import { CryptoService } from "../crypto.service"; export const USER_EVER_HAD_USER_KEY = new KeyDefinition<boolean>(CRYPTO_DISK, "everHadUserKey", { deserializer: (obj) => obj, }); -export const USER_ENCRYPTED_PRIVATE_KEY = new KeyDefinition<EncryptedString>( +export const USER_ENCRYPTED_PRIVATE_KEY = new UserKeyDefinition<EncryptedString>( CRYPTO_DISK, "privateKey", { deserializer: (obj) => obj, + clearOn: ["logout"], }, ); @@ -58,6 +65,7 @@ export const USER_PUBLIC_KEY = DeriveDefinition.from< return (await cryptoFunctionService.rsaExtractPublicKey(privateKey)) as UserPublicKey; }, }); -export const USER_KEY = new KeyDefinition<UserKey>(CRYPTO_MEMORY, "userKey", { +export const USER_KEY = new UserKeyDefinition<UserKey>(CRYPTO_MEMORY, "userKey", { deserializer: (obj) => SymmetricCryptoKey.fromJSON(obj) as UserKey, + clearOn: ["logout", "lock"], }); 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<TFrom, TTo, TDeps extends DerivedStateDependencies static from<TFrom, TTo, TDeps extends DerivedStateDependencies = never>( definition: | KeyDefinition<TFrom> + | UserKeyDefinition<TFrom> | [DeriveDefinition<unknown, TFrom, DerivedStateDependencies>, string], options: DeriveDefinitionOptions<TFrom, 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); } } static fromWithUserId<TKeyDef, TTo, TDeps extends DerivedStateDependencies = never>( definition: | KeyDefinition<TKeyDef> + | UserKeyDefinition<TKeyDef> | [DeriveDefinition<unknown, TKeyDef, DerivedStateDependencies>, 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<TFrom, TTo, TDeps extends DerivedStateDependencies } } -function isKeyDefinition( +function isFromDeriveDefinition( definition: | KeyDefinition<unknown> + | UserKeyDefinition<unknown> | [DeriveDefinition<unknown, unknown, DerivedStateDependencies>, string], -): definition is KeyDefinition<unknown> { - return Object.prototype.hasOwnProperty.call(definition, "key"); +): definition is [DeriveDefinition<unknown, unknown, DerivedStateDependencies>, string] { + return Array.isArray(definition); } 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..f20bc910c0 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 @@ -156,7 +156,6 @@ 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(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..22d658c552 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout.service.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout.service.ts @@ -96,10 +96,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { 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); From 9d10825dbd891c0f41fe1b4c4dd3ca4171f63be5 Mon Sep 17 00:00:00 2001 From: Jake Fink <jfink@bitwarden.com> Date: Tue, 9 Apr 2024 20:50:20 -0400 Subject: [PATCH 144/351] [PM-5362] Add MP Service (attempt #2) (#8619) * create mp and kdf service * update mp service interface to not rely on active user * rename observable methods * update crypto service with new MP service * add master password service to login strategies - make fake service for easier testing - fix crypto service tests * update auth service and finish strategies * auth request refactors * more service refactors and constructor updates * setMasterKey refactors * remove master key methods from crypto service * remove master key and hash from state service * missed fixes * create migrations and fix references * fix master key imports * default force set password reason to none * add password reset reason observable factory to service * remove kdf changes and migrate only disk data * update migration number * fix sync service deps * use disk for force set password state * fix desktop migration * fix sso test * fix tests * fix more tests * fix even more tests * fix even more tests * fix cli * remove kdf service abstraction * add missing deps for browser * fix merge conflicts * clear reset password reason on lock or logout * fix tests * fix other tests * add jsdocs to abstraction * use state provider in crypto service * inverse master password service factory * add clearOn to master password service * add parameter validation to master password service * add component level userId * add missed userId * migrate key hash * fix login strategy service * delete crypto master key from account * migrate master key encrypted user key * rename key hash to master key hash * use mp service for getMasterKeyEncryptedUserKey * fix tests * fix user key decryption logic * add clear methods to mp service * fix circular dep and encryption issue * fix test * remove extra account service call * use EncString in state provider * fix tests * return to using encrypted string for serialization --- .../auth-request-service.factory.ts | 16 +- .../key-connector-service.factory.ts | 9 + .../login-strategy-service.factory.ts | 9 + .../master-password-service.factory.ts | 42 ++++ .../user-verification-service.factory.ts | 9 + apps/browser/src/auth/popup/lock.component.ts | 3 + .../src/auth/popup/set-password.component.ts | 58 +---- apps/browser/src/auth/popup/sso.component.ts | 8 +- .../src/auth/popup/two-factor.component.ts | 6 + .../browser/src/background/main.background.ts | 23 +- .../background/nativeMessaging.background.ts | 16 +- .../vault-timeout-service.factory.ts | 12 + .../crypto-service.factory.ts | 6 + .../services/browser-crypto.service.ts | 3 + apps/cli/src/auth/commands/unlock.command.ts | 17 +- apps/cli/src/bw.ts | 16 +- apps/cli/src/commands/serve.command.ts | 2 + apps/cli/src/program.ts | 4 + apps/desktop/src/app/app.component.ts | 7 +- .../src/app/services/services.module.ts | 2 + apps/desktop/src/auth/lock.component.spec.ts | 6 + apps/desktop/src/auth/lock.component.ts | 3 + .../src/auth/set-password.component.ts | 6 + apps/desktop/src/auth/sso.component.ts | 6 + apps/desktop/src/auth/two-factor.component.ts | 6 + .../services/electron-crypto.service.spec.ts | 4 + .../services/electron-crypto.service.ts | 13 +- .../src/services/native-messaging.service.ts | 6 +- .../user-key-rotation.service.spec.ts | 14 +- .../key-rotation/user-key-rotation.service.ts | 5 +- apps/web/src/app/auth/lock.component.ts | 70 +----- apps/web/src/app/auth/sso.component.ts | 6 + apps/web/src/app/auth/two-factor.component.ts | 6 + libs/angular/jest.config.js | 10 +- .../src/auth/components/lock.component.ts | 17 +- .../auth/components/set-password.component.ts | 24 +- .../src/auth/components/sso.component.spec.ts | 15 +- .../src/auth/components/sso.component.ts | 8 +- .../components/two-factor.component.spec.ts | 16 +- .../auth/components/two-factor.component.ts | 8 +- .../update-temp-password.component.ts | 14 +- libs/angular/src/auth/guards/auth.guard.ts | 12 +- .../src/services/jslib-services.module.ts | 28 ++- .../auth-request-login.strategy.spec.ts | 25 ++- .../auth-request-login.strategy.ts | 22 +- .../login-strategies/login.strategy.spec.ts | 18 +- .../common/login-strategies/login.strategy.ts | 4 + .../password-login.strategy.spec.ts | 26 ++- .../password-login.strategy.ts | 23 +- .../sso-login.strategy.spec.ts | 22 +- .../login-strategies/sso-login.strategy.ts | 17 +- .../user-api-login.strategy.spec.ts | 13 +- .../user-api-login.strategy.ts | 9 +- .../webauthn-login.strategy.spec.ts | 11 +- .../webauthn-login.strategy.ts | 6 + .../auth-request/auth-request.service.spec.ts | 42 +++- .../auth-request/auth-request.service.ts | 18 +- .../login-strategy.service.spec.ts | 17 +- .../login-strategy.service.ts | 19 +- .../master-password.service.abstraction.ts | 82 +++++++ .../services/key-connector.service.spec.ts | 16 +- .../auth/services/key-connector.service.ts | 13 +- .../fake-master-password.service.ts | 64 ++++++ .../master-password.service.ts | 140 ++++++++++++ .../user-verification.service.ts | 16 +- .../platform/abstractions/crypto.service.ts | 31 --- .../platform/abstractions/state.service.ts | 30 --- .../models/domain/account-keys.spec.ts | 7 - .../src/platform/models/domain/account.ts | 10 - .../platform/services/crypto.service.spec.ts | 18 +- .../src/platform/services/crypto.service.ts | 107 ++++----- .../src/platform/services/state.service.ts | 103 --------- .../src/platform/state/state-definitions.ts | 2 + .../vault-timeout.service.spec.ts | 23 +- .../vault-timeout/vault-timeout.service.ts | 10 +- libs/common/src/state-migrations/migrate.ts | 6 +- ...-move-master-key-state-to-provider.spec.ts | 210 ++++++++++++++++++ .../55-move-master-key-state-to-provider.ts | 111 +++++++++ .../src/vault/services/sync/sync.service.ts | 12 +- 79 files changed, 1373 insertions(+), 501 deletions(-) create mode 100644 apps/browser/src/auth/background/service-factories/master-password-service.factory.ts create mode 100644 libs/common/src/auth/abstractions/master-password.service.abstraction.ts create mode 100644 libs/common/src/auth/services/master-password/fake-master-password.service.ts create mode 100644 libs/common/src/auth/services/master-password/master-password.service.ts create mode 100644 libs/common/src/state-migrations/migrations/55-move-master-key-state-to-provider.spec.ts create mode 100644 libs/common/src/state-migrations/migrations/55-move-master-key-state-to-provider.ts 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..295fedbadd 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 @@ -17,18 +17,21 @@ import { FactoryOptions, factory, } from "../../../platform/background/service-factories/factory-options"; + +import { accountServiceFactory, AccountServiceInitOptions } from "./account-service.factory"; import { - stateServiceFactory, - StateServiceInitOptions, -} from "../../../platform/background/service-factories/state-service.factory"; + internalMasterPasswordServiceFactory, + MasterPasswordServiceInitOptions, +} from "./master-password-service.factory"; type AuthRequestServiceFactoryOptions = FactoryOptions; export type AuthRequestServiceInitOptions = AuthRequestServiceFactoryOptions & AppIdServiceInitOptions & + AccountServiceInitOptions & + MasterPasswordServiceInitOptions & CryptoServiceInitOptions & - ApiServiceInitOptions & - StateServiceInitOptions; + ApiServiceInitOptions; export function authRequestServiceFactory( cache: { authRequestService?: AuthRequestServiceAbstraction } & CachedServices, @@ -41,9 +44,10 @@ 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), ), ); } 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 4a0dd07b32..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 @@ -31,6 +31,11 @@ import { 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,6 +45,8 @@ type KeyConnectorServiceFactoryOptions = FactoryOptions & { }; export type KeyConnectorServiceInitOptions = KeyConnectorServiceFactoryOptions & + AccountServiceInitOptions & + MasterPasswordServiceInitOptions & CryptoServiceInitOptions & ApiServiceInitOptions & TokenServiceInitOptions & @@ -58,6 +65,8 @@ export function keyConnectorServiceFactory( opts, async () => new KeyConnectorService( + 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/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<InternalMasterPasswordServiceAbstraction> { + return factory( + cache, + "masterPasswordService", + opts, + async () => new MasterPasswordService(await stateProviderFactory(cache, opts)), + ); +} + +export async function masterPasswordServiceFactory( + cache: { masterPasswordService?: InternalMasterPasswordServiceAbstraction } & CachedServices, + opts: MasterPasswordServiceInitOptions, +): Promise<MasterPasswordServiceAbstraction> { + return (await internalMasterPasswordServiceFactory( + cache, + opts, + )) as MasterPasswordServiceAbstraction; +} 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/lock.component.ts b/apps/browser/src/auth/popup/lock.component.ts index f232eca45a..16c32337cf 100644 --- a/apps/browser/src/auth/popup/lock.component.ts +++ b/apps/browser/src/auth/popup/lock.component.ts @@ -12,6 +12,7 @@ import { InternalPolicyService } from "@bitwarden/common/admin-console/abstracti 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"; @@ -41,6 +42,7 @@ export class LockComponent extends BaseLockComponent { fido2PopoutSessionData$ = fido2PopoutSessionData$(); constructor( + masterPasswordService: InternalMasterPasswordServiceAbstraction, router: Router, i18nService: I18nService, platformUtilsService: PlatformUtilsService, @@ -66,6 +68,7 @@ export class LockComponent extends BaseLockComponent { accountService: AccountService, ) { super( + masterPasswordService, router, i18nService, platformUtilsService, 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 228c7401fd..14df0d1752 100644 --- a/apps/browser/src/auth/popup/sso.component.ts +++ b/apps/browser/src/auth/popup/sso.component.ts @@ -9,7 +9,9 @@ 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 { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -45,7 +47,9 @@ export class SsoComponent extends BaseSsoComponent { logService: LogService, userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, configService: ConfigService, - protected authService: AuthService, + 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 9bac336695..98363bc93c 100644 --- a/apps/browser/src/auth/popup/two-factor.component.ts +++ b/apps/browser/src/auth/popup/two-factor.component.ts @@ -11,6 +11,8 @@ 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 { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; @@ -58,6 +60,8 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { configService: ConfigService, ssoLoginService: SsoLoginServiceAbstraction, private dialogService: DialogService, + masterPasswordService: InternalMasterPasswordServiceAbstraction, + accountService: AccountService, @Inject(WINDOW) protected win: Window, private browserMessagingApi: ZonedMessageListenerService, ) { @@ -78,6 +82,8 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { 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/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index a7fadc6d6f..f649c5a598 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -32,6 +32,7 @@ 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 { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service"; @@ -46,6 +47,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 { MasterPasswordService } from "@bitwarden/common/auth/services/master-password/master-password.service"; import { SsoLoginService } from "@bitwarden/common/auth/services/sso-login.service"; import { TokenService } from "@bitwarden/common/auth/services/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/services/two-factor.service"; @@ -242,6 +244,7 @@ export default class MainBackground { keyGenerationService: KeyGenerationServiceAbstraction; cryptoService: CryptoServiceAbstraction; cryptoFunctionService: CryptoFunctionServiceAbstraction; + masterPasswordService: InternalMasterPasswordServiceAbstraction; tokenService: TokenServiceAbstraction; appIdService: AppIdServiceAbstraction; apiService: ApiServiceAbstraction; @@ -480,8 +483,11 @@ export default class MainBackground { const themeStateService = new DefaultThemeStateService(this.globalStateProvider); + this.masterPasswordService = new MasterPasswordService(this.stateProvider); + this.i18nService = new I18nService(BrowserApi.getUILanguage(), this.globalStateProvider); this.cryptoService = new BrowserCryptoService( + this.masterPasswordService, this.keyGenerationService, this.cryptoFunctionService, this.encryptService, @@ -525,6 +531,8 @@ export default class MainBackground { this.badgeSettingsService = new BadgeSettingsService(this.stateProvider); this.policyApiService = new PolicyApiService(this.policyService, this.apiService); this.keyConnectorService = new KeyConnectorService( + this.accountService, + this.masterPasswordService, this.cryptoService, this.apiService, this.tokenService, @@ -578,9 +586,10 @@ export default class MainBackground { this.authRequestService = new AuthRequestService( this.appIdService, + this.accountService, + this.masterPasswordService, this.cryptoService, this.apiService, - this.stateService, ); this.authService = new AuthService( @@ -597,6 +606,8 @@ export default class MainBackground { ); this.loginStrategyService = new LoginStrategyService( + this.accountService, + this.masterPasswordService, this.cryptoService, this.apiService, this.tokenService, @@ -672,6 +683,8 @@ export default class MainBackground { this.userVerificationService = new UserVerificationService( this.stateService, this.cryptoService, + this.accountService, + this.masterPasswordService, this.i18nService, this.userVerificationApiService, this.userDecryptionOptionsService, @@ -694,6 +707,8 @@ export default class MainBackground { this.vaultSettingsService = new VaultSettingsService(this.stateProvider); this.vaultTimeoutService = new VaultTimeoutService( + this.accountService, + this.masterPasswordService, this.cipherService, this.folderService, this.collectionService, @@ -729,6 +744,8 @@ export default class MainBackground { this.providerService = new ProviderService(this.stateProvider); this.syncService = new SyncService( + this.masterPasswordService, + this.accountService, this.apiService, this.domainSettingsService, this.folderService, @@ -878,6 +895,8 @@ export default class MainBackground { this.fido2Service, ); this.nativeMessagingBackground = new NativeMessagingBackground( + this.accountService, + this.masterPasswordService, this.cryptoService, this.cryptoFunctionService, this.runtimeBackground, @@ -1107,7 +1126,7 @@ export default class MainBackground { const status = await this.authService.getAuthStatus(userId); const forcePasswordReset = - (await this.stateService.getForceSetPasswordReason({ userId: userId })) != + (await firstValueFrom(this.masterPasswordService.forceSetPasswordReason$(userId))) != ForceSetPasswordReason.None; await this.systemService.clearPendingClipboard(); diff --git a/apps/browser/src/background/nativeMessaging.background.ts b/apps/browser/src/background/nativeMessaging.background.ts index 240fb1dede..faf2e6e2cc 100644 --- a/apps/browser/src/background/nativeMessaging.background.ts +++ b/apps/browser/src/background/nativeMessaging.background.ts @@ -1,6 +1,8 @@ import { firstValueFrom } from "rxjs"; +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 { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; @@ -71,6 +73,8 @@ export class NativeMessagingBackground { private validatingFingerprint: boolean; constructor( + private accountService: AccountService, + private masterPasswordService: InternalMasterPasswordServiceAbstraction, private cryptoService: CryptoService, private cryptoFunctionService: CryptoFunctionService, private runtimeBackground: RuntimeBackground, @@ -336,10 +340,14 @@ export class NativeMessagingBackground { ) as UserKey; await this.cryptoService.setUserKey(userKey); } else if (message.keyB64) { + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; // Backwards compatibility to support cases in which the user hasn't updated their desktop app // TODO: Remove after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3472) - let encUserKey = await this.stateService.getEncryptedCryptoSymmetricKey(); - encUserKey ||= await this.stateService.getMasterKeyEncryptedUserKey(); + const encUserKeyPrim = await this.stateService.getEncryptedCryptoSymmetricKey(); + const encUserKey = + encUserKeyPrim != null + ? new EncString(encUserKeyPrim) + : await this.masterPasswordService.getMasterKeyEncryptedUserKey(userId); if (!encUserKey) { throw new Error("No encrypted user key found"); } @@ -348,9 +356,9 @@ export class NativeMessagingBackground { ) as MasterKey; const userKey = await this.cryptoService.decryptUserKeyWithMasterKey( masterKey, - new EncString(encUserKey), + encUserKey, ); - await this.cryptoService.setMasterKey(masterKey); + await this.masterPasswordService.setMasterKey(masterKey, userId); await this.cryptoService.setUserKey(userKey); } else { throw new Error("No key received"); diff --git a/apps/browser/src/background/service-factories/vault-timeout-service.factory.ts b/apps/browser/src/background/service-factories/vault-timeout-service.factory.ts index 0e4d1420da..14f055114b 100644 --- a/apps/browser/src/background/service-factories/vault-timeout-service.factory.ts +++ b/apps/browser/src/background/service-factories/vault-timeout-service.factory.ts @@ -1,9 +1,17 @@ import { VaultTimeoutService as AbstractVaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; +import { + accountServiceFactory, + AccountServiceInitOptions, +} from "../../auth/background/service-factories/account-service.factory"; import { authServiceFactory, AuthServiceInitOptions, } from "../../auth/background/service-factories/auth-service.factory"; +import { + internalMasterPasswordServiceFactory, + MasterPasswordServiceInitOptions, +} from "../../auth/background/service-factories/master-password-service.factory"; import { CryptoServiceInitOptions, cryptoServiceFactory, @@ -57,6 +65,8 @@ type VaultTimeoutServiceFactoryOptions = FactoryOptions & { }; export type VaultTimeoutServiceInitOptions = VaultTimeoutServiceFactoryOptions & + AccountServiceInitOptions & + MasterPasswordServiceInitOptions & CipherServiceInitOptions & FolderServiceInitOptions & CollectionServiceInitOptions & @@ -79,6 +89,8 @@ export function vaultTimeoutServiceFactory( opts, async () => new VaultTimeoutService( + await accountServiceFactory(cache, opts), + await internalMasterPasswordServiceFactory(cache, opts), await cipherServiceFactory(cache, opts), await folderServiceFactory(cache, opts), await collectionServiceFactory(cache, opts), diff --git a/apps/browser/src/platform/background/service-factories/crypto-service.factory.ts b/apps/browser/src/platform/background/service-factories/crypto-service.factory.ts index 97614660d1..ed4fde162c 100644 --- a/apps/browser/src/platform/background/service-factories/crypto-service.factory.ts +++ b/apps/browser/src/platform/background/service-factories/crypto-service.factory.ts @@ -4,6 +4,10 @@ import { AccountServiceInitOptions, accountServiceFactory, } from "../../../auth/background/service-factories/account-service.factory"; +import { + internalMasterPasswordServiceFactory, + MasterPasswordServiceInitOptions, +} from "../../../auth/background/service-factories/master-password-service.factory"; import { StateServiceInitOptions, stateServiceFactory, @@ -34,6 +38,7 @@ import { StateProviderInitOptions, stateProviderFactory } from "./state-provider type CryptoServiceFactoryOptions = FactoryOptions; export type CryptoServiceInitOptions = CryptoServiceFactoryOptions & + MasterPasswordServiceInitOptions & KeyGenerationServiceInitOptions & CryptoFunctionServiceInitOptions & EncryptServiceInitOptions & @@ -53,6 +58,7 @@ export function cryptoServiceFactory( opts, async () => new BrowserCryptoService( + await internalMasterPasswordServiceFactory(cache, opts), await keyGenerationServiceFactory(cache, opts), await cryptoFunctionServiceFactory(cache, opts), await encryptServiceFactory(cache, opts), diff --git a/apps/browser/src/platform/services/browser-crypto.service.ts b/apps/browser/src/platform/services/browser-crypto.service.ts index 969dbdf761..d7533a22d6 100644 --- a/apps/browser/src/platform/services/browser-crypto.service.ts +++ b/apps/browser/src/platform/services/browser-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"; @@ -17,6 +18,7 @@ import { UserKey } from "@bitwarden/common/types/key"; export class BrowserCryptoService extends CryptoService { constructor( + masterPasswordService: InternalMasterPasswordServiceAbstraction, keyGenerationService: KeyGenerationService, cryptoFunctionService: CryptoFunctionService, encryptService: EncryptService, @@ -28,6 +30,7 @@ export class BrowserCryptoService extends CryptoService { private biometricStateService: BiometricStateService, ) { super( + masterPasswordService, keyGenerationService, cryptoFunctionService, encryptService, 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 0e6571f775..a2e4afe709 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -28,6 +28,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv 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"; @@ -168,6 +169,7 @@ export class Main { organizationUserService: OrganizationUserService; collectionService: CollectionService; vaultTimeoutService: VaultTimeoutService; + masterPasswordService: InternalMasterPasswordServiceAbstraction; vaultTimeoutSettingsService: VaultTimeoutSettingsService; syncService: SyncService; eventCollectionService: EventCollectionServiceAbstraction; @@ -352,6 +354,7 @@ export class Main { ); this.cryptoService = new CryptoService( + this.masterPasswordService, this.keyGenerationService, this.cryptoFunctionService, this.encryptService, @@ -432,6 +435,8 @@ export class Main { this.policyApiService = new PolicyApiService(this.policyService, this.apiService); this.keyConnectorService = new KeyConnectorService( + this.accountService, + this.masterPasswordService, this.cryptoService, this.apiService, this.tokenService, @@ -471,9 +476,10 @@ export class Main { this.authRequestService = new AuthRequestService( this.appIdService, + this.accountService, + this.masterPasswordService, this.cryptoService, this.apiService, - this.stateService, ); this.billingAccountProfileStateService = new DefaultBillingAccountProfileStateService( @@ -481,6 +487,8 @@ export class Main { ); this.loginStrategyService = new LoginStrategyService( + this.accountService, + this.masterPasswordService, this.cryptoService, this.apiService, this.tokenService, @@ -568,6 +576,8 @@ export class Main { this.userVerificationService = new UserVerificationService( this.stateService, this.cryptoService, + this.accountService, + this.masterPasswordService, this.i18nService, this.userVerificationApiService, this.userDecryptionOptionsService, @@ -578,6 +588,8 @@ export class Main { ); this.vaultTimeoutService = new VaultTimeoutService( + this.accountService, + this.masterPasswordService, this.cipherService, this.folderService, this.collectionService, @@ -596,6 +608,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, 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/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/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index 884296ea29..b0b411c5f0 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -26,6 +26,7 @@ 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"; @@ -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, @@ -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) { diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index 84932ce7d9..8e412d4977 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -20,6 +20,7 @@ import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaul 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 { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { BroadcasterService as BroadcasterServiceAbstraction } from "@bitwarden/common/platform/abstractions/broadcaster.service"; @@ -228,6 +229,7 @@ const safeProviders: SafeProvider[] = [ provide: CryptoServiceAbstraction, useClass: ElectronCryptoService, deps: [ + InternalMasterPasswordServiceAbstraction, KeyGenerationServiceAbstraction, CryptoFunctionServiceAbstraction, EncryptService, diff --git a/apps/desktop/src/auth/lock.component.spec.ts b/apps/desktop/src/auth/lock.component.spec.ts index 0339889bf7..c125eba022 100644 --- a/apps/desktop/src/auth/lock.component.spec.ts +++ b/apps/desktop/src/auth/lock.component.spec.ts @@ -14,7 +14,9 @@ import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abs 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 { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; @@ -52,6 +54,7 @@ describe("LockComponent", () => { let broadcasterServiceMock: MockProxy<BroadcasterService>; let platformUtilsServiceMock: MockProxy<PlatformUtilsService>; let activatedRouteMock: MockProxy<ActivatedRoute>; + let mockMasterPasswordService: FakeMasterPasswordService; const mockUserId = Utils.newGuid() as UserId; const accountService: FakeAccountService = mockAccountServiceWith(mockUserId); @@ -67,6 +70,8 @@ describe("LockComponent", () => { activatedRouteMock = mock<ActivatedRoute>(); activatedRouteMock.queryParams = mock<ActivatedRoute["queryParams"]>(); + mockMasterPasswordService = new FakeMasterPasswordService(); + biometricStateService.dismissedRequirePasswordOnStartCallout$ = of(false); biometricStateService.promptAutomatically$ = of(false); biometricStateService.promptCancelled$ = of(false); @@ -74,6 +79,7 @@ describe("LockComponent", () => { await TestBed.configureTestingModule({ declarations: [LockComponent, I18nPipe], providers: [ + { provide: InternalMasterPasswordServiceAbstraction, useValue: mockMasterPasswordService }, { provide: I18nService, useValue: mock<I18nService>(), diff --git a/apps/desktop/src/auth/lock.component.ts b/apps/desktop/src/auth/lock.component.ts index 8b1448c06f..16b58c5bbe 100644 --- a/apps/desktop/src/auth/lock.component.ts +++ b/apps/desktop/src/auth/lock.component.ts @@ -11,6 +11,7 @@ import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abs 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"; @@ -38,6 +39,7 @@ export class LockComponent extends BaseLockComponent { private autoPromptBiometric = false; constructor( + masterPasswordService: InternalMasterPasswordServiceAbstraction, router: Router, i18nService: I18nService, platformUtilsService: PlatformUtilsService, @@ -63,6 +65,7 @@ export class LockComponent extends BaseLockComponent { accountService: AccountService, ) { super( + masterPasswordService, router, i18nService, platformUtilsService, 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 210319b9ed..cc261f1235 100644 --- a/apps/desktop/src/auth/sso.component.ts +++ b/apps/desktop/src/auth/sso.component.ts @@ -7,6 +7,8 @@ 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 { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; @@ -39,6 +41,8 @@ export class SsoComponent extends BaseSsoComponent { logService: LogService, userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, 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 fdbc52b4bf..d1b84c1fa0 100644 --- a/apps/desktop/src/auth/two-factor.component.ts +++ b/apps/desktop/src/auth/two-factor.component.ts @@ -11,6 +11,8 @@ 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 { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; @@ -60,6 +62,8 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction, configService: ConfigService, + masterPasswordService: InternalMasterPasswordServiceAbstraction, + accountService: AccountService, @Inject(WINDOW) protected win: Window, ) { super( @@ -79,6 +83,8 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { 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/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<PlatformUtilsService>(); const logService = mock<LogService>(); const stateService = mock<StateService>(); + let masterPasswordService: FakeMasterPasswordService; let accountService: FakeAccountService; let stateProvider: FakeStateProvider; const biometricStateService = mock<BiometricStateService>(); @@ -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/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<string, SymmetricCryptoKey>(); 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/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 09c7bf9ace..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,6 +2,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject } from "rxjs"; import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.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"; @@ -9,7 +10,6 @@ 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 { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; 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"; @@ -22,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"; @@ -46,8 +50,10 @@ describe("KeyRotationService", () => { const mockUserId = Utils.newGuid() as UserId; const mockAccountService: FakeAccountService = mockAccountServiceWith(mockUserId); + let mockMasterPasswordService: FakeMasterPasswordService = new FakeMasterPasswordService(); beforeAll(() => { + mockMasterPasswordService = new FakeMasterPasswordService(); mockApiService = mock<UserKeyRotationApiService>(); mockCipherService = mock<CipherService>(); mockFolderService = mock<FolderService>(); @@ -61,6 +67,7 @@ describe("KeyRotationService", () => { mockConfigService = mock<ConfigService>(); keyRotationService = new UserKeyRotationService( + mockMasterPasswordService, mockApiService, mockCipherService, mockFolderService, @@ -174,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 03bc604b4d..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 @@ -3,6 +3,7 @@ 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 { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -25,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, @@ -61,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) { diff --git a/apps/web/src/app/auth/lock.component.ts b/apps/web/src/app/auth/lock.component.ts index a1d4724396..021bf0f9df 100644 --- a/apps/web/src/app/auth/lock.component.ts +++ b/apps/web/src/app/auth/lock.component.ts @@ -1,80 +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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -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, - accountService: AccountService, - ) { - super( - router, - i18nService, - platformUtilsService, - messagingService, - cryptoService, - vaultTimeoutService, - vaultTimeoutSettingsService, - environmentService, - stateService, - apiService, - logService, - ngZone, - policyApiService, - policyService, - passwordStrengthService, - dialogService, - deviceTrustCryptoService, - userVerificationService, - pinCryptoService, - biometricStateService, - accountService, - ); - } - async ngOnInit() { await super.ngOnInit(); this.onSuccessfulSubmit = async () => { diff --git a/apps/web/src/app/auth/sso.component.ts b/apps/web/src/app/auth/sso.component.ts index cdd979aa89..e120b2749f 100644 --- a/apps/web/src/app/auth/sso.component.ts +++ b/apps/web/src/app/auth/sso.component.ts @@ -10,6 +10,8 @@ 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"; @@ -46,6 +48,8 @@ export class SsoComponent extends BaseSsoComponent { private validationService: ValidationService, userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, 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/two-factor.component.ts b/apps/web/src/app/auth/two-factor.component.ts index 65bf1dba58..eed84b91f1 100644 --- a/apps/web/src/app/auth/two-factor.component.ts +++ b/apps/web/src/app/auth/two-factor.component.ts @@ -10,6 +10,8 @@ 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 { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; @@ -50,6 +52,8 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnDest userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction, configService: ConfigService, + masterPasswordService: InternalMasterPasswordServiceAbstraction, + accountService: AccountService, @Inject(WINDOW) protected win: Window, ) { super( @@ -69,6 +73,8 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnDest userDecryptionOptionsService, ssoLoginService, configService, + masterPasswordService, + accountService, ); this.onSuccessfulLoginNavigate = this.goAfterLogIn; } 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: ["<rootDir>/test.setup.ts"], - moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, { - prefix: "<rootDir>/", - }), + moduleNameMapper: pathsToModuleNameMapper( + // lets us use @bitwarden/common/spec in tests + { "@bitwarden/common/spec": ["../common/spec"], ...(compilerOptions?.paths ?? {}) }, + { + prefix: "<rootDir>/", + }, + ), }; diff --git a/libs/angular/src/auth/components/lock.component.ts b/libs/angular/src/auth/components/lock.component.ts index aa3b801ded..6602a917c9 100644 --- a/libs/angular/src/auth/components/lock.component.ts +++ b/libs/angular/src/auth/components/lock.component.ts @@ -12,6 +12,7 @@ import { InternalPolicyService } from "@bitwarden/common/admin-console/abstracti 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"; @@ -56,6 +57,7 @@ export class LockComponent implements OnInit, OnDestroy { private destroy$ = new Subject<void>(); constructor( + protected masterPasswordService: InternalMasterPasswordServiceAbstraction, protected router: Router, protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, @@ -206,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(); @@ -215,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, @@ -244,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 { @@ -262,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); } @@ -292,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/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<void>; 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 c5c062d9a7..269ec51e30 100644 --- a/libs/angular/src/auth/components/sso.component.spec.ts +++ b/libs/angular/src/auth/components/sso.component.spec.ts @@ -12,10 +12,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 { 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 { 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"; @@ -23,7 +26,9 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic 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<TestSsoComponent>; + const userId = "userId" as UserId; // Mock Services let mockLoginStrategyService: MockProxy<LoginStrategyServiceAbstraction>; @@ -67,6 +73,8 @@ describe("SsoComponent", () => { let mockLogService: MockProxy<LogService>; let mockUserDecryptionOptionsService: MockProxy<UserDecryptionOptionsServiceAbstraction>; let mockConfigService: MockProxy<ConfigService>; + let mockMasterPasswordService: FakeMasterPasswordService; + let mockAccountService: FakeAccountService; // Mock authService.logIn params let code: string; @@ -117,6 +125,8 @@ describe("SsoComponent", () => { mockLogService = mock(); mockUserDecryptionOptionsService = mock(); mockConfigService = mock(); + mockAccountService = mockAccountServiceWith(userId); + mockMasterPasswordService = new FakeMasterPasswordService(); // Mock loginStrategyService.logIn params code = "code"; @@ -199,6 +209,8 @@ describe("SsoComponent", () => { }, { provide: LogService, useValue: mockLogService }, { 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 68d6e72e8d..30815beef8 100644 --- a/libs/angular/src/auth/components/sso.component.ts +++ b/libs/angular/src/auth/components/sso.component.ts @@ -11,6 +11,8 @@ 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"; @@ -66,6 +68,8 @@ export class SsoComponent { protected logService: LogService, protected userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, 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 bff39188ea..0eb248f6d9 100644 --- a/libs/angular/src/auth/components/two-factor.component.spec.ts +++ b/libs/angular/src/auth/components/two-factor.component.spec.ts @@ -15,11 +15,14 @@ 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 { 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 { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; @@ -27,6 +30,8 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic 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<TestTwoFactorComponent>; + const userId = "userId" as UserId; // Mock Services let mockLoginStrategyService: MockProxy<LoginStrategyServiceAbstraction>; @@ -63,6 +69,8 @@ describe("TwoFactorComponent", () => { let mockUserDecryptionOptionsService: MockProxy<UserDecryptionOptionsServiceAbstraction>; let mockSsoLoginService: MockProxy<SsoLoginServiceAbstraction>; let mockConfigService: MockProxy<ConfigService>; + let mockMasterPasswordService: FakeMasterPasswordService; + let mockAccountService: FakeAccountService; let mockUserDecryptionOpts: { noMasterPassword: UserDecryptionOptions; @@ -93,6 +101,8 @@ describe("TwoFactorComponent", () => { mockUserDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>(); mockSsoLoginService = mock<SsoLoginServiceAbstraction>(); mockConfigService = mock<ConfigService>(); + mockAccountService = mockAccountServiceWith(userId); + mockMasterPasswordService = new FakeMasterPasswordService(); mockUserDecryptionOpts = { noMasterPassword: new UserDecryptionOptions({ @@ -170,6 +180,8 @@ describe("TwoFactorComponent", () => { }, { provide: SsoLoginServiceAbstraction, useValue: mockSsoLoginService }, { provide: ConfigService, useValue: mockConfigService }, + { provide: InternalMasterPasswordServiceAbstraction, useValue: mockMasterPasswordService }, + { provide: AccountService, useValue: mockAccountService }, ], }); @@ -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 c306e6cc80..f73f0483be 100644 --- a/libs/angular/src/auth/components/two-factor.component.ts +++ b/libs/angular/src/auth/components/two-factor.component.ts @@ -14,6 +14,8 @@ 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 { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type"; @@ -92,6 +94,8 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI protected userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, protected ssoLoginService: SsoLoginServiceAbstraction, protected configService: ConfigService, + protected masterPasswordService: InternalMasterPasswordServiceAbstraction, + protected accountService: AccountService, ) { super(environmentService, i18nService, platformUtilsService); this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win); @@ -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/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 73f2bb4a32..ce60271e27 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -60,6 +60,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 { + 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,6 +82,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 { 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"; @@ -359,6 +364,8 @@ const safeProviders: SafeProvider[] = [ provide: LoginStrategyServiceAbstraction, useClass: LoginStrategyService, deps: [ + AccountServiceAbstraction, + InternalMasterPasswordServiceAbstraction, CryptoServiceAbstraction, ApiServiceAbstraction, TokenServiceAbstraction, @@ -521,6 +528,7 @@ const safeProviders: SafeProvider[] = [ provide: CryptoServiceAbstraction, useClass: CryptoService, deps: [ + InternalMasterPasswordServiceAbstraction, KeyGenerationServiceAbstraction, CryptoFunctionServiceAbstraction, EncryptService, @@ -587,6 +595,8 @@ const safeProviders: SafeProvider[] = [ provide: SyncServiceAbstraction, useClass: SyncService, deps: [ + InternalMasterPasswordServiceAbstraction, + AccountServiceAbstraction, ApiServiceAbstraction, DomainSettingsService, InternalFolderService, @@ -626,6 +636,8 @@ const safeProviders: SafeProvider[] = [ provide: VaultTimeoutService, useClass: VaultTimeoutService, deps: [ + AccountServiceAbstraction, + InternalMasterPasswordServiceAbstraction, CipherServiceAbstraction, FolderServiceAbstraction, CollectionServiceAbstraction, @@ -771,10 +783,21 @@ const safeProviders: SafeProvider[] = [ useClass: PolicyApiService, deps: [InternalPolicyService, ApiServiceAbstraction], }), + safeProvider({ + provide: InternalMasterPasswordServiceAbstraction, + useClass: MasterPasswordService, + deps: [StateProvider], + }), + safeProvider({ + provide: MasterPasswordServiceAbstraction, + useExisting: InternalMasterPasswordServiceAbstraction, + }), safeProvider({ provide: KeyConnectorServiceAbstraction, useClass: KeyConnectorService, deps: [ + AccountServiceAbstraction, + InternalMasterPasswordServiceAbstraction, CryptoServiceAbstraction, ApiServiceAbstraction, TokenServiceAbstraction, @@ -791,6 +814,8 @@ const safeProviders: SafeProvider[] = [ deps: [ StateServiceAbstraction, CryptoServiceAbstraction, + AccountServiceAbstraction, + InternalMasterPasswordServiceAbstraction, I18nServiceAbstraction, UserVerificationApiServiceAbstraction, UserDecryptionOptionsServiceAbstraction, @@ -934,9 +959,10 @@ const safeProviders: SafeProvider[] = [ useClass: AuthRequestService, deps: [ AppIdServiceAbstraction, + AccountServiceAbstraction, + InternalMasterPasswordServiceAbstraction, CryptoServiceAbstraction, ApiServiceAbstraction, - StateServiceAbstraction, ], }), safeProvider({ 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<DeviceTrustCryptoServiceAbstraction>; let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>; + 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<DeviceTrustCryptoServiceAbstraction>(); billingAccountProfileStateService = mock<BillingAccountProfileStateService>(); + 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 31a0cebbfe..e47f0f88ee 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"; @@ -47,6 +49,8 @@ export class AuthRequestLoginStrategy extends LoginStrategy { constructor( data: AuthRequestLoginStrategyData, + accountService: AccountService, + masterPasswordService: InternalMasterPasswordServiceAbstraction, cryptoService: CryptoService, apiService: ApiService, tokenService: TokenService, @@ -61,6 +65,8 @@ export class AuthRequestLoginStrategy extends LoginStrategy { billingAccountProfileStateService: BillingAccountProfileStateService, ) { super( + accountService, + masterPasswordService, cryptoService, apiService, tokenService, @@ -114,8 +120,15 @@ 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, + ); } } @@ -137,7 +150,8 @@ export class AuthRequestLoginStrategy extends LoginStrategy { } private async trySetUserKeyWithMasterKey(): Promise<void> { - 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 0ac22047c5..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,11 +32,13 @@ 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 { UserId } from "@bitwarden/common/types/guid"; import { UserKey, MasterKey } from "@bitwarden/common/types/key"; import { LoginStrategyServiceAbstraction } from "../abstractions"; @@ -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<LoginStrategyServiceAbstraction>; let cryptoService: MockProxy<CryptoService>; @@ -118,6 +123,9 @@ describe("LoginStrategy", () => { let credentials: PasswordLoginCredentials; beforeEach(async () => { + accountService = mockAccountServiceWith(userId); + masterPasswordService = new FakeMasterPasswordService(); + loginStrategyService = mock<LoginStrategyServiceAbstraction>(); cryptoService = mock<CryptoService>(); apiService = mock<ApiService>(); @@ -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, @@ -241,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); @@ -260,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); @@ -382,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 4fe99b276c..df6aa171db 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"; @@ -60,6 +62,8 @@ export abstract class LoginStrategy { protected abstract cache: BehaviorSubject<LoginStrategyData>; constructor( + protected accountService: AccountService, + protected masterPasswordService: InternalMasterPasswordServiceAbstraction, protected cryptoService: CryptoService, protected apiService: ApiService, protected tokenService: TokenService, 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<LoginStrategyServiceAbstraction>; let cryptoService: MockProxy<CryptoService>; @@ -71,6 +77,9 @@ describe("PasswordLoginStrategy", () => { let tokenResponse: IdentityTokenResponse; beforeEach(async () => { + accountService = mockAccountServiceWith(userId); + masterPasswordService = new FakeMasterPasswordService(); + loginStrategyService = mock<LoginStrategyServiceAbstraction>(); cryptoService = mock<CryptoService>(); apiService = mock<ApiService>(); @@ -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 d3de3ea6ba..52c97d5d85 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"; @@ -70,6 +72,8 @@ export class PasswordLoginStrategy extends LoginStrategy { constructor( data: PasswordLoginStrategyData, + accountService: AccountService, + masterPasswordService: InternalMasterPasswordServiceAbstraction, cryptoService: CryptoService, apiService: ApiService, tokenService: TokenService, @@ -86,6 +90,8 @@ export class PasswordLoginStrategy extends LoginStrategy { billingAccountProfileStateService: BillingAccountProfileStateService, ) { super( + accountService, + masterPasswordService, cryptoService, apiService, tokenService, @@ -157,8 +163,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; } @@ -184,7 +192,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,8 +202,9 @@ 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<void> { @@ -204,7 +214,8 @@ export class PasswordLoginStrategy extends LoginStrategy { } await this.cryptoService.setMasterKeyEncryptedUserKey(response.key); - 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/sso-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts index d4b0b13eaf..bce62681d0 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<CryptoService>; let apiService: MockProxy<ApiService>; let tokenService: MockProxy<TokenService>; @@ -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<CryptoService>(); apiService = mock<ApiService>(); tokenService = mock<TokenService>(); @@ -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(); }); @@ -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 7745104bd1..db0228a338 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"; @@ -79,6 +81,8 @@ export class SsoLoginStrategy extends LoginStrategy { constructor( data: SsoLoginStrategyData, + accountService: AccountService, + masterPasswordService: InternalMasterPasswordServiceAbstraction, cryptoService: CryptoService, apiService: ApiService, tokenService: TokenService, @@ -96,6 +100,8 @@ export class SsoLoginStrategy extends LoginStrategy { billingAccountProfileStateService: BillingAccountProfileStateService, ) { super( + accountService, + masterPasswordService, cryptoService, apiService, tokenService, @@ -138,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({ @@ -323,7 +333,8 @@ export class SsoLoginStrategy extends LoginStrategy { } private async trySetUserKeyWithMasterKey(): Promise<void> { - 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<CryptoService>; let apiService: MockProxy<ApiService>; @@ -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<CryptoService>(); apiService = mock<ApiService>(); tokenService = mock<TokenService>(); @@ -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..421746b49c 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"; @@ -39,6 +41,8 @@ export class UserApiLoginStrategy extends LoginStrategy { constructor( data: UserApiLoginStrategyData, + accountService: AccountService, + masterPasswordService: InternalMasterPasswordServiceAbstraction, cryptoService: CryptoService, apiService: ApiService, tokenService: TokenService, @@ -54,6 +58,8 @@ export class UserApiLoginStrategy extends LoginStrategy { billingAccountProfileStateService: BillingAccountProfileStateService, ) { super( + accountService, + masterPasswordService, cryptoService, apiService, tokenService, @@ -95,7 +101,8 @@ export class UserApiLoginStrategy extends LoginStrategy { 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); 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<CryptoService>; let apiService!: MockProxy<ApiService>; @@ -63,6 +67,9 @@ describe("WebAuthnLoginStrategy", () => { beforeEach(() => { jest.clearAllMocks(); + accountService = new FakeAccountService(null); + masterPasswordService = new FakeMasterPasswordService(); + cryptoService = mock<CryptoService>(); apiService = mock<ApiService>(); tokenService = mock<TokenService>(); @@ -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..843978e2a2 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"; @@ -41,6 +43,8 @@ export class WebAuthnLoginStrategy extends LoginStrategy { constructor( data: WebAuthnLoginStrategyData, + accountService: AccountService, + masterPasswordService: InternalMasterPasswordServiceAbstraction, cryptoService: CryptoService, apiService: ApiService, tokenService: TokenService, @@ -54,6 +58,8 @@ export class WebAuthnLoginStrategy extends LoginStrategy { billingAccountProfileStateService: BillingAccountProfileStateService, ) { super( + accountService, + masterPasswordService, cryptoService, apiService, tokenService, 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 80d00b2a01..f04628ffd9 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,13 +2,15 @@ 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 { 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"; @@ -16,17 +18,27 @@ import { AuthRequestService } from "./auth-request.service"; describe("AuthRequestService", () => { let sut: AuthRequestService; + let accountService: FakeAccountService; + let masterPasswordService: FakeMasterPasswordService; const appIdService = mock<AppIdService>(); const cryptoService = mock<CryptoService>(); const apiService = mock<ApiService>(); - const stateService = mock<StateService>(); 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, + ); mockPrivateKey = new Uint8Array(64); }); @@ -67,8 +79,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, @@ -130,8 +142,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); @@ -144,10 +156,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 eb39659f53..5f8dcfd729 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,12 +1,13 @@ -import { Observable, Subject } from "rxjs"; +import { firstValueFrom, Observable, Subject } 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 { 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 { MasterKey, UserKey } from "@bitwarden/common/types/key"; @@ -19,9 +20,10 @@ export class AuthRequestService implements AuthRequestServiceAbstraction { constructor( private appIdService: AppIdService, + private accountService: AccountService, + private masterPasswordService: InternalMasterPasswordServiceAbstraction, private cryptoService: CryptoService, private apiService: ApiService, - private stateService: StateService, ) { this.authRequestPushNotification$ = this.authRequestPushNotificationSubject.asObservable(); } @@ -38,8 +40,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; @@ -92,8 +95,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); } 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<CryptoService>; let apiService: MockProxy<ApiService>; let tokenService: MockProxy<TokenService>; @@ -61,7 +70,11 @@ describe("LoginStrategyService", () => { let stateProvider: FakeGlobalStateProvider; let loginStrategyCacheExpirationState: FakeGlobalState<Date | null>; + const userId = "USER_ID" as UserId; + beforeEach(() => { + accountService = mockAccountServiceWith(userId); + masterPasswordService = new FakeMasterPasswordService(); cryptoService = mock<CryptoService>(); apiService = mock<ApiService>(); tokenService = mock<TokenService>(); @@ -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 b55f38af7f..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 @@ -9,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"; @@ -81,6 +83,8 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { currentAuthType$: Observable<AuthenticationType | null>; constructor( + protected accountService: AccountService, + protected masterPasswordService: InternalMasterPasswordServiceAbstraction, protected cryptoService: CryptoService, protected apiService: ApiService, protected tokenService: TokenService, @@ -257,7 +261,8 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { ): Promise<AuthRequestResponse> { 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; @@ -266,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), @@ -333,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, @@ -351,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, @@ -370,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, @@ -387,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, @@ -403,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/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<ForceSetPasswordReason>; + /** + * 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<MasterKey>; + /** + * 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<string>; + /** + * 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<EncString>; +} + +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<void>; + /** + * Clear the master key for the user. + * @param userId The user ID. + * @throws If the user ID is missing. + */ + abstract clearMasterKey: (userId: UserId) => Promise<void>; + /** + * 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<void>; + /** + * 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<void>; + + /** + * 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<void>; + /** + * 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<void>; +} diff --git a/libs/common/src/auth/services/key-connector.service.spec.ts b/libs/common/src/auth/services/key-connector.service.spec.ts index 50fed856f9..e3e5fbdbe7 100644 --- a/libs/common/src/auth/services/key-connector.service.spec.ts +++ b/libs/common/src/auth/services/key-connector.service.spec.ts @@ -21,6 +21,7 @@ import { 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", () => { @@ -36,6 +37,7 @@ describe("KeyConnectorService", () => { let stateProvider: FakeStateProvider; let accountService: FakeAccountService; + let masterPasswordService: FakeMasterPasswordService; const mockUserId = Utils.newGuid() as UserId; const mockOrgId = Utils.newGuid() as OrganizationId; @@ -47,10 +49,13 @@ describe("KeyConnectorService", () => { beforeEach(() => { jest.clearAllMocks(); + masterPasswordService = new FakeMasterPasswordService(); accountService = mockAccountServiceWith(mockUserId); stateProvider = new FakeStateProvider(accountService); keyConnectorService = new KeyConnectorService( + accountService, + masterPasswordService, cryptoService, apiService, tokenService, @@ -214,7 +219,10 @@ describe("KeyConnectorService", () => { // Assert expect(apiService.getMasterKeyFromKeyConnector).toHaveBeenCalledWith(url); - expect(cryptoService.setMasterKey).toHaveBeenCalledWith(masterKey); + expect(masterPasswordService.mock.setMasterKey).toHaveBeenCalledWith( + masterKey, + expect.any(String), + ); }); it("should handle errors thrown during the process", async () => { @@ -241,10 +249,10 @@ describe("KeyConnectorService", () => { // 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(cryptoService, "getMasterKey").mockResolvedValue(masterKey); jest.spyOn(apiService, "postUserKeyToKeyConnector").mockResolvedValue(); // Act @@ -252,7 +260,6 @@ describe("KeyConnectorService", () => { // Assert expect(keyConnectorService.getManagingOrganization).toHaveBeenCalled(); - expect(cryptoService.getMasterKey).toHaveBeenCalled(); expect(apiService.postUserKeyToKeyConnector).toHaveBeenCalledWith( organization.keyConnectorUrl, keyConnectorRequest, @@ -268,8 +275,8 @@ describe("KeyConnectorService", () => { 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(cryptoService, "getMasterKey").mockResolvedValue(masterKey); jest.spyOn(apiService, "postUserKeyToKeyConnector").mockRejectedValue(error); jest.spyOn(logService, "error"); @@ -280,7 +287,6 @@ describe("KeyConnectorService", () => { // Assert expect(logService.error).toHaveBeenCalledWith(error); expect(keyConnectorService.getManagingOrganization).toHaveBeenCalled(); - expect(cryptoService.getMasterKey).toHaveBeenCalled(); expect(apiService.postUserKeyToKeyConnector).toHaveBeenCalledWith( organization.keyConnectorUrl, keyConnectorRequest, diff --git a/libs/common/src/auth/services/key-connector.service.ts b/libs/common/src/auth/services/key-connector.service.ts index d1502ce06c..f8e523cce4 100644 --- a/libs/common/src/auth/services/key-connector.service.ts +++ b/libs/common/src/auth/services/key-connector.service.ts @@ -16,7 +16,9 @@ import { 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"; @@ -45,6 +47,8 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { private usesKeyConnectorState: ActiveUserState<boolean>; private convertAccountToKeyConnectorState: ActiveUserState<boolean>; constructor( + private accountService: AccountService, + private masterPasswordService: InternalMasterPasswordServiceAbstraction, private cryptoService: CryptoService, private apiService: ApiService, private tokenService: TokenService, @@ -78,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 { @@ -99,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); } @@ -136,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]); 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<InternalMasterPasswordServiceAbstraction>(); + + // eslint-disable-next-line rxjs/no-exposed-subjects -- test class + masterKeySubject = new ReplaySubject<MasterKey>(1); + // eslint-disable-next-line rxjs/no-exposed-subjects -- test class + masterKeyHashSubject = new ReplaySubject<string>(1); + // eslint-disable-next-line rxjs/no-exposed-subjects -- test class + forceSetPasswordReasonSubject = new ReplaySubject<ForceSetPasswordReason>(1); + + constructor(initialMasterKey?: MasterKey, initialMasterKeyHash?: string) { + this.masterKeySubject.next(initialMasterKey); + this.masterKeyHashSubject.next(initialMasterKeyHash); + } + + masterKey$(userId: UserId): Observable<MasterKey> { + return this.masterKeySubject.asObservable(); + } + + setMasterKey(masterKey: MasterKey, userId: UserId): Promise<void> { + return this.mock.setMasterKey(masterKey, userId); + } + + clearMasterKey(userId: UserId): Promise<void> { + return this.mock.clearMasterKey(userId); + } + + masterKeyHash$(userId: UserId): Observable<string> { + return this.masterKeyHashSubject.asObservable(); + } + + getMasterKeyEncryptedUserKey(userId: UserId): Promise<EncString> { + return this.mock.getMasterKeyEncryptedUserKey(userId); + } + + setMasterKeyEncryptedUserKey(encryptedKey: EncString, userId: UserId): Promise<void> { + return this.mock.setMasterKeyEncryptedUserKey(encryptedKey, userId); + } + + setMasterKeyHash(masterKeyHash: string, userId: UserId): Promise<void> { + return this.mock.setMasterKeyHash(masterKeyHash, userId); + } + + clearMasterKeyHash(userId: UserId): Promise<void> { + return this.mock.clearMasterKeyHash(userId); + } + + forceSetPasswordReason$(userId: UserId): Observable<ForceSetPasswordReason> { + return this.forceSetPasswordReasonSubject.asObservable(); + } + + setForceSetPasswordReason(reason: ForceSetPasswordReason, userId: UserId): Promise<void> { + 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<MasterKey>(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<string>(MASTER_PASSWORD_DISK, "masterKeyHash", { + deserializer: (masterKeyHash) => masterKeyHash, + clearOn: ["logout"], +}); + +/** Disk to persist through lock */ +const MASTER_KEY_ENCRYPTED_USER_KEY = new UserKeyDefinition<EncryptedString>( + MASTER_PASSWORD_DISK, + "masterKeyEncryptedUserKey", + { + deserializer: (key) => key, + clearOn: ["logout"], + }, +); + +/** Disk to persist through lock and account switches */ +const FORCE_SET_PASSWORD_REASON = new UserKeyDefinition<ForceSetPasswordReason>( + MASTER_PASSWORD_DISK, + "forceSetPasswordReason", + { + deserializer: (reason) => reason, + clearOn: ["logout"], + }, +); + +export class MasterPasswordService implements InternalMasterPasswordServiceAbstraction { + constructor(private stateProvider: StateProvider) {} + + masterKey$(userId: UserId): Observable<MasterKey> { + if (userId == null) { + throw new Error("User ID is required."); + } + return this.stateProvider.getUser(userId, MASTER_KEY).state$; + } + + masterKeyHash$(userId: UserId): Observable<string> { + if (userId == null) { + throw new Error("User ID is required."); + } + return this.stateProvider.getUser(userId, MASTER_KEY_HASH).state$; + } + + forceSetPasswordReason$(userId: UserId): Observable<ForceSetPasswordReason> { + 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<EncString> { + 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<void> { + 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<void> { + 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<void> { + 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<void> { + 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<void> { + 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<void> { + 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/user-verification/user-verification.service.ts b/libs/common/src/auth/services/user-verification/user-verification.service.ts index 0b4cd96099..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, @@ -164,7 +170,8 @@ export class UserVerificationService implements UserVerificationServiceAbstracti private async verifyUserByMasterPassword( verification: MasterPasswordVerification, ): Promise<boolean> { - 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<boolean> { + 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/platform/abstractions/crypto.service.ts b/libs/common/src/platform/abstractions/crypto.service.ts index ed451fd896..6609a1014e 100644 --- a/libs/common/src/platform/abstractions/crypto.service.ts +++ b/libs/common/src/platform/abstractions/crypto.service.ts @@ -105,18 +105,6 @@ export abstract class CryptoService { * @param userId The desired user */ abstract setMasterKeyEncryptedUserKey(UserKeyMasterKey: string, userId?: string): Promise<void>; - /** - * Sets the user's master key - * @param key The user's master key to set - * @param userId The desired user - */ - abstract setMasterKey(key: MasterKey, userId?: string): Promise<void>; - /** - * @param userId The desired user - * @returns The user's master key - */ - abstract getMasterKey(userId?: string): Promise<MasterKey>; - /** * @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 @@ -136,11 +124,6 @@ export abstract class CryptoService { kdf: KdfType, KdfConfig: KdfConfig, ): Promise<MasterKey>; - /** - * Clears the user's master key - * @param userId The desired user - */ - abstract clearMasterKey(userId?: string): Promise<void>; /** * Encrypts the existing (or provided) user key with the * provided master key @@ -178,20 +161,6 @@ export abstract class CryptoService { key: MasterKey, hashPurpose?: HashPurpose, ): Promise<string>; - /** - * Sets the user's master password hash - * @param keyHash The user's master password hash to set - */ - abstract setMasterKeyHash(keyHash: string): Promise<void>; - /** - * @returns The user's master password hash - */ - abstract getMasterKeyHash(): Promise<string>; - /** - * Clears the user's stored master password hash - * @param userId The desired user - */ - abstract clearMasterKeyHash(userId?: string): Promise<void>; /** * Compares the provided master password to the stored password hash and server password hash. * Updates the stored hash if outdated. diff --git a/libs/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index 4971481381..227cb43879 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -1,14 +1,12 @@ 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 { UserId } from "../../types/guid"; -import { 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"; @@ -17,7 +15,6 @@ import { KdfType } from "../enums"; 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. @@ -48,22 +45,6 @@ export abstract class StateService<T extends Account = Account> { getAddEditCipherInfo: (options?: StorageOptions) => Promise<AddEditCipherInfo>; setAddEditCipherInfo: (value: AddEditCipherInfo, options?: StorageOptions) => Promise<void>; - /** - * Gets the user's master key - */ - getMasterKey: (options?: StorageOptions) => Promise<MasterKey>; - /** - * Sets the user's master key - */ - setMasterKey: (value: MasterKey, options?: StorageOptions) => Promise<void>; - /** - * Gets the user key encrypted by the master key - */ - getMasterKeyEncryptedUserKey: (options?: StorageOptions) => Promise<string>; - /** - * Sets the user key encrypted by the master key - */ - setMasterKeyEncryptedUserKey: (value: string, options?: StorageOptions) => Promise<void>; /** * Gets the user's auto key */ @@ -108,10 +89,6 @@ export abstract class StateService<T extends Account = Account> { * @deprecated For migration purposes only, use getUserKeyMasterKey instead */ getEncryptedCryptoSymmetricKey: (options?: StorageOptions) => Promise<string>; - /** - * @deprecated For legacy purposes only, use getMasterKey instead - */ - getCryptoMasterKey: (options?: StorageOptions) => Promise<SymmetricCryptoKey>; /** * @deprecated For migration purposes only, use getUserKeyAuto instead */ @@ -189,18 +166,11 @@ export abstract class StateService<T extends Account = Account> { setEncryptedPinProtected: (value: string, options?: StorageOptions) => Promise<void>; getEverBeenUnlocked: (options?: StorageOptions) => Promise<boolean>; setEverBeenUnlocked: (value: boolean, options?: StorageOptions) => Promise<void>; - getForceSetPasswordReason: (options?: StorageOptions) => Promise<ForceSetPasswordReason>; - setForceSetPasswordReason: ( - value: ForceSetPasswordReason, - options?: StorageOptions, - ) => Promise<void>; getIsAuthenticated: (options?: StorageOptions) => Promise<boolean>; getKdfConfig: (options?: StorageOptions) => Promise<KdfConfig>; setKdfConfig: (kdfConfig: KdfConfig, options?: StorageOptions) => Promise<void>; getKdfType: (options?: StorageOptions) => Promise<KdfType>; setKdfType: (value: KdfType, options?: StorageOptions) => Promise<void>; - getKeyHash: (options?: StorageOptions) => Promise<string>; - setKeyHash: (value: string, options?: StorageOptions) => Promise<void>; getLastActive: (options?: StorageOptions) => Promise<number>; setLastActive: (value: number, options?: StorageOptions) => Promise<void>; getLastSync: (options?: StorageOptions) => Promise<string>; 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 4a96da1b48..6bdb08edd5 100644 --- a/libs/common/src/platform/models/domain/account-keys.spec.ts +++ b/libs/common/src/platform/models/domain/account-keys.spec.ts @@ -2,7 +2,6 @@ import { makeStaticByteArray } from "../../../../spec"; import { Utils } from "../../misc/utils"; import { AccountKeys, EncryptionPair } from "./account"; -import { SymmetricCryptoKey } from "./symmetric-crypto-key"; describe("AccountKeys", () => { describe("toJSON", () => { @@ -32,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({ diff --git a/libs/common/src/platform/models/domain/account.ts b/libs/common/src/platform/models/domain/account.ts index 4ed36fd389..753b15c09b 100644 --- a/libs/common/src/platform/models/domain/account.ts +++ b/libs/common/src/platform/models/domain/account.ts @@ -1,7 +1,6 @@ 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 { @@ -10,7 +9,6 @@ import { } from "../../../tools/generator/password"; import { UsernameGeneratorOptions } from "../../../tools/generator/username/username-generation-options"; 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"; @@ -90,12 +88,8 @@ export class AccountData { } export class AccountKeys { - masterKey?: MasterKey; - masterKeyEncryptedUserKey?: string; 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*/ @@ -120,8 +114,6 @@ export class AccountKeys { return null; } return Object.assign(new AccountKeys(), obj, { - masterKey: SymmetricCryptoKey.fromJSON(obj?.masterKey), - cryptoMasterKey: SymmetricCryptoKey.fromJSON(obj?.cryptoMasterKey), cryptoSymmetricKey: EncryptionPair.fromJSON( obj?.cryptoSymmetricKey, SymmetricCryptoKey.fromJSON, @@ -150,10 +142,8 @@ export class AccountProfile { email?: string; emailVerified?: boolean; everBeenUnlocked?: boolean; - forceSetPasswordReason?: ForceSetPasswordReason; lastSync?: string; userId?: string; - keyHash?: string; kdfIterations?: number; kdfMemory?: number; kdfParallelism?: number; diff --git a/libs/common/src/platform/services/crypto.service.spec.ts b/libs/common/src/platform/services/crypto.service.spec.ts index c17d3f97d2..6d0fdb1423 100644 --- a/libs/common/src/platform/services/crypto.service.spec.ts +++ b/libs/common/src/platform/services/crypto.service.spec.ts @@ -5,6 +5,7 @@ import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-a 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"; @@ -41,12 +42,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, @@ -158,14 +162,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 () => { @@ -175,17 +179,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); }); }); @@ -340,9 +344,7 @@ describe("cryptoService", () => { describe("clearKeys", () => { it("resolves active user id when called with no user id", async () => { let callCount = 0; - accountService.activeAccount$ = accountService.activeAccountSubject.pipe( - tap(() => callCount++), - ); + stateProvider.activeUserId$ = stateProvider.activeUserId$.pipe(tap(() => callCount++)); await cryptoService.clearKeys(null); expect(callCount).toBe(1); diff --git a/libs/common/src/platform/services/crypto.service.ts b/libs/common/src/platform/services/crypto.service.ts index df7528b13c..ae588cbc31 100644 --- a/libs/common/src/platform/services/crypto.service.ts +++ b/libs/common/src/platform/services/crypto.service.ts @@ -6,6 +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 { InternalMasterPasswordServiceAbstraction } from "../../auth/abstractions/master-password.service.abstraction"; import { AuthenticationStatus } from "../../auth/enums/authentication-status"; import { KdfConfig } from "../../auth/models/domain/kdf-config"; import { Utils } from "../../platform/misc/utils"; @@ -82,6 +83,7 @@ export class CryptoService implements CryptoServiceAbstraction { readonly everHadUserKey$: Observable<boolean>; constructor( + protected masterPasswordService: InternalMasterPasswordServiceAbstraction, protected keyGenerationService: KeyGenerationService, protected cryptoFunctionService: CryptoFunctionService, protected encryptService: EncryptService, @@ -181,12 +183,16 @@ export class CryptoService implements CryptoServiceAbstraction { } async isLegacyUser(masterKey?: MasterKey, userId?: UserId): Promise<boolean> { - 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<UserKey> { + userId ??= await firstValueFrom(this.stateProvider.activeUserId$); + const userKey = await this.getUserKey(userId); if (userKey) { return userKey; @@ -194,7 +200,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<UserKey> { @@ -233,7 +240,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."); } @@ -277,28 +287,17 @@ export class CryptoService implements CryptoServiceAbstraction { } async setMasterKeyEncryptedUserKey(userKeyMasterKey: string, userId?: UserId): Promise<void> { - await this.stateService.setMasterKeyEncryptedUserKey(userKeyMasterKey, { userId: userId }); - } - - async setMasterKey(key: MasterKey, userId?: UserId): Promise<void> { - await this.stateService.setMasterKey(key, { userId: userId }); - } - - async getMasterKey(userId?: UserId): Promise<MasterKey> { - 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 }), @@ -312,6 +311,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, @@ -327,10 +327,6 @@ export class CryptoService implements CryptoServiceAbstraction { )) as MasterKey; } - async clearMasterKey(userId?: UserId): Promise<void> { - await this.stateService.setMasterKey(null, { userId: userId }); - } - async encryptUserKeyWithMasterKey( masterKey: MasterKey, userKey?: UserKey, @@ -339,32 +335,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<UserKey> { - 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; @@ -383,12 +375,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<string> { - 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."); @@ -399,20 +395,12 @@ export class CryptoService implements CryptoServiceAbstraction { return Utils.fromBufferToB64(hash); } - async setMasterKeyHash(keyHash: string): Promise<void> { - await this.stateService.setKeyHash(keyHash); - } - - async getMasterKeyHash(): Promise<string> { - return await this.stateService.getKeyHash(); - } - - async clearMasterKeyHash(userId?: UserId): Promise<void> { - return await this.stateService.setKeyHash(null, { userId: userId }); - } - + // TODO: move to MasterPasswordService async compareAndUpdateKeyHash(masterPassword: string, masterKey: MasterKey): Promise<boolean> { - 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, @@ -430,7 +418,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; } } @@ -652,14 +640,14 @@ export class CryptoService implements CryptoServiceAbstraction { } async clearKeys(userId?: UserId): Promise<any> { - userId ||= (await firstValueFrom(this.accountService.activeAccount$))?.id; + 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.clearMasterKeyHash(userId); await this.clearOrgKeys(userId); await this.clearProviderKeys(userId); await this.clearKeyPair(userId); @@ -1014,7 +1002,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/state.service.ts b/libs/common/src/platform/services/state.service.ts index a35659a7ac..b3e33cf362 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -5,14 +5,12 @@ 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 { UserId } from "../../types/guid"; -import { 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"; @@ -35,7 +33,6 @@ 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"; @@ -273,65 +270,6 @@ export class StateService< ); } - /** - * @deprecated Do not save the Master Key. Use the User Symmetric Key instead - */ - async getCryptoMasterKey(options?: StorageOptions): Promise<SymmetricCryptoKey> { - 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<MasterKey> { - 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<void> { - 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<string> { - 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<void> { - 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 */ @@ -823,30 +761,6 @@ export class StateService< ); } - async getForceSetPasswordReason(options?: StorageOptions): Promise<ForceSetPasswordReason> { - return ( - ( - await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()), - ) - )?.profile?.forceSetPasswordReason ?? ForceSetPasswordReason.None - ); - } - - async setForceSetPasswordReason( - value: ForceSetPasswordReason, - options?: StorageOptions, - ): Promise<void> { - 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 getIsAuthenticated(options?: StorageOptions): Promise<boolean> { return ( (await this.tokenService.getAccessToken(options?.userId as UserId)) != null && @@ -897,23 +811,6 @@ export class StateService< ); } - async getKeyHash(options?: StorageOptions): Promise<string> { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.profile?.keyHash; - } - - async setKeyHash(value: string, options?: StorageOptions): Promise<void> { - 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<number> { options = this.reconcileOptions(options, await this.defaultOnDiskOptions()); diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index d9265cf10c..10c2f3d36d 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -37,6 +37,8 @@ export const BILLING_DISK = new StateDefinition("billing", "disk"); 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", { 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 f20bc910c0..0594de741c 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<CipherService>; let folderService: MockProxy<FolderService>; let collectionService: MockProxy<CollectionService>; @@ -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<VaultTimeoutAction[]>([]); vaultTimeoutService = new VaultTimeoutService( + accountService, + masterPasswordService, cipherService, folderService, collectionService, @@ -123,6 +135,15 @@ describe("VaultTimeoutService", () => { stateService.activeAccount$ = new BehaviorSubject<string>(globalSetups?.userId); + if (globalSetups?.userId) { + accountService.activeAccountSubject.next({ + id: globalSetups.userId as UserId, + status: accounts[globalSetups.userId]?.authStatus, + email: null, + name: null, + }); + } + platformUtilsService.isViewOpen.mockResolvedValue(globalSetups?.isViewOpen ?? false); vaultTimeoutSettingsService.vaultTimeoutAction$.mockImplementation((userId) => { @@ -156,7 +177,7 @@ describe("VaultTimeoutService", () => { expect(vaultTimeoutSettingsService.availableVaultTimeoutActions$).toHaveBeenCalledWith(userId); expect(stateService.setEverBeenUnlocked).toHaveBeenCalledWith(true, { userId: userId }); expect(stateService.setUserKeyAutoUnlock).toHaveBeenCalledWith(null, { userId: 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 22d658c552..72252036c8 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,7 +88,7 @@ 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(); @@ -92,12 +96,12 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { 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.clearMasterKey(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 faccddb0af..76f0d7fd46 100644 --- a/libs/common/src/state-migrations/migrate.ts +++ b/libs/common/src/state-migrations/migrate.ts @@ -51,6 +51,7 @@ import { RememberedEmailMigrator } from "./migrations/51-move-remembered-email-t 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 { 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"; @@ -58,7 +59,7 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting import { MinVersionMigrator } from "./migrations/min-version"; export const MIN_VERSION = 3; -export const CURRENT_VERSION = 54; +export const CURRENT_VERSION = 55; export type MinVersion = typeof MIN_VERSION; @@ -115,7 +116,8 @@ export function createMigrationBuilder() { .with(RememberedEmailMigrator, 50, 51) .with(DeleteInstalledVersion, 51, 52) .with(DeviceTrustCryptoServiceStateProviderMigrator, 52, 53) - .with(SendMigrator, 53, 54); + .with(SendMigrator, 53, 54) + .with(MoveMasterKeyStateToProviderMigrator, 54, CURRENT_VERSION); } export async function currentVersion( 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<MigrationHelper>; + 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<void> { + const accounts = await helper.getAccounts<ExpectedAccountType>(); + async function migrateAccount(userId: string, account: ExpectedAccountType): Promise<void> { + 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<void> { + const accounts = await helper.getAccounts<ExpectedAccountType>(); + async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise<void> { + 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/vault/services/sync/sync.service.ts b/libs/common/src/vault/services/sync/sync.service.ts index d4601d9621..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, @@ -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, ); } } From 9fb3e9b3ee66bbe1160ab9d513007a9ebbbe57d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa?= <dani-garcia@users.noreply.github.com> Date: Wed, 10 Apr 2024 15:00:25 +0200 Subject: [PATCH 145/351] Fix issues with invalid locale for appx (#8665) --- apps/desktop/electron-builder.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/electron-builder.json b/apps/desktop/electron-builder.json index 5ce5ef948f..dcc4d911ed 100644 --- a/apps/desktop/electron-builder.json +++ b/apps/desktop/electron-builder.json @@ -203,7 +203,7 @@ "si", "sk", "sl", - "sr", + "sr-cyrl", "sv", "te", "th", From 560033cb880e617d649b07734db563d28da0e7ae Mon Sep 17 00:00:00 2001 From: Matt Gibson <mgibson@bitwarden.com> Date: Wed, 10 Apr 2024 08:01:34 -0500 Subject: [PATCH 146/351] Remove usages of `chrome.storage` (#8664) * Enable clearing and retrieving all values from local storage I didn't add these methods to the base abstract class because we don't currently have a use case for them. If we develop one, we can just lift it up. * Use new browser local storage methods for reseed task * Remove the now dangerous methods enabling usage of `chrome.storage` Any direct reference to chrome storage needs to handle serialization tags, which is best dealt with through the classes implementing `AbstractChromeStorageService` --- apps/browser/src/autofill/utils/index.spec.ts | 28 ------ apps/browser/src/autofill/utils/index.ts | 13 --- .../browser/src/background/main.background.ts | 16 +--- .../abstract-chrome-storage-api.service.ts | 26 ++++-- .../chrome-storage-api.service.spec.ts | 10 ++- .../browser-local-storage.service.spec.ts | 89 +++++++++++++++++++ .../services/browser-local-storage.service.ts | 28 ++++++ 7 files changed, 149 insertions(+), 61 deletions(-) create mode 100644 apps/browser/src/platform/services/browser-local-storage.service.spec.ts diff --git a/apps/browser/src/autofill/utils/index.spec.ts b/apps/browser/src/autofill/utils/index.spec.ts index 2fe8496b8d..af67d41601 100644 --- a/apps/browser/src/autofill/utils/index.spec.ts +++ b/apps/browser/src/autofill/utils/index.spec.ts @@ -8,7 +8,6 @@ import { generateRandomCustomElementName, sendExtensionMessage, setElementStyles, - getFromLocalStorage, setupExtensionDisconnectAction, setupAutofillInitDisconnectAction, } from "./index"; @@ -124,33 +123,6 @@ describe("setElementStyles", () => { }); }); -describe("getFromLocalStorage", () => { - it("returns a promise with the storage object pulled from the extension storage api", async () => { - const localStorage: Record<string, any> = { - testValue: "test", - another: "another", - }; - jest.spyOn(chrome.storage.local, "get").mockImplementation((keys, callback) => { - const localStorageObject: Record<string, string> = {}; - - if (typeof keys === "string") { - localStorageObject[keys] = localStorage[keys]; - } else if (Array.isArray(keys)) { - for (const key of keys) { - localStorageObject[key] = localStorage[key]; - } - } - - callback(localStorageObject); - }); - - const returnValue = await getFromLocalStorage("testValue"); - - expect(chrome.storage.local.get).toHaveBeenCalled(); - expect(returnValue).toEqual({ testValue: "test" }); - }); -}); - describe("setupExtensionDisconnectAction", () => { afterEach(() => { jest.clearAllMocks(); diff --git a/apps/browser/src/autofill/utils/index.ts b/apps/browser/src/autofill/utils/index.ts index 2644425d70..72e7f9ab62 100644 --- a/apps/browser/src/autofill/utils/index.ts +++ b/apps/browser/src/autofill/utils/index.ts @@ -106,18 +106,6 @@ function setElementStyles( } } -/** - * Get data from local storage based on the keys provided. - * - * @param keys - String or array of strings of keys to get from local storage - * @deprecated Do not call this, use state-relevant services instead - */ -async function getFromLocalStorage(keys: string | string[]): Promise<Record<string, any>> { - return new Promise((resolve) => { - chrome.storage.local.get(keys, (storage: Record<string, any>) => resolve(storage)); - }); -} - /** * Sets up a long-lived connection with the extension background * and triggers an onDisconnect event if the extension context @@ -278,7 +266,6 @@ export { buildSvgDomElement, sendExtensionMessage, setElementStyles, - getFromLocalStorage, setupExtensionDisconnectAction, setupAutofillInitDisconnectAction, elementIsFillableFormField, diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index f649c5a598..a7bbca9cf0 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -234,7 +234,7 @@ import RuntimeBackground from "./runtime.background"; export default class MainBackground { messagingService: MessagingServiceAbstraction; - storageService: AbstractStorageService & ObservableStorageService; + storageService: BrowserLocalStorageService; secureStorageService: AbstractStorageService; memoryStorageService: AbstractMemoryStorageService; memoryStorageForStateProviders: AbstractMemoryStorageService & ObservableStorageService; @@ -1255,18 +1255,8 @@ export default class MainBackground { return; } - const getStorage = (): Promise<any> => - new Promise((resolve) => { - chrome.storage.local.get(null, (o: any) => resolve(o)); - }); - - const clearStorage = (): Promise<void> => - new Promise((resolve) => { - chrome.storage.local.clear(() => resolve()); - }); - - const storage = await getStorage(); - await clearStorage(); + const storage = await this.storageService.getAll(); + await this.storageService.clear(); for (const key in storage) { // eslint-disable-next-line diff --git a/apps/browser/src/platform/services/abstractions/abstract-chrome-storage-api.service.ts b/apps/browser/src/platform/services/abstractions/abstract-chrome-storage-api.service.ts index a5681d65c0..64935ab591 100644 --- a/apps/browser/src/platform/services/abstractions/abstract-chrome-storage-api.service.ts +++ b/apps/browser/src/platform/services/abstractions/abstract-chrome-storage-api.service.ts @@ -10,6 +10,8 @@ import { fromChromeEvent } from "../../browser/from-chrome-event"; export const serializationIndicator = "__json__"; +type serializedObject = { [serializationIndicator]: true; value: string }; + export const objToStore = (obj: any) => { if (obj == null) { return null; @@ -61,11 +63,7 @@ export default abstract class AbstractChromeStorageService return new Promise((resolve) => { this.chromeStorageApi.get(key, (obj: any) => { if (obj != null && obj[key] != null) { - let value = obj[key]; - if (value[serializationIndicator] && typeof value.value === "string") { - value = JSON.parse(value.value); - } - resolve(value as T); + resolve(this.processGetObject(obj[key])); return; } resolve(null); @@ -95,4 +93,22 @@ export default abstract class AbstractChromeStorageService }); }); } + + /** Backwards compatible resolution of retrieved object with new serialized storage */ + protected processGetObject<T>(obj: T | serializedObject): T | null { + if (this.isSerialized(obj)) { + obj = JSON.parse(obj.value); + } + return obj as T; + } + + /** Type guard for whether an object is tagged as serialized */ + protected isSerialized<T>(value: T | serializedObject): value is serializedObject { + const asSerialized = value as serializedObject; + return ( + asSerialized != null && + asSerialized[serializationIndicator] && + typeof asSerialized.value === "string" + ); + } } diff --git a/apps/browser/src/platform/services/abstractions/chrome-storage-api.service.spec.ts b/apps/browser/src/platform/services/abstractions/chrome-storage-api.service.spec.ts index bb89dc8a6a..812901879d 100644 --- a/apps/browser/src/platform/services/abstractions/chrome-storage-api.service.spec.ts +++ b/apps/browser/src/platform/services/abstractions/chrome-storage-api.service.spec.ts @@ -66,6 +66,7 @@ describe("ChromeStorageApiService", () => { describe("get", () => { let getMock: jest.Mock; + const key = "key"; beforeEach(() => { // setup get @@ -76,7 +77,6 @@ describe("ChromeStorageApiService", () => { }); it("returns a stored value when it is serialized", async () => { - const key = "key"; const value = { key: "value" }; store[key] = objToStore(value); const result = await service.get(key); @@ -84,7 +84,6 @@ describe("ChromeStorageApiService", () => { }); it("returns a stored value when it is not serialized", async () => { - const key = "key"; const value = "value"; store[key] = value; const result = await service.get(key); @@ -95,5 +94,12 @@ describe("ChromeStorageApiService", () => { const result = await service.get("key"); expect(result).toBeNull(); }); + + it("returns null when the stored object is null", async () => { + store[key] = null; + + const result = await service.get(key); + expect(result).toBeNull(); + }); }); }); diff --git a/apps/browser/src/platform/services/browser-local-storage.service.spec.ts b/apps/browser/src/platform/services/browser-local-storage.service.spec.ts new file mode 100644 index 0000000000..37ea37dbf6 --- /dev/null +++ b/apps/browser/src/platform/services/browser-local-storage.service.spec.ts @@ -0,0 +1,89 @@ +import { objToStore } from "./abstractions/abstract-chrome-storage-api.service"; +import BrowserLocalStorageService from "./browser-local-storage.service"; + +describe("BrowserLocalStorageService", () => { + let service: BrowserLocalStorageService; + let store: Record<any, any>; + + beforeEach(() => { + store = {}; + + service = new BrowserLocalStorageService(); + }); + + describe("clear", () => { + let clearMock: jest.Mock; + + beforeEach(() => { + clearMock = chrome.storage.local.clear as jest.Mock; + }); + + it("uses the api to clear", async () => { + await service.clear(); + + expect(clearMock).toHaveBeenCalledTimes(1); + }); + }); + + describe("getAll", () => { + let getMock: jest.Mock; + + beforeEach(() => { + // setup get + getMock = chrome.storage.local.get as jest.Mock; + getMock.mockImplementation((key, callback) => { + if (key == null) { + callback(store); + } else { + callback({ [key]: store[key] }); + } + }); + }); + + it("returns all values", async () => { + store["key1"] = "string"; + store["key2"] = 0; + const result = await service.getAll(); + + expect(result).toEqual(store); + }); + + it("handles empty stores", async () => { + const result = await service.getAll(); + + expect(result).toEqual({}); + }); + + it("handles stores with null values", async () => { + store["key"] = null; + + const result = await service.getAll(); + expect(result).toEqual(store); + }); + + it("handles values processed for storage", async () => { + const obj = { test: 2 }; + const key = "key"; + store[key] = objToStore(obj); + + const result = await service.getAll(); + + expect(result).toEqual({ + [key]: obj, + }); + }); + + // This is a test of backwards compatibility before local storage was serialized. + it("handles values that were stored without processing for storage", async () => { + const obj = { test: 2 }; + const key = "key"; + store[key] = obj; + + const result = await service.getAll(); + + expect(result).toEqual({ + [key]: obj, + }); + }); + }); +}); diff --git a/apps/browser/src/platform/services/browser-local-storage.service.ts b/apps/browser/src/platform/services/browser-local-storage.service.ts index 2efd03a046..e1f9f63676 100644 --- a/apps/browser/src/platform/services/browser-local-storage.service.ts +++ b/apps/browser/src/platform/services/browser-local-storage.service.ts @@ -4,4 +4,32 @@ export default class BrowserLocalStorageService extends AbstractChromeStorageSer constructor() { super(chrome.storage.local); } + + /** + * Clears local storage + */ + async clear() { + await chrome.storage.local.clear(); + } + + /** + * Retrieves all objects stored in local storage. + * + * @remarks This method processes values prior to resolving, do not use `chrome.storage.local` directly + * @returns Promise resolving to keyed object of all stored data + */ + async getAll(): Promise<Record<string, unknown>> { + return new Promise((resolve) => { + this.chromeStorageApi.get(null, (allStorage) => { + const resolved = Object.entries(allStorage).reduce( + (agg, [key, value]) => { + agg[key] = this.processGetObject(value); + return agg; + }, + {} as Record<string, unknown>, + ); + resolve(resolved); + }); + }); + } } From 2bce6c538c30303e113b0e88c3773e95c5fab612 Mon Sep 17 00:00:00 2001 From: SmithThe4th <gsmith@bitwarden.com> Date: Wed, 10 Apr 2024 14:02:46 +0100 Subject: [PATCH 147/351] [PM-6194] Refactor injection of services in browser services module (#8380) * refactored injector of services on the browser service module * refactored the search and popup serach service to use state provider * renamed back to default * removed token service that was readded during merge conflict * Updated search service construction on the cli * updated to use user key definition * Reafctored all components that refernce issearchable * removed commented variable * added uncommited code to remove dependencies not needed anymore * added uncommited code to remove dependencies not needed anymore --- .../browser/src/background/main.background.ts | 4 +- .../search-service.factory.ts | 8 +- .../popup/services/popup-search.service.ts | 15 +- .../src/popup/services/services.module.ts | 36 ++--- .../popup/send/send-groupings.component.ts | 4 +- .../components/action-buttons.component.ts | 4 +- .../popup/components/fido2/fido2.component.ts | 2 +- .../components/vault/current-tab.component.ts | 22 +-- .../vault/vault-filter.component.ts | 36 +++-- .../popup/components/vault/view.component.ts | 4 +- apps/cli/src/bw.ts | 2 +- apps/desktop/src/app/app.component.ts | 2 +- .../common/base.people.component.ts | 45 ++++-- .../organizations/manage/groups.component.ts | 26 ++-- .../organizations/members/people.component.ts | 12 +- apps/web/src/app/app.component.ts | 2 +- .../vault/individual-vault/vault.component.ts | 6 +- .../app/vault/org-vault/vault.component.ts | 8 +- .../providers/clients/clients.component.ts | 62 +++++--- .../providers/manage/people.component.ts | 11 +- .../manage-client-organizations.component.ts | 38 +++-- .../src/services/jslib-services.module.ts | 2 +- libs/angular/src/tools/send/send.component.ts | 34 ++++- .../vault/components/vault-items.component.ts | 30 +++- .../common/src/abstractions/search.service.ts | 12 +- .../src/platform/state/state-definitions.ts | 1 + libs/common/src/services/search.service.ts | 135 +++++++++++++++--- .../vault-timeout/vault-timeout.service.ts | 2 +- libs/common/src/types/guid.ts | 1 + .../src/vault/services/cipher.service.ts | 9 +- 30 files changed, 393 insertions(+), 182 deletions(-) diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index a7bbca9cf0..4ddbf73088 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -514,7 +514,7 @@ export default class MainBackground { this.apiService, this.fileUploadService, ); - this.searchService = new SearchService(this.logService, this.i18nService); + this.searchService = new SearchService(this.logService, this.i18nService, this.stateProvider); this.collectionService = new CollectionService( this.cryptoService, @@ -1177,7 +1177,7 @@ export default class MainBackground { const newActiveUser = await this.stateService.clean({ userId: userId }); if (userId == null || userId === currentUserId) { - this.searchService.clearIndex(); + await this.searchService.clearIndex(); } await this.stateEventRunnerService.handleEvent("logout", currentUserId as UserId); diff --git a/apps/browser/src/background/service-factories/search-service.factory.ts b/apps/browser/src/background/service-factories/search-service.factory.ts index 38c7620b5a..aa83d2afd2 100644 --- a/apps/browser/src/background/service-factories/search-service.factory.ts +++ b/apps/browser/src/background/service-factories/search-service.factory.ts @@ -14,12 +14,17 @@ import { logServiceFactory, LogServiceInitOptions, } from "../../platform/background/service-factories/log-service.factory"; +import { + stateProviderFactory, + StateProviderInitOptions, +} from "../../platform/background/service-factories/state-provider.factory"; type SearchServiceFactoryOptions = FactoryOptions; export type SearchServiceInitOptions = SearchServiceFactoryOptions & LogServiceInitOptions & - I18nServiceInitOptions; + I18nServiceInitOptions & + StateProviderInitOptions; export function searchServiceFactory( cache: { searchService?: AbstractSearchService } & CachedServices, @@ -33,6 +38,7 @@ export function searchServiceFactory( new SearchService( await logServiceFactory(cache, opts), await i18nServiceFactory(cache, opts), + await stateProviderFactory(cache, opts), ), ); } diff --git a/apps/browser/src/popup/services/popup-search.service.ts b/apps/browser/src/popup/services/popup-search.service.ts index bc5e565e6c..40e6fd2d96 100644 --- a/apps/browser/src/popup/services/popup-search.service.ts +++ b/apps/browser/src/popup/services/popup-search.service.ts @@ -1,17 +1,14 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { StateProvider } from "@bitwarden/common/platform/state"; import { SearchService } from "@bitwarden/common/services/search.service"; export class PopupSearchService extends SearchService { - constructor( - private mainSearchService: SearchService, - logService: LogService, - i18nService: I18nService, - ) { - super(logService, i18nService); + constructor(logService: LogService, i18nService: I18nService, stateProvider: StateProvider) { + super(logService, i18nService, stateProvider); } - clearIndex() { + clearIndex(): Promise<void> { throw new Error("Not available."); } @@ -19,7 +16,7 @@ export class PopupSearchService extends SearchService { throw new Error("Not available."); } - getIndexForSearch() { - return this.mainSearchService.getIndexForSearch(); + async getIndexForSearch() { + return await super.getIndexForSearch(); } } diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 037246d3c4..1d42381c1e 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -74,17 +74,15 @@ import { GlobalStateProvider, StateProvider, } from "@bitwarden/common/platform/state"; -import { SearchService } from "@bitwarden/common/services/search.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 { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; -import { CipherFileUploadService } from "@bitwarden/common/vault/abstractions/file-upload/cipher-file-upload.service"; import { FolderService as FolderServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; +import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service"; +import { TotpService } from "@bitwarden/common/vault/services/totp.service"; import { DialogService } from "@bitwarden/components"; -import { VaultExportServiceAbstraction } from "@bitwarden/vault-export-core"; import { UnauthGuardService } from "../../auth/popup/services"; import { AutofillService as AutofillServiceAbstraction } from "../../autofill/services/abstractions/autofill.service"; @@ -187,19 +185,8 @@ const safeProviders: SafeProvider[] = [ }), safeProvider({ provide: SearchServiceAbstraction, - useFactory: (logService: LogService, i18nService: I18nServiceAbstraction) => { - return new PopupSearchService( - getBgService<SearchService>("searchService")(), - logService, - i18nService, - ); - }, - deps: [LogService, I18nServiceAbstraction], - }), - safeProvider({ - provide: CipherFileUploadService, - useFactory: getBgService<CipherFileUploadService>("cipherFileUploadService"), - deps: [], + useClass: PopupSearchService, + deps: [LogService, I18nServiceAbstraction, StateProvider], }), safeProvider({ provide: CipherService, @@ -231,11 +218,6 @@ const safeProviders: SafeProvider[] = [ useClass: BrowserEnvironmentService, deps: [LogService, StateProvider, AccountServiceAbstraction], }), - safeProvider({ - provide: TotpService, - useFactory: getBgService<TotpService>("totpService"), - deps: [], - }), safeProvider({ provide: I18nServiceAbstraction, useFactory: (globalStateProvider: GlobalStateProvider) => { @@ -252,6 +234,11 @@ const safeProviders: SafeProvider[] = [ }, deps: [EncryptService], }), + safeProvider({ + provide: TotpServiceAbstraction, + useClass: TotpService, + deps: [CryptoFunctionService, LogService], + }), safeProvider({ provide: AuthRequestServiceAbstraction, useFactory: getBgService<AuthRequestServiceAbstraction>("authRequestService"), @@ -333,11 +320,6 @@ const safeProviders: SafeProvider[] = [ BillingAccountProfileStateService, ], }), - safeProvider({ - provide: VaultExportServiceAbstraction, - useFactory: getBgService<VaultExportServiceAbstraction>("exportService"), - deps: [], - }), safeProvider({ provide: KeyConnectorService, useFactory: getBgService<KeyConnectorService>("keyConnectorService"), 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 9b3ecc7163..a49773367d 100644 --- a/apps/browser/src/tools/popup/send/send-groupings.component.ts +++ b/apps/browser/src/tools/popup/send/send-groupings.component.ts @@ -171,9 +171,7 @@ 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() { 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/current-tab.component.ts b/apps/browser/src/vault/popup/components/vault/current-tab.component.ts index d9cf6550fa..dd1b6790de 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,7 +1,7 @@ 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 { SearchService } from "@bitwarden/common/abstractions/search.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; @@ -120,8 +120,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 +238,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) { 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 2510e2f966..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"; @@ -53,7 +53,6 @@ export class VaultFilterComponent implements OnInit, OnDestroy { folderCounts = new Map<string, number>(); collectionCounts = new Map<string, number>(); typeCounts = new Map<CipherType, number>(); - 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<void>(); + private _searchText$ = new BehaviorSubject<string>(""); + private isSearchable: boolean = false; + + get searchText() { + return this._searchText$.value; + } + set searchText(value: string) { + this._searchText$.next(value); + } constructor( private i18nService: I18nService, @@ -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) { 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/cli/src/bw.ts b/apps/cli/src/bw.ts index a2e4afe709..fd6552e2f0 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -414,7 +414,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(); diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index b0b411c5f0..257921e2ad 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -609,7 +609,7 @@ 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(); + await this.searchService.clearIndex(); this.authService.logOut(async () => { if (expired) { this.platformUtilsService.showToast( 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<void>; protected allUsers: UserType[] = []; @@ -97,7 +98,19 @@ export abstract class BasePeopleComponent< protected didScroll = false; protected pageSize = 100; + protected destroy$ = new Subject<void>(); + private pagedUsersCount = 0; + private _searchText$ = new BehaviorSubject<string>(""); + 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<void>; abstract confirmUser(user: UserType, publicKey: Uint8Array): Promise<void>; + 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/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<string>(""); private destroy$ = new Subject<void>(); private refreshGroups$ = new BehaviorSubject<void>(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/people.component.ts b/apps/web/src/app/admin-console/organizations/members/people.component.ts index 0da0ab79f0..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"; @@ -73,10 +72,7 @@ import { ResetPasswordComponent } from "./components/reset-password.component"; selector: "app-org-people", templateUrl: "people.component.html", }) -export class PeopleComponent - extends BasePeopleComponent<OrganizationUserView> - implements OnInit, OnDestroy -{ +export class PeopleComponent extends BasePeopleComponent<OrganizationUserView> { @ViewChild("groupsTemplate", { read: ViewContainerRef, static: true }) groupsModalRef: ViewContainerRef; @ViewChild("confirmTemplate", { read: ViewContainerRef, static: true }) @@ -99,7 +95,6 @@ export class PeopleComponent orgResetPasswordPolicyEnabled = false; protected canUseSecretsManager$: Observable<boolean>; - private destroy$ = new Subject<void>(); constructor( apiService: ApiService, @@ -210,8 +205,7 @@ export class PeopleComponent } ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); + super.ngOnDestroy(); } async load() { diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts index 628875f04a..7a3b34969a 100644 --- a/apps/web/src/app/app.component.ts +++ b/apps/web/src/app/app.component.ts @@ -281,7 +281,7 @@ export class AppComponent implements OnDestroy, OnInit { 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/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index 2027b0102b..6fe31f29f4 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -272,7 +272,7 @@ export class VaultComponent implements OnInit, OnDestroy { concatMap(async ([ciphers, filter, searchText]) => { const filterFunction = createFilterFunction(filter); - if (this.searchService.isSearchable(searchText)) { + if (await this.searchService.isSearchable(searchText)) { return await this.searchService.searchCiphers(searchText, [filterFunction], ciphers); } @@ -283,7 +283,7 @@ export class VaultComponent implements OnInit, OnDestroy { const collections$ = combineLatest([nestedCollections$, filter$, this.currentSearchText$]).pipe( filter(([collections, filter]) => collections != undefined && filter != undefined), - map(([collections, filter, searchText]) => { + concatMap(async ([collections, filter, searchText]) => { if (filter.collectionId === undefined || filter.collectionId === Unassigned) { return []; } @@ -303,7 +303,7 @@ export class VaultComponent implements OnInit, OnDestroy { collectionsToReturn = selectedCollection?.children.map((c) => c.node) ?? []; } - if (this.searchService.isSearchable(searchText)) { + if (await this.searchService.isSearchable(searchText)) { collectionsToReturn = this.searchPipe.transform( collectionsToReturn, searchText, diff --git a/apps/web/src/app/vault/org-vault/vault.component.ts b/apps/web/src/app/vault/org-vault/vault.component.ts index cb01951fcc..50d3216150 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.ts +++ b/apps/web/src/app/vault/org-vault/vault.component.ts @@ -331,7 +331,7 @@ export class VaultComponent implements OnInit, OnDestroy { } } - this.searchService.indexCiphers(ciphers, organization.id); + await this.searchService.indexCiphers(ciphers, organization.id); return ciphers; }), ); @@ -350,7 +350,7 @@ export class VaultComponent implements OnInit, OnDestroy { const collections$ = combineLatest([nestedCollections$, filter$, this.currentSearchText$]).pipe( filter(([collections, filter]) => collections != undefined && filter != undefined), - map(([collections, filter, searchText]) => { + concatMap(async ([collections, filter, searchText]) => { if ( filter.collectionId === Unassigned || (filter.collectionId === undefined && filter.type !== undefined) @@ -369,7 +369,7 @@ export class VaultComponent implements OnInit, OnDestroy { collectionsToReturn = selectedCollection?.children.map((c) => c.node) ?? []; } - if (this.searchService.isSearchable(searchText)) { + if (await this.searchService.isSearchable(searchText)) { collectionsToReturn = this.searchPipe.transform( collectionsToReturn, searchText, @@ -436,7 +436,7 @@ export class VaultComponent implements OnInit, OnDestroy { const filterFunction = createFilterFunction(filter); - if (this.searchService.isSearchable(searchText)) { + if (await this.searchService.isSearchable(searchText)) { return await this.searchService.searchCiphers(searchText, [filterFunction], ciphers); } diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts index 72cce7aac3..abdfd6deff 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts @@ -1,7 +1,7 @@ import { Component, 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 { ModalService } from "@bitwarden/angular/services/modal.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -39,7 +39,6 @@ const DisallowedPlanTypes = [ // eslint-disable-next-line rxjs-angular/prefer-takeuntil export class ClientsComponent implements OnInit { providerId: string; - searchText: string; addableOrganizations: Organization[]; loading = true; manageOrganizations = false; @@ -57,6 +56,17 @@ export class ClientsComponent implements OnInit { FeatureFlag.EnableConsolidatedBilling, false, ); + private destroy$ = new Subject<void>(); + private _searchText$ = new BehaviorSubject<string>(""); + private isSearching: boolean = false; + + get searchText() { + return this._searchText$.value; + } + + set searchText(value: string) { + this._searchText$.next(value); + } constructor( private route: ActivatedRoute, @@ -77,27 +87,41 @@ export class ClientsComponent implements OnInit { ) {} async ngOnInit() { - // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe - const enableConsolidatedBilling = await firstValueFrom(this.enableConsolidatedBilling$); if (enableConsolidatedBilling) { await this.router.navigate(["../manage-client-organizations"], { relativeTo: this.route }); } else { - // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe - this.route.parent.params.subscribe(async (params) => { - this.providerId = params.providerId; + this.route.parent.params + .pipe( + switchMap((params) => { + this.providerId = params.providerId; + return from(this.load()); + }), + takeUntil(this.destroy$), + ) + .subscribe(); - 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.route.queryParams.pipe(first(), takeUntil(this.destroy$)).subscribe((qParams) => { + this.searchText = qParams.search; }); + + this._searchText$ + .pipe( + switchMap((searchText) => from(this.searchService.isSearchable(searchText))), + takeUntil(this.destroy$), + ) + .subscribe((isSearchable) => { + this.isSearching = isSearchable; + }); } } + ngOnDestroy() { + 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 : []; @@ -118,20 +142,14 @@ export class ClientsComponent implements OnInit { } 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 this.resetPaging(); } return !searching && this.clients && this.clients.length > this.pageSize; } - isSearching() { - return this.searchService.isSearchable(this.searchText); - } - - async resetPaging() { + resetPaging() { this.pagedClients = []; this.loadMore(); } diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/people.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/people.component.ts index b83daf24b5..8688373ff7 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/people.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/people.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; +import { Component, ViewChild, ViewContainerRef } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; import { first } from "rxjs/operators"; @@ -34,10 +34,7 @@ import { UserAddEditComponent } from "./user-add-edit.component"; templateUrl: "people.component.html", }) // eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class PeopleComponent - extends BasePeopleComponent<ProviderUserUserDetailsResponse> - implements OnInit -{ +export class PeopleComponent extends BasePeopleComponent<ProviderUserUserDetailsResponse> { @ViewChild("addEdit", { read: ViewContainerRef, static: true }) addEditModalRef: ViewContainerRef; @ViewChild("groupsTemplate", { read: ViewContainerRef, static: true }) groupsModalRef: ViewContainerRef; @@ -119,6 +116,10 @@ export class PeopleComponent }); } + ngOnDestroy(): void { + super.ngOnDestroy(); + } + getUsers(): Promise<ListResponse<ProviderUserUserDetailsResponse>> { return this.apiService.getProviderUsers(this.providerId); } 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 index 79dd25e891..a9f341be94 100644 --- 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 @@ -1,8 +1,8 @@ import { SelectionModel } from "@angular/cdk/collections"; -import { Component, OnInit } from "@angular/core"; +import { Component, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute } 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 { ApiService } from "@bitwarden/common/abstractions/api.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; @@ -23,12 +23,22 @@ import { ManageClientOrganizationSubscriptionComponent } from "./manage-client-o }) // eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class ManageClientOrganizationsComponent implements OnInit { +export class ManageClientOrganizationsComponent implements OnInit, OnDestroy { providerId: string; loading = true; manageOrganizations = false; + private destroy$ = new Subject<void>(); + private _searchText$ = new BehaviorSubject<string>(""); + private isSearching: boolean = false; + + get searchText() { + return this._searchText$.value; + } + set searchText(search: string) { + this._searchText$.value; + this.selection.clear(); this.dataSource.filter = search; } @@ -67,6 +77,20 @@ export class ManageClientOrganizationsComponent implements OnInit { 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() { @@ -80,7 +104,7 @@ export class ManageClientOrganizationsComponent implements OnInit { } 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 @@ -89,10 +113,6 @@ export class ManageClientOrganizationsComponent implements OnInit { return !searching && this.clients && this.clients.length > this.pageSize; } - isSearching() { - return this.searchService.isSearchable(this.searchText); - } - async resetPaging() { this.pagedClients = []; this.loadMore(); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index ce60271e27..79bb6714d0 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -726,7 +726,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: SearchServiceAbstraction, useClass: SearchService, - deps: [LogService, I18nServiceAbstraction], + deps: [LogService, I18nServiceAbstraction, StateProvider], }), safeProvider({ provide: NotificationsServiceAbstraction, diff --git a/libs/angular/src/tools/send/send.component.ts b/libs/angular/src/tools/send/send.component.ts index 90d9b39e8c..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, mergeMap, 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<void>(); private _filteredSends: SendView[]; + private _searchText$ = new BehaviorSubject<string>(""); + 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() { @@ -122,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/vault-items.component.ts b/libs/angular/src/vault/components/vault-items.component.ts index cdfb1b6299..458b10865c 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<CipherView>(); @Output() onCipherRightClicked = new EventEmitter<CipherView>(); @@ -23,13 +24,15 @@ export class VaultItemsComponent { protected searchPending = false; + private destroy$ = new Subject<void>(); private searchTimeout: any = null; - private _searchText: string = null; + private isSearchable: boolean = false; + private _searchText$ = new BehaviorSubject<string>(""); get searchText() { - return this._searchText; + return this._searchText$.value; } set searchText(value: string) { - this._searchText = value; + this._searchText$.next(value); } constructor( @@ -37,6 +40,21 @@ 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 { + throw new Error("Method not implemented."); + } + async load(filter: (cipher: CipherView) => boolean = null, deleted = false) { this.deleted = deleted ?? false; await this.applyFilter(filter); @@ -90,7 +108,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/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<IndexedEntityId | null>; + + clearIndex: () => Promise<void>; + isSearchable: (query: string) => Promise<boolean>; + indexCiphers: (ciphersToIndex: CipherView[], indexedEntityGuid?: string) => Promise<void>; searchCiphers: ( query: string, filter?: ((cipher: CipherView) => boolean) | ((cipher: CipherView) => boolean)[], diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index 10c2f3d36d..b6855c5271 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -127,3 +127,4 @@ 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"); diff --git a/libs/common/src/services/search.service.ts b/libs/common/src/services/search.service.ts index 773d51297a..429992b076 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<SerializedLunrIndex>( + VAULT_SEARCH_MEMORY, + "searchIndex", + { + deserializer: (obj: Jsonify<SerializedLunrIndex>) => obj, + clearOn: ["lock"], + }, +); + +/** + * 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<IndexedEntityId>( + VAULT_SEARCH_MEMORY, + "searchIndexedEntityId", + { + deserializer: (obj: Jsonify<IndexedEntityId>) => obj, + clearOn: ["lock"], + }, +); + +/** + * 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<boolean>( + VAULT_SEARCH_MEMORY, + "isIndexing", + { + deserializer: (obj: Jsonify<boolean>) => obj, + clearOn: ["lock"], + }, +); + export class SearchService implements SearchServiceAbstraction { private static registeredPipeline = false; - indexedEntityId?: string = null; - private indexing = false; - private index: lunr.Index = null; + private searchIndexState: ActiveUserState<SerializedLunrIndex> = + this.stateProvider.getActive(LUNR_SEARCH_INDEX); + private readonly index$: Observable<lunr.Index | null> = this.searchIndexState.state$.pipe( + map((searchIndex) => (searchIndex ? lunr.Index.load(searchIndex) : null)), + ); + + private searchIndexEntityIdState: ActiveUserState<IndexedEntityId> = this.stateProvider.getActive( + LUNR_SEARCH_INDEXED_ENTITY_ID, + ); + readonly indexedEntityId$: Observable<IndexedEntityId | null> = + this.searchIndexEntityIdState.state$.pipe(map((id) => id)); + + private searchIsIndexingState: ActiveUserState<boolean> = + this.stateProvider.getActive(LUNR_SEARCH_INDEXING); + private readonly searchIsIndexing$: Observable<boolean> = 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<void> { + await this.searchIndexEntityIdState.update(() => null); + await this.searchIndexState.update(() => null); + await this.searchIsIndexingState.update(() => null); } - isSearchable(query: string): boolean { + async isSearchable(query: string): Promise<boolean> { 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<void> { + 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<lunr.Index | null> { + return await firstValueFrom(this.index$); + } + + private async setIndexForSearch(index: SerializedLunrIndex): Promise<void> { + await this.searchIndexState.update(() => index); + } + + private async setIndexedEntityIdForSearch(indexedEntityId: IndexedEntityId): Promise<void> { + await this.searchIndexEntityIdState.update(() => indexedEntityId); + } + + private async setIsIndexing(indexing: boolean): Promise<void> { + await this.searchIsIndexingState.update(() => indexing); + } + + private async getIsIndexing(): Promise<boolean> { + return await firstValueFrom(this.searchIsIndexing$); } private fieldExtractor(c: CipherView, joined: boolean) { 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 72252036c8..35faf0fcee 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout.service.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout.service.ts @@ -91,7 +91,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { 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(); } diff --git a/libs/common/src/types/guid.ts b/libs/common/src/types/guid.ts index 714f5dffc3..97c87e684e 100644 --- a/libs/common/src/types/guid.ts +++ b/libs/common/src/types/guid.ts @@ -8,3 +8,4 @@ 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/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 4a6e96ead7..7d3772f8c5 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -89,9 +89,9 @@ export class CipherService implements CipherServiceAbstraction { } if (this.searchService != null) { if (value == null) { - this.searchService.clearIndex(); + await this.searchService.clearIndex(); } else { - this.searchService.indexCiphers(value); + await this.searchService.indexCiphers(value); } } } @@ -333,9 +333,10 @@ export class CipherService implements CipherServiceAbstraction { 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.getDecryptedCipherCache(), userId); } } From 84cd01165cda81af088885f261268ed9a45c28a2 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Wed, 10 Apr 2024 08:59:20 -0500 Subject: [PATCH 148/351] Auth `UserKeyDefinition` Migration (#8587) * Migrate DeviceTrustCryptoService * Migrate SsoLoginService * Migrate TokenService * Update libs/common/src/auth/services/token.state.ts Co-authored-by: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> * Fix Test * Actually Fix Tests --------- Co-authored-by: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> --- ...ice-trust-crypto.service.implementation.ts | 8 ++-- .../src/auth/services/sso-login.service.ts | 19 ++++++-- .../common/src/auth/services/token.service.ts | 4 +- .../src/auth/services/token.state.spec.ts | 11 +++-- libs/common/src/auth/services/token.state.ts | 43 +++++++++++++------ 5 files changed, 61 insertions(+), 24 deletions(-) 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 e65c5cd499..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 @@ -14,7 +14,7 @@ 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, KeyDefinition, StateProvider } from "../../platform/state"; +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"; @@ -27,16 +27,18 @@ import { } 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 KeyDefinition<DeviceKey>(DEVICE_TRUST_DISK_LOCAL, "deviceKey", { +export const DEVICE_KEY = new UserKeyDefinition<DeviceKey>(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 KeyDefinition<boolean>( +export const SHOULD_TRUST_DEVICE = new UserKeyDefinition<boolean>( 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 }, ); 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<string>(SSO_DISK, "ssoState", { /** * Uses disk storage so that the organization sso identifier can be persisted across sso redirects. */ -const ORGANIZATION_SSO_IDENTIFIER = new KeyDefinition<string>( +const USER_ORGANIZATION_SSO_IDENTIFIER = new UserKeyDefinition<string>( + 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<string>( 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.ts b/libs/common/src/auth/services/token.service.ts index fb13c21870..db39997663 100644 --- a/libs/common/src/auth/services/token.service.ts +++ b/libs/common/src/auth/services/token.service.ts @@ -15,8 +15,8 @@ import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypt import { GlobalState, GlobalStateProvider, - KeyDefinition, SingleUserStateProvider, + UserKeyDefinition, } from "../../platform/state"; import { UserId } from "../../types/guid"; import { TokenService as TokenServiceAbstraction } from "../abstractions/token.service"; @@ -863,7 +863,7 @@ export class TokenService implements TokenServiceAbstraction { private async getStateValueByUserIdAndKeyDef( userId: UserId, - storageLocation: KeyDefinition<string>, + storageLocation: UserKeyDefinition<string>, ): Promise<string | undefined> { // 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 24eddc73f5..55f97b7e00 100644 --- a/libs/common/src/auth/services/token.state.spec.ts +++ b/libs/common/src/auth/services/token.state.spec.ts @@ -1,4 +1,4 @@ -import { KeyDefinition } from "../../platform/state"; +import { KeyDefinition, UserKeyDefinition } from "../../platform/state"; import { ACCESS_TOKEN_DISK, @@ -28,8 +28,8 @@ describe.each([ "deserializes state key definitions", ( keyDefinition: - | KeyDefinition<string> - | KeyDefinition<boolean> + | UserKeyDefinition<string> + | UserKeyDefinition<boolean> | KeyDefinition<Record<string, string>>, state: string | boolean | Record<string, string>, ) => { @@ -50,7 +50,10 @@ describe.each([ return typeof value === "object" && value !== null && !Array.isArray(value); } - function testDeserialization<T>(keyDefinition: KeyDefinition<T>, state: T) { + function testDeserialization<T>( + keyDefinition: KeyDefinition<T> | UserKeyDefinition<T>, + 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 368f3c4ca2..a8c6878fbb 100644 --- a/libs/common/src/auth/services/token.state.ts +++ b/libs/common/src/auth/services/token.state.ts @@ -1,30 +1,41 @@ -import { KeyDefinition, TOKEN_DISK, TOKEN_DISK_LOCAL, TOKEN_MEMORY } from "../../platform/state"; +import { + KeyDefinition, + TOKEN_DISK, + TOKEN_DISK_LOCAL, + TOKEN_MEMORY, + UserKeyDefinition, +} from "../../platform/state"; // 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 KeyDefinition<string>(TOKEN_DISK, "accessToken", { +export const ACCESS_TOKEN_DISK = new UserKeyDefinition<string>(TOKEN_DISK, "accessToken", { deserializer: (accessToken) => accessToken, + clearOn: [], // Manually handled }); -export const ACCESS_TOKEN_MEMORY = new KeyDefinition<string>(TOKEN_MEMORY, "accessToken", { +export const ACCESS_TOKEN_MEMORY = new UserKeyDefinition<string>(TOKEN_MEMORY, "accessToken", { deserializer: (accessToken) => accessToken, + clearOn: [], // Manually handled }); -export const REFRESH_TOKEN_DISK = new KeyDefinition<string>(TOKEN_DISK, "refreshToken", { +export const REFRESH_TOKEN_DISK = new UserKeyDefinition<string>(TOKEN_DISK, "refreshToken", { deserializer: (refreshToken) => refreshToken, + clearOn: [], // Manually handled }); -export const REFRESH_TOKEN_MEMORY = new KeyDefinition<string>(TOKEN_MEMORY, "refreshToken", { +export const REFRESH_TOKEN_MEMORY = new UserKeyDefinition<string>(TOKEN_MEMORY, "refreshToken", { deserializer: (refreshToken) => refreshToken, + clearOn: [], // Manually handled }); -export const REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE = new KeyDefinition<boolean>( +export const REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE = new UserKeyDefinition<boolean>( TOKEN_DISK, "refreshTokenMigratedToSecureStorage", { deserializer: (refreshTokenMigratedToSecureStorage) => refreshTokenMigratedToSecureStorage, + clearOn: [], // Don't clear on lock/logout so that we always check the correct place (secure storage) for the refresh token if it's been migrated }, ); @@ -36,26 +47,34 @@ export const EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL = KeyDefinition.record<str }, ); -export const API_KEY_CLIENT_ID_DISK = new KeyDefinition<string>(TOKEN_DISK, "apiKeyClientId", { +export const API_KEY_CLIENT_ID_DISK = new UserKeyDefinition<string>(TOKEN_DISK, "apiKeyClientId", { deserializer: (apiKeyClientId) => apiKeyClientId, + clearOn: [], // Manually handled }); -export const API_KEY_CLIENT_ID_MEMORY = new KeyDefinition<string>(TOKEN_MEMORY, "apiKeyClientId", { - deserializer: (apiKeyClientId) => apiKeyClientId, -}); +export const API_KEY_CLIENT_ID_MEMORY = new UserKeyDefinition<string>( + TOKEN_MEMORY, + "apiKeyClientId", + { + deserializer: (apiKeyClientId) => apiKeyClientId, + clearOn: [], // Manually handled + }, +); -export const API_KEY_CLIENT_SECRET_DISK = new KeyDefinition<string>( +export const API_KEY_CLIENT_SECRET_DISK = new UserKeyDefinition<string>( TOKEN_DISK, "apiKeyClientSecret", { deserializer: (apiKeyClientSecret) => apiKeyClientSecret, + clearOn: [], // Manually handled }, ); -export const API_KEY_CLIENT_SECRET_MEMORY = new KeyDefinition<string>( +export const API_KEY_CLIENT_SECRET_MEMORY = new UserKeyDefinition<string>( TOKEN_MEMORY, "apiKeyClientSecret", { deserializer: (apiKeyClientSecret) => apiKeyClientSecret, + clearOn: [], // Manually handled }, ); From 138b24d1230c0092b756d46a4b44146af268d5c5 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Wed, 10 Apr 2024 09:00:00 -0500 Subject: [PATCH 149/351] Migrate `SMOnboardingTasksService` (#8595) --- .../overview/sm-onboarding-tasks.service.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/overview/sm-onboarding-tasks.service.ts b/bitwarden_license/bit-web/src/app/secrets-manager/overview/sm-onboarding-tasks.service.ts index 77a218bdf8..ddca88048f 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/overview/sm-onboarding-tasks.service.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/overview/sm-onboarding-tasks.service.ts @@ -3,16 +3,21 @@ import { Observable, map } from "rxjs"; import { ActiveUserState, - KeyDefinition, SM_ONBOARDING_DISK, StateProvider, + UserKeyDefinition, } from "@bitwarden/common/platform/state"; export type SMOnboardingTasks = Record<string, Record<string, boolean>>; -const SM_ONBOARDING_TASKS_KEY = new KeyDefinition<SMOnboardingTasks>(SM_ONBOARDING_DISK, "tasks", { - deserializer: (b) => b, -}); +const SM_ONBOARDING_TASKS_KEY = new UserKeyDefinition<SMOnboardingTasks>( + 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", From 05f22b9cbc748eefe98514567ca7b0abf12cfa1d Mon Sep 17 00:00:00 2001 From: Cesar Gonzalez <cesar.a.gonzalezcs@gmail.com> Date: Wed, 10 Apr 2024 10:35:17 -0500 Subject: [PATCH 150/351] [PM-7388] Fix AutofillService dependency reference to TotpService within poppup services module (#8668) --- apps/browser/src/popup/services/services.module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 1d42381c1e..39b827827b 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -312,7 +312,7 @@ const safeProviders: SafeProvider[] = [ deps: [ CipherService, AutofillSettingsServiceAbstraction, - TotpService, + TotpServiceAbstraction, EventCollectionServiceAbstraction, LogService, DomainSettingsService, From 744f3a4d1cf9e75b9099462d279e14aa64e331e1 Mon Sep 17 00:00:00 2001 From: Matt Gibson <mgibson@bitwarden.com> Date: Wed, 10 Apr 2024 10:35:44 -0500 Subject: [PATCH 151/351] Use `UserKeyDefinition` for user-scoped data (#8667) --- .../services/key-state/user-key.state.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) 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 abb26b92da..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,17 @@ 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, - UserKeyDefinition, -} 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<boolean>(CRYPTO_DISK, "everHadUserKey", { - deserializer: (obj) => obj, -}); +export const USER_EVER_HAD_USER_KEY = new UserKeyDefinition<boolean>( + CRYPTO_DISK, + "everHadUserKey", + { + deserializer: (obj) => obj, + clearOn: ["logout"], + }, +); export const USER_ENCRYPTED_PRIVATE_KEY = new UserKeyDefinition<EncryptedString>( CRYPTO_DISK, From 1e7329d1ef00e587747ccd7bf8540514d1d7e1d0 Mon Sep 17 00:00:00 2001 From: Cesar Gonzalez <cesar.a.gonzalezcs@gmail.com> Date: Wed, 10 Apr 2024 11:23:02 -0500 Subject: [PATCH 152/351] [PM-7370] Remove usage of `BrowserMessagingPrivateModeBackgroundService` and `MultithreadEncryptService` from manifest v3 (#8654) --- .../browser/src/background/main.background.ts | 31 +++++++++++-------- .../session-sync-observable/session-syncer.ts | 8 +++++ .../src/popup/services/services.module.ts | 13 ++++++-- 3 files changed, 37 insertions(+), 15 deletions(-) diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 4ddbf73088..8db80f6f2b 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -364,9 +364,10 @@ export default class MainBackground { const logoutCallback = async (expired: boolean, userId?: UserId) => await this.logout(expired, userId); - this.messagingService = this.popupOnlyContext - ? new BrowserMessagingPrivateModeBackgroundService() - : new BrowserMessagingService(); + this.messagingService = + this.isPrivateMode && BrowserApi.isManifestVersion(2) + ? new BrowserMessagingPrivateModeBackgroundService() + : new BrowserMessagingService(); this.logService = new ConsoleLogService(false); this.cryptoFunctionService = new WebCryptoFunctionService(self); this.keyGenerationService = new KeyGenerationService(this.cryptoFunctionService); @@ -408,13 +409,14 @@ export default class MainBackground { storageServiceProvider, ); - this.encryptService = flagEnabled("multithreadDecryption") - ? new MultithreadEncryptServiceImplementation( - this.cryptoFunctionService, - this.logService, - true, - ) - : new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, true); + this.encryptService = + flagEnabled("multithreadDecryption") && BrowserApi.isManifestVersion(2) + ? new MultithreadEncryptServiceImplementation( + this.cryptoFunctionService, + this.logService, + true, + ) + : new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, true); this.singleUserStateProvider = new DefaultSingleUserStateProvider( storageServiceProvider, @@ -558,10 +560,13 @@ export default class MainBackground { const backgroundMessagingService = new (class extends MessagingServiceAbstraction { // AuthService should send the messages to the background not popup. send = (subscriber: string, arg: any = {}) => { + if (BrowserApi.isManifestVersion(3)) { + that.messagingService.send(subscriber, arg); + return; + } + const message = Object.assign({}, { command: subscriber }, arg); - // 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 - that.runtimeBackground.processMessage(message, that as any); + void that.runtimeBackground.processMessage(message, that as any); }; })(); diff --git a/apps/browser/src/platform/decorators/session-sync-observable/session-syncer.ts b/apps/browser/src/platform/decorators/session-sync-observable/session-syncer.ts index 692e33bcce..6561d5074c 100644 --- a/apps/browser/src/platform/decorators/session-sync-observable/session-syncer.ts +++ b/apps/browser/src/platform/decorators/session-sync-observable/session-syncer.ts @@ -93,6 +93,10 @@ export class SessionSyncer { } async update(serializedValue: any) { + if (!serializedValue) { + return; + } + const unBuiltValue = JSON.parse(serializedValue); if (!BrowserApi.isManifestVersion(3) && BrowserApi.isBackgroundPage(self)) { await this.memoryStorageService.save(this.metaData.sessionKey, serializedValue); @@ -104,6 +108,10 @@ export class SessionSyncer { } private async updateSession(value: any) { + if (!value) { + return; + } + const serializedValue = JSON.stringify(value); if (BrowserApi.isManifestVersion(3) || BrowserApi.isBackgroundPage(self)) { await this.memoryStorageService.save(this.metaData.sessionKey, serializedValue); diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 39b827827b..40daf1b04d 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -62,6 +62,7 @@ import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/p import { AbstractMemoryStorageService, AbstractStorageService, + ObservableStorageService, } from "@bitwarden/common/platform/abstractions/storage.service"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; @@ -157,7 +158,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: MessagingService, useFactory: () => { - return needsBackgroundInit + return needsBackgroundInit && BrowserApi.isManifestVersion(2) ? new BrowserMessagingPrivateModePopupService() : new BrowserMessagingService(); }, @@ -369,7 +370,15 @@ const safeProviders: SafeProvider[] = [ }), safeProvider({ provide: OBSERVABLE_MEMORY_STORAGE, - useClass: ForegroundMemoryStorageService, + useFactory: () => { + if (BrowserApi.isManifestVersion(2)) { + return new ForegroundMemoryStorageService(); + } + + return getBgService<AbstractStorageService & ObservableStorageService>( + "memoryStorageForStateProviders", + )(); + }, deps: [], }), safeProvider({ From be362988b01b62621249a6a1bb56cd39f93af9f5 Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Thu, 11 Apr 2024 02:52:13 +1000 Subject: [PATCH 153/351] [AC-2436] Show unassigned items banner in web (#8655) * Boostrap basic banner, show for all admins * Remove UI banner, fix method calls * Invert showBanner -> hideBanner * Add api call * Minor tweaks and wording * Change to active user state * Add tests * Fix mixed up names * Simplify logic * Add feature flag * Do not clear on logout * Update apps/web/src/locales/en/messages.json --------- Co-authored-by: Addison Beck <github@addisonbeck.com> --- .../layouts/header/web-header.component.html | 12 ++-- .../layouts/header/web-header.component.ts | 10 +++- ...eb-unassigned-items-banner.service.spec.ts | 56 +++++++++++++++++++ .../web-unassigned-items-banner.service.ts | 46 +++++++++++++++ apps/web/src/locales/en/messages.json | 3 + libs/common/src/abstractions/api.service.ts | 1 + libs/common/src/enums/feature-flag.enum.ts | 1 + .../src/platform/state/state-definitions.ts | 4 ++ libs/common/src/services/api.service.ts | 5 ++ 9 files changed, 131 insertions(+), 7 deletions(-) create mode 100644 apps/web/src/app/layouts/header/web-unassigned-items-banner.service.spec.ts create mode 100644 apps/web/src/app/layouts/header/web-unassigned-items-banner.service.ts 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..1555726e2b 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,18 @@ <bit-banner class="-tw-m-6 tw-flex tw-flex-col tw-pb-6" - (onClose)="webLayoutMigrationBannerService.hideBanner()" - *ngIf="webLayoutMigrationBannerService.showBanner$ | async" + (onClose)="webUnassignedItemsBannerService.hideBanner()" + *ngIf=" + (unassignedItemsBannerEnabled$ | async) && (webUnassignedItemsBannerService.showBanner$ | async) + " > - {{ "newWebApp" | i18n }} + {{ "unassignedItemsBanner" | i18n }} <a - href="https://bitwarden.com/blog/bitwarden-design-updating-the-navigation-in-the-web-app" + href="https://bitwarden.com/help/unassigned-vault-items-moved-to-admin-console" bitLink linkType="contrast" target="_blank" rel="noreferrer" - >{{ "releaseBlog" | i18n }}</a + >{{ "learnMore" | i18n }}</a > </bit-banner> <header diff --git a/apps/web/src/app/layouts/header/web-header.component.ts b/apps/web/src/app/layouts/header/web-header.component.ts index a7f9e184ed..6016463ebb 100644 --- a/apps/web/src/app/layouts/header/web-header.component.ts +++ b/apps/web/src/app/layouts/header/web-header.component.ts @@ -3,13 +3,15 @@ import { ActivatedRoute } from "@angular/router"; import { combineLatest, map, Observable } from "rxjs"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.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 { AccountProfile } from "@bitwarden/common/platform/models/domain/account"; -import { WebLayoutMigrationBannerService } from "./web-layout-migration-banner.service"; +import { WebUnassignedItemsBannerService } from "./web-unassigned-items-banner.service"; @Component({ selector: "app-header", @@ -31,6 +33,9 @@ export class WebHeaderComponent { protected canLock$: Observable<boolean>; protected selfHosted: boolean; protected hostname = location.hostname; + protected unassignedItemsBannerEnabled$ = this.configService.getFeatureFlag$( + FeatureFlag.UnassignedItemsBanner, + ); constructor( private route: ActivatedRoute, @@ -38,7 +43,8 @@ export class WebHeaderComponent { private platformUtilsService: PlatformUtilsService, private vaultTimeoutSettingsService: VaultTimeoutSettingsService, private messagingService: MessagingService, - protected webLayoutMigrationBannerService: WebLayoutMigrationBannerService, + protected webUnassignedItemsBannerService: WebUnassignedItemsBannerService, + private configService: ConfigService, ) { this.routeData$ = this.route.data.pipe( map((params) => { diff --git a/apps/web/src/app/layouts/header/web-unassigned-items-banner.service.spec.ts b/apps/web/src/app/layouts/header/web-unassigned-items-banner.service.spec.ts new file mode 100644 index 0000000000..a9db11a201 --- /dev/null +++ b/apps/web/src/app/layouts/header/web-unassigned-items-banner.service.spec.ts @@ -0,0 +1,56 @@ +import { MockProxy, mock } from "jest-mock-extended"; +import { firstValueFrom, skip } from "rxjs"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { FakeStateProvider, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; + +import { + SHOW_BANNER_KEY, + WebUnassignedItemsBannerService, +} from "./web-unassigned-items-banner.service"; + +describe("WebUnassignedItemsBanner", () => { + let stateProvider: FakeStateProvider; + let apiService: MockProxy<ApiService>; + + const sutFactory = () => new WebUnassignedItemsBannerService(stateProvider, apiService); + + beforeEach(() => { + const fakeAccountService = mockAccountServiceWith("userId" as UserId); + stateProvider = new FakeStateProvider(fakeAccountService); + apiService = mock(); + }); + + 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(); + // skip first value so we get the recomputed value after the server call + expect(await firstValueFrom(sut.showBanner$.pipe(skip(1)))).toBe(true); + // Expect to have updated local state + expect(await firstValueFrom(showBanner.state$)).toBe(true); + expect(apiService.getShowUnassignedCiphersBanner).toHaveBeenCalledTimes(1); + }); +}); diff --git a/apps/web/src/app/layouts/header/web-unassigned-items-banner.service.ts b/apps/web/src/app/layouts/header/web-unassigned-items-banner.service.ts new file mode 100644 index 0000000000..8f09b68547 --- /dev/null +++ b/apps/web/src/app/layouts/header/web-unassigned-items-banner.service.ts @@ -0,0 +1,46 @@ +import { Injectable } from "@angular/core"; +import { EMPTY, concatMap } from "rxjs"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { + StateProvider, + UNASSIGNED_ITEMS_BANNER_DISK, + UserKeyDefinition, +} from "@bitwarden/common/platform/state"; + +export const SHOW_BANNER_KEY = new UserKeyDefinition<boolean>( + 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 WebUnassignedItemsBannerService { + private _showBanner = this.stateProvider.getActive(SHOW_BANNER_KEY); + + showBanner$ = this._showBanner.state$.pipe( + concatMap(async (showBanner) => { + // null indicates that the user has not seen or dismissed the banner yet - get the flag from server + if (showBanner == null) { + const showBannerResponse = await this.apiService.getShowUnassignedCiphersBanner(); + await this._showBanner.update(() => showBannerResponse); + return EMPTY; // complete the inner observable without emitting any value; the update on the previous line will trigger another run + } + + return showBanner; + }), + ); + + constructor( + private stateProvider: StateProvider, + private apiService: ApiService, + ) {} + + async hideBanner() { + await this._showBanner.update(() => false); + } +} diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 307c5be70c..e8944471cc 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -7899,5 +7899,8 @@ }, "machineAccountAccessUpdated": { "message": "Machine account access updated" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/libs/common/src/abstractions/api.service.ts b/libs/common/src/abstractions/api.service.ts index 20ed3216a5..811cca8638 100644 --- a/libs/common/src/abstractions/api.service.ts +++ b/libs/common/src/abstractions/api.service.ts @@ -207,6 +207,7 @@ export abstract class ApiService { emergencyAccessId?: string, ) => Promise<AttachmentResponse>; getCiphersOrganization: (organizationId: string) => Promise<ListResponse<CipherResponse>>; + getShowUnassignedCiphersBanner: () => Promise<boolean>; postCipher: (request: CipherRequest) => Promise<CipherResponse>; postCipherCreate: (request: CipherCreateRequest) => Promise<CipherResponse>; postCipherAdmin: (request: CipherCreateRequest) => Promise<CipherResponse>; diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 9d427034bd..b937e6c462 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -9,6 +9,7 @@ export enum FeatureFlag { ShowPaymentMethodWarningBanners = "show-payment-method-warning-banners", EnableConsolidatedBilling = "enable-consolidated-billing", AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section", + UnassignedItemsBanner = "unassigned-items-banner", } // Replace this with a type safe lookup of the feature flag values in PM-2282 diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index b6855c5271..ed6ef1590d 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -76,6 +76,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", { diff --git a/libs/common/src/services/api.service.ts b/libs/common/src/services/api.service.ts index 6306eb1e28..501b924e5b 100644 --- a/libs/common/src/services/api.service.ts +++ b/libs/common/src/services/api.service.ts @@ -506,6 +506,11 @@ export class ApiService implements ApiServiceAbstraction { return new ListResponse(r, CipherResponse); } + async getShowUnassignedCiphersBanner(): Promise<boolean> { + const r = await this.send("GET", "/ciphers/has-unassigned-ciphers", null, true, true); + return r; + } + async postCipher(request: CipherRequest): Promise<CipherResponse> { const r = await this.send("POST", "/ciphers", request, true, true); return new CipherResponse(r); From 94bfdf2f9ce18d53b4493b952c7e724fb9d9f3d8 Mon Sep 17 00:00:00 2001 From: Jake Fink <jfink@bitwarden.com> Date: Wed, 10 Apr 2024 13:02:59 -0400 Subject: [PATCH 154/351] add snap description (#8672) --- apps/desktop/electron-builder.json | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/desktop/electron-builder.json b/apps/desktop/electron-builder.json index dcc4d911ed..81e88abca8 100644 --- a/apps/desktop/electron-builder.json +++ b/apps/desktop/electron-builder.json @@ -228,6 +228,7 @@ "artifactName": "${productName}-${version}-${arch}.${ext}" }, "snap": { + "summary": "**Installation**\nBitwarden requires access to the `password-manager-service`. Please enable it through permissions or by running `sudo snap connect bitwarden:password-manager-service` after installation.", "autoStart": true, "base": "core22", "confinement": "strict", From 4a3cd24510114429074b0717dd3c2a89d1fd8d2e Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Wed, 10 Apr 2024 10:30:09 -0700 Subject: [PATCH 155/351] instantiate service (#8671) --- apps/browser/src/background/main.background.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 8db80f6f2b..1b9364556e 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -10,6 +10,7 @@ import { AuthRequestServiceAbstraction, AuthRequestService, LoginEmailServiceAbstraction, + LoginEmailService, } from "@bitwarden/auth/common"; import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service"; import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service"; @@ -610,6 +611,8 @@ export default class MainBackground { this.stateProvider, ); + this.loginEmailService = new LoginEmailService(this.stateProvider); + this.loginStrategyService = new LoginStrategyService( this.accountService, this.masterPasswordService, From 4c2afb41216f45492ce7a124eeb2556d08c9c38f Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Wed, 10 Apr 2024 14:10:47 -0400 Subject: [PATCH 156/351] Add tax information to provider setup component when FF is on. (#8616) --- .../billing/shared/tax-info.component.html | 24 +++++++------- .../app/billing/shared/tax-info.component.ts | 31 ++++++++++++++++--- .../providers/providers.module.ts | 3 +- .../providers/setup/setup.component.html | 3 ++ .../providers/setup/setup.component.ts | 28 ++++++++++++++++- .../organization-api.service.abstraction.ts | 4 +-- .../provider/provider-setup.request.ts | 3 ++ .../organization/organization-api.service.ts | 4 +-- ...ts => expanded-tax-info-update.request.ts} | 2 +- .../billing/models/request/payment.request.ts | 4 +-- 10 files changed, 80 insertions(+), 26 deletions(-) rename libs/common/src/billing/models/request/{organization-tax-info-update.request.ts => expanded-tax-info-update.request.ts} (66%) 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 @@ /> </div> </div> - <div - class="col-6" - *ngIf="organizationId && taxInfo.country !== 'US' && countrySupportsTax(taxInfo.country)" - > + <div class="col-6" *ngIf="showTaxIdCheckbox"> <div class="form-group form-check"> <input class="form-check-input" @@ -295,21 +292,22 @@ </div> </div> </div> -<div - class="row" - *ngIf="organizationId && taxInfo.includeTaxId && countrySupportsTax(taxInfo.country)" -> +<div class="row" *ngIf="showTaxIdFields"> <div class="col-6"> <div class="form-group"> <label for="taxId">{{ "taxIdNumber" | i18n }}</label> - <input id="taxId" class="form-control" type="text" name="taxId" [(ngModel)]="taxInfo.taxId" /> + <input + id="taxId" + class="form-control" + type="text" + name="taxId" + [(ngModel)]="taxInfo.taxId" + [required]="taxInfo.includeTaxId" + /> </div> </div> </div> -<div - class="row" - *ngIf="organizationId && taxInfo.includeTaxId && countrySupportsTax(taxInfo.country)" -> +<div class="row" *ngIf="showTaxIdFields"> <div class="col-6"> <div class="form-group"> <label for="addressLine1">{{ "address1" | i18n }}</label> 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/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts index 70ae19f770..81cc7c2919 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts @@ -4,7 +4,7 @@ import { FormsModule } from "@angular/forms"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { SearchModule } from "@bitwarden/components"; -import { OrganizationPlansComponent } from "@bitwarden/web-vault/app/billing"; +import { OrganizationPlansComponent, TaxInfoComponent } from "@bitwarden/web-vault/app/billing"; import { PaymentMethodWarningsModule } from "@bitwarden/web-vault/app/billing/shared"; import { OssModule } from "@bitwarden/web-vault/app/oss.module"; @@ -39,6 +39,7 @@ import { SetupComponent } from "./setup/setup.component"; SearchModule, ProvidersLayoutComponent, PaymentMethodWarningsModule, + TaxInfoComponent, ], declarations: [ AcceptProviderComponent, diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.html index 1e7146bb58..d1cf666874 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.html @@ -25,6 +25,9 @@ required /> </div> + <div *ngIf="enableConsolidatedBilling$ | async" class="form-group col-12"> + <app-tax-info /> + </div> </div> <div class="mt-4"> 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 b3d3112bf5..ed7b42c959 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,9 +1,11 @@ -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 { 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 { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -12,6 +14,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl 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,6 +39,11 @@ export class SetupComponent implements OnInit { false, ); + protected enableConsolidatedBilling$ = this.configService.getFeatureFlag$( + FeatureFlag.EnableConsolidatedBilling, + false, + ); + constructor( private router: Router, private platformUtilsService: PlatformUtilsService, @@ -102,6 +112,22 @@ export class SetupComponent implements OnInit { request.token = this.token; request.key = key; + 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.apiService.postProviderSetup(this.providerId, request); this.platformUtilsService.showToast("success", null, this.i18nService.t("providerSetup")); await this.syncService.fullSync(true); 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 7f1a40d140..66a05cf613 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"; @@ -63,7 +63,7 @@ export class OrganizationApiServiceAbstraction { ) => Promise<ListResponse<OrganizationApiKeyInformationResponse>>; rotateApiKey: (id: string, request: OrganizationApiKeyRequest) => Promise<ApiKeyResponse>; getTaxInfo: (id: string) => Promise<TaxInfoResponse>; - updateTaxInfo: (id: string, request: OrganizationTaxInfoUpdateRequest) => Promise<void>; + updateTaxInfo: (id: string, request: ExpandedTaxInfoUpdateRequest) => Promise<void>; getKeys: (id: string) => Promise<OrganizationKeysResponse>; updateKeys: (id: string, request: OrganizationKeysRequest) => Promise<OrganizationKeysResponse>; getSso: (id: string) => Promise<OrganizationSsoResponse>; 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/services/organization/organization-api.service.ts b/libs/common/src/admin-console/services/organization/organization-api.service.ts index 883bf35260..262232a964 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"; @@ -257,7 +257,7 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction return new TaxInfoResponse(r); } - async updateTaxInfo(id: string, request: OrganizationTaxInfoUpdateRequest): Promise<void> { + async updateTaxInfo(id: string, request: ExpandedTaxInfoUpdateRequest): Promise<void> { // 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/billing/models/request/organization-tax-info-update.request.ts b/libs/common/src/billing/models/request/expanded-tax-info-update.request.ts similarity index 66% rename from libs/common/src/billing/models/request/organization-tax-info-update.request.ts rename to libs/common/src/billing/models/request/expanded-tax-info-update.request.ts index 0f8ec92160..6589b9c1df 100644 --- a/libs/common/src/billing/models/request/organization-tax-info-update.request.ts +++ b/libs/common/src/billing/models/request/expanded-tax-info-update.request.ts @@ -1,6 +1,6 @@ import { TaxInfoUpdateRequest } from "./tax-info-update.request"; -export class OrganizationTaxInfoUpdateRequest extends TaxInfoUpdateRequest { +export class ExpandedTaxInfoUpdateRequest extends TaxInfoUpdateRequest { taxId: string; line1: string; line2: string; diff --git a/libs/common/src/billing/models/request/payment.request.ts b/libs/common/src/billing/models/request/payment.request.ts index d54ca91f62..e73a10bcea 100644 --- a/libs/common/src/billing/models/request/payment.request.ts +++ b/libs/common/src/billing/models/request/payment.request.ts @@ -1,8 +1,8 @@ import { PaymentMethodType } from "../../enums"; -import { OrganizationTaxInfoUpdateRequest } from "./organization-tax-info-update.request"; +import { ExpandedTaxInfoUpdateRequest } from "./expanded-tax-info-update.request"; -export class PaymentRequest extends OrganizationTaxInfoUpdateRequest { +export class PaymentRequest extends ExpandedTaxInfoUpdateRequest { paymentMethodType: PaymentMethodType; paymentToken: string; } From 98ed744ae8986eb00eb780d998d3d0dbfa13efec Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Thu, 11 Apr 2024 05:13:37 +1000 Subject: [PATCH 157/351] [AC-2436] Show unassigned items banner in browser (#8656) * Boostrap basic banner, show for all admins * Remove UI banner, fix method calls * Invert showBanner -> hideBanner * Add api call * Minor tweaks and wording * Change to active user state * Add tests * Fix mixed up names * Simplify logic * Add feature flag * Do not clear on logout * Show banner in browser as well * Update apps/browser/src/_locales/en/messages.json * Update copy --------- Co-authored-by: Addison Beck <github@addisonbeck.com> Co-authored-by: Addison Beck <hello@addisonbeck.com> --- apps/browser/src/_locales/en/messages.json | 3 ++ .../vault/current-tab.component.html | 25 ++++++--- .../components/vault/current-tab.component.ts | 9 ++++ .../unassigned-items-banner.service.spec.ts | 53 +++++++++++++++++++ .../unassigned-items-banner.service.ts | 46 ++++++++++++++++ 5 files changed, 130 insertions(+), 6 deletions(-) create mode 100644 libs/angular/src/services/unassigned-items-banner.service.spec.ts create mode 100644 libs/angular/src/services/unassigned-items-banner.service.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index d802d27700..9d444ce40e 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -3005,5 +3005,8 @@ }, "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." } } 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..1a42f70701 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,32 @@ </div> <ng-container *ngIf="loaded"> <app-vault-select (onVaultSelectionChanged)="load()"></app-vault-select> - <app-callout *ngIf="showHowToAutofill" type="info" title="{{ 'howToAutofill' | i18n }}"> - <p>{{ autofillCalloutText }}</p> + <app-callout + *ngIf=" + (unassignedItemsBannerEnabled$ | async) && + (unassignedItemsBannerService.showBanner$ | async) + " + type="info" + > + <p> + {{ "unassignedItemsBanner" | i18n }} + <a + href="https://bitwarden.com/help/unassigned-vault-items-moved-to-admin-console" + bitLink + linkType="contrast" + target="_blank" + rel="noreferrer" + >{{ "learnMore" | i18n }}</a + > + </p> <button type="button" class="btn primary callout-half" appStopClick - (click)="dismissCallout()" + (click)="unassignedItemsBannerService.hideBanner()" > {{ "gotIt" | i18n }} </button> - <button type="button" class="btn callout-half" appStopClick (click)="goToSettings()"> - {{ "autofillSettings" | i18n }} - </button> </app-callout> <div class="box list" *ngIf="loginCiphers"> <h2 class="box-header"> 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 dd1b6790de..6389be48c3 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 @@ -3,11 +3,14 @@ import { Router } from "@angular/router"; 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"; @@ -54,6 +57,10 @@ export class CurrentTabComponent implements OnInit, OnDestroy { private loadedTimeout: number; private searchTimeout: number; + protected unassignedItemsBannerEnabled$ = this.configService.getFeatureFlag$( + FeatureFlag.UnassignedItemsBanner, + ); + constructor( private platformUtilsService: PlatformUtilsService, private cipherService: CipherService, @@ -70,6 +77,8 @@ export class CurrentTabComponent implements OnInit, OnDestroy { private organizationService: OrganizationService, private vaultFilterService: VaultFilterService, private vaultSettingsService: VaultSettingsService, + private configService: ConfigService, + protected unassignedItemsBannerService: UnassignedItemsBannerService, ) {} async ngOnInit() { 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..eedfbf3429 --- /dev/null +++ b/libs/angular/src/services/unassigned-items-banner.service.spec.ts @@ -0,0 +1,53 @@ +import { MockProxy, mock } from "jest-mock-extended"; +import { firstValueFrom, skip } from "rxjs"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { FakeStateProvider, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; + +import { SHOW_BANNER_KEY, UnassignedItemsBannerService } from "./unassigned-items-banner.service"; + +describe("UnassignedItemsBanner", () => { + let stateProvider: FakeStateProvider; + let apiService: MockProxy<ApiService>; + + const sutFactory = () => new UnassignedItemsBannerService(stateProvider, apiService); + + beforeEach(() => { + const fakeAccountService = mockAccountServiceWith("userId" as UserId); + stateProvider = new FakeStateProvider(fakeAccountService); + apiService = mock(); + }); + + 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(); + // skip first value so we get the recomputed value after the server call + expect(await firstValueFrom(sut.showBanner$.pipe(skip(1)))).toBe(true); + // Expect to have updated local state + expect(await firstValueFrom(showBanner.state$)).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..dd374fe5ce --- /dev/null +++ b/libs/angular/src/services/unassigned-items-banner.service.ts @@ -0,0 +1,46 @@ +import { Injectable } from "@angular/core"; +import { EMPTY, concatMap } from "rxjs"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { + StateProvider, + UNASSIGNED_ITEMS_BANNER_DISK, + UserKeyDefinition, +} from "@bitwarden/common/platform/state"; + +export const SHOW_BANNER_KEY = new UserKeyDefinition<boolean>( + 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 (showBanner) => { + // null indicates that the user has not seen or dismissed the banner yet - get the flag from server + if (showBanner == null) { + const showBannerResponse = await this.apiService.getShowUnassignedCiphersBanner(); + await this._showBanner.update(() => showBannerResponse); + return EMPTY; // complete the inner observable without emitting any value; the update on the previous line will trigger another run + } + + return showBanner; + }), + ); + + constructor( + private stateProvider: StateProvider, + private apiService: ApiService, + ) {} + + async hideBanner() { + await this._showBanner.update(() => false); + } +} From ab83a367dd5e0cdcca73f8881b305ea4cb484985 Mon Sep 17 00:00:00 2001 From: Addison Beck <github@addisonbeck.com> Date: Wed, 10 Apr 2024 16:13:41 -0500 Subject: [PATCH 158/351] Address review feedback on `UnassignedBannerService` (#8680) * Introduce `UnassignedItemsBannerApiService` * Delete `WebUnassignedItemsBannerService` --- .../layouts/header/web-header.component.html | 4 +- .../layouts/header/web-header.component.ts | 5 +- ...eb-unassigned-items-banner.service.spec.ts | 56 ------------------- .../web-unassigned-items-banner.service.ts | 46 --------------- .../unassigned-items-banner.api.service.ts | 19 +++++++ .../unassigned-items-banner.service.spec.ts | 4 +- .../unassigned-items-banner.service.ts | 5 +- libs/common/src/abstractions/api.service.ts | 1 - libs/common/src/services/api.service.ts | 5 -- 9 files changed, 28 insertions(+), 117 deletions(-) delete mode 100644 apps/web/src/app/layouts/header/web-unassigned-items-banner.service.spec.ts delete mode 100644 apps/web/src/app/layouts/header/web-unassigned-items-banner.service.ts create mode 100644 libs/angular/src/services/unassigned-items-banner.api.service.ts 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 1555726e2b..e1cda607c0 100644 --- a/apps/web/src/app/layouts/header/web-header.component.html +++ b/apps/web/src/app/layouts/header/web-header.component.html @@ -1,8 +1,8 @@ <bit-banner class="-tw-m-6 tw-flex tw-flex-col tw-pb-6" - (onClose)="webUnassignedItemsBannerService.hideBanner()" + (onClose)="unassignedItemsBannerService.hideBanner()" *ngIf=" - (unassignedItemsBannerEnabled$ | async) && (webUnassignedItemsBannerService.showBanner$ | async) + (unassignedItemsBannerEnabled$ | async) && (unassignedItemsBannerService.showBanner$ | async) " > {{ "unassignedItemsBanner" | i18n }} diff --git a/apps/web/src/app/layouts/header/web-header.component.ts b/apps/web/src/app/layouts/header/web-header.component.ts index 6016463ebb..1f012e52dd 100644 --- a/apps/web/src/app/layouts/header/web-header.component.ts +++ b/apps/web/src/app/layouts/header/web-header.component.ts @@ -2,6 +2,7 @@ import { Component, Input } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { combineLatest, map, Observable } from "rxjs"; +import { UnassignedItemsBannerService } from "@bitwarden/angular/services/unassigned-items-banner.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; @@ -11,8 +12,6 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { AccountProfile } from "@bitwarden/common/platform/models/domain/account"; -import { WebUnassignedItemsBannerService } from "./web-unassigned-items-banner.service"; - @Component({ selector: "app-header", templateUrl: "./web-header.component.html", @@ -43,7 +42,7 @@ export class WebHeaderComponent { private platformUtilsService: PlatformUtilsService, private vaultTimeoutSettingsService: VaultTimeoutSettingsService, private messagingService: MessagingService, - protected webUnassignedItemsBannerService: WebUnassignedItemsBannerService, + protected unassignedItemsBannerService: UnassignedItemsBannerService, private configService: ConfigService, ) { this.routeData$ = this.route.data.pipe( diff --git a/apps/web/src/app/layouts/header/web-unassigned-items-banner.service.spec.ts b/apps/web/src/app/layouts/header/web-unassigned-items-banner.service.spec.ts deleted file mode 100644 index a9db11a201..0000000000 --- a/apps/web/src/app/layouts/header/web-unassigned-items-banner.service.spec.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { MockProxy, mock } from "jest-mock-extended"; -import { firstValueFrom, skip } from "rxjs"; - -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { FakeStateProvider, mockAccountServiceWith } from "@bitwarden/common/spec"; -import { UserId } from "@bitwarden/common/types/guid"; - -import { - SHOW_BANNER_KEY, - WebUnassignedItemsBannerService, -} from "./web-unassigned-items-banner.service"; - -describe("WebUnassignedItemsBanner", () => { - let stateProvider: FakeStateProvider; - let apiService: MockProxy<ApiService>; - - const sutFactory = () => new WebUnassignedItemsBannerService(stateProvider, apiService); - - beforeEach(() => { - const fakeAccountService = mockAccountServiceWith("userId" as UserId); - stateProvider = new FakeStateProvider(fakeAccountService); - apiService = mock(); - }); - - 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(); - // skip first value so we get the recomputed value after the server call - expect(await firstValueFrom(sut.showBanner$.pipe(skip(1)))).toBe(true); - // Expect to have updated local state - expect(await firstValueFrom(showBanner.state$)).toBe(true); - expect(apiService.getShowUnassignedCiphersBanner).toHaveBeenCalledTimes(1); - }); -}); diff --git a/apps/web/src/app/layouts/header/web-unassigned-items-banner.service.ts b/apps/web/src/app/layouts/header/web-unassigned-items-banner.service.ts deleted file mode 100644 index 8f09b68547..0000000000 --- a/apps/web/src/app/layouts/header/web-unassigned-items-banner.service.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Injectable } from "@angular/core"; -import { EMPTY, concatMap } from "rxjs"; - -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { - StateProvider, - UNASSIGNED_ITEMS_BANNER_DISK, - UserKeyDefinition, -} from "@bitwarden/common/platform/state"; - -export const SHOW_BANNER_KEY = new UserKeyDefinition<boolean>( - 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 WebUnassignedItemsBannerService { - private _showBanner = this.stateProvider.getActive(SHOW_BANNER_KEY); - - showBanner$ = this._showBanner.state$.pipe( - concatMap(async (showBanner) => { - // null indicates that the user has not seen or dismissed the banner yet - get the flag from server - if (showBanner == null) { - const showBannerResponse = await this.apiService.getShowUnassignedCiphersBanner(); - await this._showBanner.update(() => showBannerResponse); - return EMPTY; // complete the inner observable without emitting any value; the update on the previous line will trigger another run - } - - return showBanner; - }), - ); - - constructor( - private stateProvider: StateProvider, - private apiService: ApiService, - ) {} - - async hideBanner() { - await this._showBanner.update(() => false); - } -} 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<boolean> { + 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 index eedfbf3429..ac80f7d651 100644 --- a/libs/angular/src/services/unassigned-items-banner.service.spec.ts +++ b/libs/angular/src/services/unassigned-items-banner.service.spec.ts @@ -1,15 +1,15 @@ import { MockProxy, mock } from "jest-mock-extended"; import { firstValueFrom, skip } from "rxjs"; -import { ApiService } from "@bitwarden/common/abstractions/api.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<ApiService>; + let apiService: MockProxy<UnassignedItemsBannerApiService>; const sutFactory = () => new UnassignedItemsBannerService(stateProvider, apiService); diff --git a/libs/angular/src/services/unassigned-items-banner.service.ts b/libs/angular/src/services/unassigned-items-banner.service.ts index dd374fe5ce..bc567aa44e 100644 --- a/libs/angular/src/services/unassigned-items-banner.service.ts +++ b/libs/angular/src/services/unassigned-items-banner.service.ts @@ -1,13 +1,14 @@ import { Injectable } from "@angular/core"; import { EMPTY, concatMap } from "rxjs"; -import { ApiService } from "@bitwarden/common/abstractions/api.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<boolean>( UNASSIGNED_ITEMS_BANNER_DISK, "showBanner", @@ -37,7 +38,7 @@ export class UnassignedItemsBannerService { constructor( private stateProvider: StateProvider, - private apiService: ApiService, + private apiService: UnassignedItemsBannerApiService, ) {} async hideBanner() { diff --git a/libs/common/src/abstractions/api.service.ts b/libs/common/src/abstractions/api.service.ts index 811cca8638..20ed3216a5 100644 --- a/libs/common/src/abstractions/api.service.ts +++ b/libs/common/src/abstractions/api.service.ts @@ -207,7 +207,6 @@ export abstract class ApiService { emergencyAccessId?: string, ) => Promise<AttachmentResponse>; getCiphersOrganization: (organizationId: string) => Promise<ListResponse<CipherResponse>>; - getShowUnassignedCiphersBanner: () => Promise<boolean>; postCipher: (request: CipherRequest) => Promise<CipherResponse>; postCipherCreate: (request: CipherCreateRequest) => Promise<CipherResponse>; postCipherAdmin: (request: CipherCreateRequest) => Promise<CipherResponse>; diff --git a/libs/common/src/services/api.service.ts b/libs/common/src/services/api.service.ts index 501b924e5b..6306eb1e28 100644 --- a/libs/common/src/services/api.service.ts +++ b/libs/common/src/services/api.service.ts @@ -506,11 +506,6 @@ export class ApiService implements ApiServiceAbstraction { return new ListResponse(r, CipherResponse); } - async getShowUnassignedCiphersBanner(): Promise<boolean> { - const r = await this.send("GET", "/ciphers/has-unassigned-ciphers", null, true, true); - return r; - } - async postCipher(request: CipherRequest): Promise<CipherResponse> { const r = await this.send("POST", "/ciphers", request, true, true); return new CipherResponse(r); From 83fef10c4c7f8861a93570e7bfbb4ad27118696a Mon Sep 17 00:00:00 2001 From: Bitwarden DevOps <106330231+bitwarden-devops-bot@users.noreply.github.com> Date: Wed, 10 Apr 2024 17:31:20 -0400 Subject: [PATCH 159/351] Bumped desktop version to (#8683) --- apps/desktop/package.json | 2 +- apps/desktop/src/package-lock.json | 4 ++-- apps/desktop/src/package.json | 2 +- package-lock.json | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 52dd0fafdb..842bc0015c 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.2", + "version": "2024.4.0", "keywords": [ "bitwarden", "password", diff --git a/apps/desktop/src/package-lock.json b/apps/desktop/src/package-lock.json index 167c32cc81..15a681a564 100644 --- a/apps/desktop/src/package-lock.json +++ b/apps/desktop/src/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bitwarden/desktop", - "version": "2024.3.2", + "version": "2024.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bitwarden/desktop", - "version": "2024.3.2", + "version": "2024.4.0", "license": "GPL-3.0", "dependencies": { "@bitwarden/desktop-native": "file:../desktop_native", diff --git a/apps/desktop/src/package.json b/apps/desktop/src/package.json index cfc0b9b4e2..5e574bd6d5 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.2", + "version": "2024.4.0", "author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)", "homepage": "https://bitwarden.com", "license": "GPL-3.0", diff --git a/package-lock.json b/package-lock.json index d19f27e535..7ef7b7f070 100644 --- a/package-lock.json +++ b/package-lock.json @@ -233,7 +233,7 @@ }, "apps/desktop": { "name": "@bitwarden/desktop", - "version": "2024.3.2", + "version": "2024.4.0", "hasInstallScript": true, "license": "GPL-3.0" }, From 4607ae73c0a2a9606d895f2c4173fa649e5ba13e Mon Sep 17 00:00:00 2001 From: Bitwarden DevOps <106330231+bitwarden-devops-bot@users.noreply.github.com> Date: Wed, 10 Apr 2024 17:45:31 -0400 Subject: [PATCH 160/351] Bumped browser,desktop version to (#8684) --- apps/browser/package.json | 2 +- apps/browser/src/manifest.json | 2 +- apps/browser/src/manifest.v3.json | 2 +- apps/desktop/package.json | 2 +- apps/desktop/src/package-lock.json | 4 ++-- apps/desktop/src/package.json | 2 +- package-lock.json | 4 ++-- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/browser/package.json b/apps/browser/package.json index d06eadf58d..ee6d100572 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/browser", - "version": "2024.3.1", + "version": "2024.4.1", "scripts": { "build": "webpack", "build:mv3": "cross-env MANIFEST_VERSION=3 webpack", diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index 271b2c76a2..aec7523d5e 100644 --- a/apps/browser/src/manifest.json +++ b/apps/browser/src/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "__MSG_extName__", "short_name": "__MSG_appName__", - "version": "2024.3.1", + "version": "2024.4.1", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index e7b0c0cd1e..d67b4affab 100644 --- a/apps/browser/src/manifest.v3.json +++ b/apps/browser/src/manifest.v3.json @@ -3,7 +3,7 @@ "minimum_chrome_version": "102.0", "name": "__MSG_extName__", "short_name": "__MSG_appName__", - "version": "2024.3.1", + "version": "2024.4.1", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 842bc0015c..0dc23b04b1 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.4.0", + "version": "2024.4.1", "keywords": [ "bitwarden", "password", diff --git a/apps/desktop/src/package-lock.json b/apps/desktop/src/package-lock.json index 15a681a564..0531345131 100644 --- a/apps/desktop/src/package-lock.json +++ b/apps/desktop/src/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bitwarden/desktop", - "version": "2024.4.0", + "version": "2024.4.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bitwarden/desktop", - "version": "2024.4.0", + "version": "2024.4.1", "license": "GPL-3.0", "dependencies": { "@bitwarden/desktop-native": "file:../desktop_native", diff --git a/apps/desktop/src/package.json b/apps/desktop/src/package.json index 5e574bd6d5..6527c21521 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.4.0", + "version": "2024.4.1", "author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)", "homepage": "https://bitwarden.com", "license": "GPL-3.0", diff --git a/package-lock.json b/package-lock.json index 7ef7b7f070..c399536cca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -193,7 +193,7 @@ }, "apps/browser": { "name": "@bitwarden/browser", - "version": "2024.3.1" + "version": "2024.4.1" }, "apps/cli": { "name": "@bitwarden/cli", @@ -233,7 +233,7 @@ }, "apps/desktop": { "name": "@bitwarden/desktop", - "version": "2024.4.0", + "version": "2024.4.1", "hasInstallScript": true, "license": "GPL-3.0" }, From e4720de62a3f727f85aaaedf3b3d18c42cf3e3c3 Mon Sep 17 00:00:00 2001 From: Cesar Gonzalez <cesar.a.gonzalezcs@gmail.com> Date: Wed, 10 Apr 2024 17:06:34 -0500 Subject: [PATCH 161/351] [PM-7353] Autofill item selection not working from within current tab view in vault (#8670) * [PM-7353] Fix autofill not working from current tab component * [PM-7353] Fix autofill not working from current tab component * [PM-7353] Fix autofill not working from current tab component --- .../components/vault/current-tab.component.ts | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) 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 6389be48c3..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 @@ -56,6 +56,7 @@ 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, @@ -316,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() { @@ -365,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); + } } From 16c289d680833cd9417209c4108d5d880b7e5407 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Wed, 10 Apr 2024 17:55:20 -0500 Subject: [PATCH 162/351] Prefer Passed In UserId (#8602) --- apps/browser/src/background/main.background.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 1b9364556e..bbcb9f9628 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1188,7 +1188,7 @@ export default class MainBackground { await this.searchService.clearIndex(); } - await this.stateEventRunnerService.handleEvent("logout", currentUserId as UserId); + await this.stateEventRunnerService.handleEvent("logout", userId); if (newActiveUser != null) { // we have a new active user, do not continue tearing down application From 9b022d2fc0b86eb058ea037bcde2faf7c1b15224 Mon Sep 17 00:00:00 2001 From: Joseph Flinn <58369717+joseph-flinn@users.noreply.github.com> Date: Wed, 10 Apr 2024 17:40:53 -0700 Subject: [PATCH 163/351] Decrease snap description character length to reach 128 limit (#8687) --- apps/desktop/electron-builder.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/electron-builder.json b/apps/desktop/electron-builder.json index 81e88abca8..5fd26f32ba 100644 --- a/apps/desktop/electron-builder.json +++ b/apps/desktop/electron-builder.json @@ -228,7 +228,7 @@ "artifactName": "${productName}-${version}-${arch}.${ext}" }, "snap": { - "summary": "**Installation**\nBitwarden requires access to the `password-manager-service`. Please enable it through permissions or by running `sudo snap connect bitwarden:password-manager-service` after installation.", + "summary": "After installation enable required `password-manager-service` by running `sudo snap connect bitwarden:password-manager-service`.", "autoStart": true, "base": "core22", "confinement": "strict", From b843aa6bd1d188b8c1ce450249df22f562721224 Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Thu, 11 Apr 2024 21:32:44 +1000 Subject: [PATCH 164/351] [AC-2436] Fix flashing unassigned items banner (#8689) * Fix flashing banner for users who shouldn't see it * Emit the right value the first time * simplify further * restore comment --- .../services/unassigned-items-banner.service.spec.ts | 8 +++----- .../src/services/unassigned-items-banner.service.ts | 10 +++++----- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/libs/angular/src/services/unassigned-items-banner.service.spec.ts b/libs/angular/src/services/unassigned-items-banner.service.spec.ts index ac80f7d651..9b2ffc1ef9 100644 --- a/libs/angular/src/services/unassigned-items-banner.service.spec.ts +++ b/libs/angular/src/services/unassigned-items-banner.service.spec.ts @@ -1,5 +1,5 @@ import { MockProxy, mock } from "jest-mock-extended"; -import { firstValueFrom, skip } from "rxjs"; +import { firstValueFrom } from "rxjs"; import { FakeStateProvider, mockAccountServiceWith } from "@bitwarden/common/spec"; import { UserId } from "@bitwarden/common/types/guid"; @@ -44,10 +44,8 @@ describe("UnassignedItemsBanner", () => { showBanner.nextState(undefined); const sut = sutFactory(); - // skip first value so we get the recomputed value after the server call - expect(await firstValueFrom(sut.showBanner$.pipe(skip(1)))).toBe(true); - // Expect to have updated local state - expect(await firstValueFrom(showBanner.state$)).toBe(true); + + 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 index bc567aa44e..faa766a18a 100644 --- a/libs/angular/src/services/unassigned-items-banner.service.ts +++ b/libs/angular/src/services/unassigned-items-banner.service.ts @@ -1,5 +1,5 @@ import { Injectable } from "@angular/core"; -import { EMPTY, concatMap } from "rxjs"; +import { concatMap } from "rxjs"; import { StateProvider, @@ -24,15 +24,15 @@ export class UnassignedItemsBannerService { private _showBanner = this.stateProvider.getActive(SHOW_BANNER_KEY); showBanner$ = this._showBanner.state$.pipe( - concatMap(async (showBanner) => { + concatMap(async (showBannerState) => { // null indicates that the user has not seen or dismissed the banner yet - get the flag from server - if (showBanner == null) { + if (showBannerState == null) { const showBannerResponse = await this.apiService.getShowUnassignedCiphersBanner(); await this._showBanner.update(() => showBannerResponse); - return EMPTY; // complete the inner observable without emitting any value; the update on the previous line will trigger another run + return showBannerResponse; } - return showBanner; + return showBannerState; }), ); From c2b91d2d46c0d31aef46559643063765b575a85a Mon Sep 17 00:00:00 2001 From: SmithThe4th <gsmith@bitwarden.com> Date: Thu, 11 Apr 2024 17:53:16 +0100 Subject: [PATCH 165/351] [PM-4700] Fixed issue with clearing search index state (#8686) * fixed issue with clearing search index state * Decrease snap description character length to reach 128 limit (#8687) * clear user index before account is totally cleaned up * [AC-2436] Fix flashing unassigned items banner (#8689) * Fix flashing banner for users who shouldn't see it * Emit the right value the first time * simplify further * restore comment * added logout clear on option * removed redundant clear index from logout --------- Co-authored-by: Joseph Flinn <58369717+joseph-flinn@users.noreply.github.com> Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> --- apps/browser/src/background/main.background.ts | 5 ----- apps/desktop/src/app/app.component.ts | 1 - libs/common/src/services/search.service.ts | 6 +++--- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index bbcb9f9628..0e43f420ab 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1181,13 +1181,8 @@ export default class MainBackground { //Needs to be checked before state is cleaned const needStorageReseed = await this.needsStorageReseed(); - const currentUserId = await this.stateService.getUserId(); const newActiveUser = await this.stateService.clean({ userId: userId }); - if (userId == null || userId === currentUserId) { - await this.searchService.clearIndex(); - } - await this.stateEventRunnerService.handleEvent("logout", userId); if (newActiveUser != null) { diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index 257921e2ad..b2b44e6b21 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -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) { - await this.searchService.clearIndex(); this.authService.logOut(async () => { if (expired) { this.platformUtilsService.showToast( diff --git a/libs/common/src/services/search.service.ts b/libs/common/src/services/search.service.ts index 429992b076..38ddfe0e47 100644 --- a/libs/common/src/services/search.service.ts +++ b/libs/common/src/services/search.service.ts @@ -35,7 +35,7 @@ export const LUNR_SEARCH_INDEX = new UserKeyDefinition<SerializedLunrIndex>( "searchIndex", { deserializer: (obj: Jsonify<SerializedLunrIndex>) => obj, - clearOn: ["lock"], + clearOn: ["lock", "logout"], }, ); @@ -48,7 +48,7 @@ export const LUNR_SEARCH_INDEXED_ENTITY_ID = new UserKeyDefinition<IndexedEntity "searchIndexedEntityId", { deserializer: (obj: Jsonify<IndexedEntityId>) => obj, - clearOn: ["lock"], + clearOn: ["lock", "logout"], }, ); @@ -61,7 +61,7 @@ export const LUNR_SEARCH_INDEXING = new UserKeyDefinition<boolean>( "isIndexing", { deserializer: (obj: Jsonify<boolean>) => obj, - clearOn: ["lock"], + clearOn: ["lock", "logout"], }, ); From 787ad64b737a8c9f77cade966369d1d38e75987b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=A8=20Audrey=20=E2=9C=A8?= <ajensen@bitwarden.com> Date: Thu, 11 Apr 2024 13:32:50 -0400 Subject: [PATCH 166/351] apply password generator policy to all users (#8641) --- .../services/policy/policy.service.spec.ts | 18 ++++++++++++++++++ .../services/policy/policy.service.ts | 3 +++ 2 files changed, 21 insertions(+) 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 a1633d29ff..b67ef4619f 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 @@ -50,6 +50,8 @@ 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), ]); policyService = new PolicyService(stateProvider, organizationService); @@ -254,6 +256,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 0cbc7204de..a093dad61a 100644 --- a/libs/common/src/admin-console/services/policy/policy.service.ts +++ b/libs/common/src/admin-console/services/policy/policy.service.ts @@ -232,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; } From 59392418d1fc72fe2a662846039e63d738c2d1ca Mon Sep 17 00:00:00 2001 From: Kyle Spearrin <kspearrin@users.noreply.github.com> Date: Thu, 11 Apr 2024 14:14:56 -0400 Subject: [PATCH 167/351] [PM-7280] Check command args for disabled updater (#8613) * dont autoupdate on older OS and with args * remove os release checking * use dashes --- apps/desktop/src/main/updater.main.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) 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"; + } } From c7ea35280d66bec969d0af4fbd3c7fa3aa8b25fe Mon Sep 17 00:00:00 2001 From: SmithThe4th <gsmith@bitwarden.com> Date: Thu, 11 Apr 2024 19:42:42 +0100 Subject: [PATCH 168/351] Fix for not implemented ngOnDestroy method in vault items component (#8708) --- libs/angular/src/vault/components/vault-items.component.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libs/angular/src/vault/components/vault-items.component.ts b/libs/angular/src/vault/components/vault-items.component.ts index 458b10865c..20e779e77c 100644 --- a/libs/angular/src/vault/components/vault-items.component.ts +++ b/libs/angular/src/vault/components/vault-items.component.ts @@ -52,7 +52,8 @@ export class VaultItemsComponent implements OnInit, OnDestroy { } ngOnDestroy(): void { - throw new Error("Method not implemented."); + this.destroy$.next(); + this.destroy$.complete(); } async load(filter: (cipher: CipherView) => boolean = null, deleted = false) { From 8d698d9d84ee1a959fed1cd593fd144e7f859bbc Mon Sep 17 00:00:00 2001 From: Matt Gibson <mgibson@bitwarden.com> Date: Fri, 12 Apr 2024 02:25:45 -0500 Subject: [PATCH 169/351] [PM-7169][PM-5267] Remove auth status from account info (#8539) * remove active account unlocked from state service * Remove status from account service `AccountInfo` * Fixup lingering usages of status Fixup missed factories * Fixup account info usage * fixup CLI build * Fixup current account type * Add helper for all auth statuses to auth service * Fix tests * Uncomment mistakenly commented code * Rework logged out account exclusion tests * Correct test description * Avoid getters returning observables * fixup type --- .../account-switcher.component.ts | 17 +-- .../current-account.component.ts | 7 +- .../services/account-switcher.service.spec.ts | 69 ++++++++--- .../services/account-switcher.service.ts | 21 ++-- .../browser/src/background/main.background.ts | 4 +- .../event-collection-service.factory.ts | 10 +- .../event-upload-service.factory.ts | 10 +- .../src/platform/popup/header.component.ts | 16 ++- apps/browser/src/popup/app.component.ts | 6 +- apps/cli/src/bw.ts | 4 +- libs/angular/src/auth/guards/unauth.guard.ts | 11 +- .../src/services/jslib-services.module.ts | 4 +- .../user-decryption-options.service.spec.ts | 2 - libs/common/spec/fake-account-service.ts | 14 +-- .../src/auth/abstractions/account.service.ts | 24 +--- .../src/auth/abstractions/auth.service.ts | 2 + .../src/auth/services/account.service.spec.ts | 109 +++--------------- .../src/auth/services/account.service.ts | 31 ----- .../src/auth/services/auth.service.spec.ts | 19 +++ libs/common/src/auth/services/auth.service.ts | 21 ++++ ...-enrollment.service.implementation.spec.ts | 2 - .../platform/abstractions/state.service.ts | 4 - .../platform/services/crypto.service.spec.ts | 27 ----- .../src/platform/services/crypto.service.ts | 6 +- .../default-environment.service.spec.ts | 4 - .../src/platform/services/state.service.ts | 26 +---- .../default-active-user-state.spec.ts | 2 - .../event/event-collection.service.ts | 10 +- .../services/event/event-upload.service.ts | 19 +-- .../vault-timeout.service.spec.ts | 1 - .../tools/send/services/send.service.spec.ts | 2 - 31 files changed, 200 insertions(+), 304 deletions(-) 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<Record<UserId, AccountInfo>>(null); - const activeAccountSubject = new BehaviorSubject<{ id: UserId } & AccountInfo>(null); + let accountsSubject: BehaviorSubject<Record<UserId, AccountInfo>>; + let activeAccountSubject: BehaviorSubject<{ id: UserId } & AccountInfo>; + let authStatusSubject: ReplaySubject<Record<UserId, AuthenticationStatus>>; const accountService = mock<AccountService>(); const avatarService = mock<AvatarService>(); const messagingService = mock<MessagingService>(); const environmentService = mock<EnvironmentService>(); const logService = mock<LogService>(); + const authService = mock<AuthService>(); let accountSwitcherService: AccountSwitcherService; beforeEach(() => { jest.resetAllMocks(); + accountsSubject = new BehaviorSubject<Record<UserId, AccountInfo>>(null); + activeAccountSubject = new BehaviorSubject<{ id: UserId } & AccountInfo>(null); + authStatusSubject = new ReplaySubject<Record<UserId, AuthenticationStatus>>(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<UserId, AccountInfo>); - - 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<UserId, AccountInfo> = {}; + const seedStatuses: Record<UserId, AuthenticationStatus> = {}; 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<UserId, AccountInfo>); + + 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/background/main.background.ts b/apps/browser/src/background/main.background.ts index 0e43f420ab..4aecf8f585 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -779,14 +779,14 @@ export default class MainBackground { this.apiService, this.stateProvider, this.logService, - this.accountService, + this.authService, ); this.eventCollectionService = new EventCollectionService( this.cipherService, this.stateProvider, this.organizationService, this.eventUploadService, - this.accountService, + this.authService, ); this.totpService = new TotpService(this.cryptoFunctionService, this.logService); diff --git a/apps/browser/src/background/service-factories/event-collection-service.factory.ts b/apps/browser/src/background/service-factories/event-collection-service.factory.ts index ec892c73dd..b8f89c90bd 100644 --- a/apps/browser/src/background/service-factories/event-collection-service.factory.ts +++ b/apps/browser/src/background/service-factories/event-collection-service.factory.ts @@ -5,7 +5,10 @@ import { organizationServiceFactory, OrganizationServiceInitOptions, } from "../../admin-console/background/service-factories/organization-service.factory"; -import { accountServiceFactory } from "../../auth/background/service-factories/account-service.factory"; +import { + authServiceFactory, + AuthServiceInitOptions, +} from "../../auth/background/service-factories/auth-service.factory"; import { FactoryOptions, CachedServices, @@ -29,7 +32,8 @@ export type EventCollectionServiceInitOptions = EventCollectionServiceOptions & CipherServiceInitOptions & StateServiceInitOptions & OrganizationServiceInitOptions & - EventUploadServiceInitOptions; + EventUploadServiceInitOptions & + AuthServiceInitOptions; export function eventCollectionServiceFactory( cache: { eventCollectionService?: AbstractEventCollectionService } & CachedServices, @@ -45,7 +49,7 @@ export function eventCollectionServiceFactory( await stateProviderFactory(cache, opts), await organizationServiceFactory(cache, opts), await eventUploadServiceFactory(cache, opts), - await accountServiceFactory(cache, opts), + await authServiceFactory(cache, opts), ), ); } diff --git a/apps/browser/src/background/service-factories/event-upload-service.factory.ts b/apps/browser/src/background/service-factories/event-upload-service.factory.ts index 4e1d7949be..b20310e8c9 100644 --- a/apps/browser/src/background/service-factories/event-upload-service.factory.ts +++ b/apps/browser/src/background/service-factories/event-upload-service.factory.ts @@ -1,7 +1,10 @@ import { EventUploadService as AbstractEventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service"; import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service"; -import { accountServiceFactory } from "../../auth/background/service-factories/account-service.factory"; +import { + AuthServiceInitOptions, + authServiceFactory, +} from "../../auth/background/service-factories/auth-service.factory"; import { ApiServiceInitOptions, apiServiceFactory, @@ -23,7 +26,8 @@ type EventUploadServiceOptions = FactoryOptions; export type EventUploadServiceInitOptions = EventUploadServiceOptions & ApiServiceInitOptions & StateServiceInitOptions & - LogServiceInitOptions; + LogServiceInitOptions & + AuthServiceInitOptions; export function eventUploadServiceFactory( cache: { eventUploadService?: AbstractEventUploadService } & CachedServices, @@ -38,7 +42,7 @@ export function eventUploadServiceFactory( await apiServiceFactory(cache, opts), await stateProviderFactory(cache, opts), await logServiceFactory(cache, opts), - await accountServiceFactory(cache, opts), + await authServiceFactory(cache, opts), ), ); } diff --git a/apps/browser/src/platform/popup/header.component.ts b/apps/browser/src/platform/popup/header.component.ts index 6b9e9c9a3e..ebda12c2a4 100644 --- a/apps/browser/src/platform/popup/header.component.ts +++ b/apps/browser/src/platform/popup/header.component.ts @@ -1,8 +1,10 @@ import { Component, Input } from "@angular/core"; -import { Observable, map } from "rxjs"; +import { Observable, combineLatest, map, of, switchMap } 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"; +import { UserId } from "@bitwarden/common/types/guid"; import { enableAccountSwitching } from "../flags"; @@ -14,14 +16,18 @@ export class HeaderComponent { @Input() noTheme = false; @Input() hideAccountSwitcher = false; authedAccounts$: Observable<boolean>; - constructor(accountService: AccountService) { + constructor(accountService: AccountService, authService: AuthService) { this.authedAccounts$ = accountService.accounts$.pipe( - map((accounts) => { + switchMap((accounts) => { if (!enableAccountSwitching()) { - return false; + return of(false); } - return Object.values(accounts).some((a) => a.status !== AuthenticationStatus.LoggedOut); + return combineLatest( + Object.keys(accounts).map((id) => authService.authStatusFor$(id as UserId)), + ).pipe( + map((statuses) => statuses.some((status) => status !== AuthenticationStatus.LoggedOut)), + ); }), ); } diff --git a/apps/browser/src/popup/app.component.ts b/apps/browser/src/popup/app.component.ts index 03ac1612f1..b0fdaec4fc 100644 --- a/apps/browser/src/popup/app.component.ts +++ b/apps/browser/src/popup/app.component.ts @@ -1,9 +1,10 @@ import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core"; import { NavigationEnd, Router, RouterOutlet } from "@angular/router"; import { ToastrService } from "ngx-toastr"; -import { filter, concatMap, Subject, takeUntil, firstValueFrom } from "rxjs"; +import { filter, concatMap, Subject, takeUntil, firstValueFrom, map } from "rxjs"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { DialogService, SimpleDialogOptions } from "@bitwarden/components"; @@ -57,8 +58,9 @@ export class AppComponent implements OnInit, OnDestroy { this.activeUserId = userId; }); - this.stateService.activeAccountUnlocked$ + this.authService.activeAccountStatus$ .pipe( + map((status) => status === AuthenticationStatus.Unlocked), filter((unlocked) => unlocked), concatMap(async () => { await this.recordActivity(); diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index fd6552e2f0..ed8c52ed94 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -678,7 +678,7 @@ export class Main { this.apiService, this.stateProvider, this.logService, - this.accountService, + this.authService, ); this.eventCollectionService = new EventCollectionService( @@ -686,7 +686,7 @@ export class Main { this.stateProvider, this.organizationService, this.eventUploadService, - this.accountService, + this.authService, ); } 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<boolean | UrlTree> { - 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/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 79bb6714d0..3de36020da 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -756,7 +756,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: EventUploadServiceAbstraction, useClass: EventUploadService, - deps: [ApiServiceAbstraction, StateProvider, LogService, AccountServiceAbstraction], + deps: [ApiServiceAbstraction, StateProvider, LogService, AuthServiceAbstraction], }), safeProvider({ provide: EventCollectionServiceAbstraction, @@ -766,7 +766,7 @@ const safeProviders: SafeProvider[] = [ StateProvider, OrganizationServiceAbstraction, EventUploadServiceAbstraction, - AccountServiceAbstraction, + AuthServiceAbstraction, ], }), safeProvider({ 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/common/spec/fake-account-service.ts b/libs/common/spec/fake-account-service.ts index bd35d901c2..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 }); @@ -34,8 +32,6 @@ export class FakeAccountService implements AccountService { } accounts$ = this.accountsSubject.asObservable(); activeAccount$ = this.activeAccountSubject.asObservable(); - accountLock$: Observable<UserId>; - accountLogout$: Observable<UserId>; constructor(initialData: Record<UserId, AccountInfo>) { this.accountsSubject.next(initialData); @@ -57,14 +53,6 @@ export class FakeAccountService implements AccountService { await this.mock.setAccountEmail(userId, email); } - async setAccountStatus(userId: UserId, status: AuthenticationStatus): Promise<void> { - await this.mock.setAccountStatus(userId, status); - } - - async setMaxAccountStatus(userId: UserId, maxStatus: AuthenticationStatus): Promise<void> { - await this.mock.setMaxAccountStatus(userId, maxStatus); - } - async switchAccount(userId: UserId): Promise<void> { const next = userId == null ? null : { id: userId, ...this.accountsSubject["_buffer"]?.[0]?.[userId] }; 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<Record<UserId, AccountInfo>>; activeAccount$: Observable<{ id: UserId | undefined } & AccountInfo>; - accountLock$: Observable<UserId>; - accountLogout$: Observable<UserId>; /** * 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<void>; - /** - * 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<void>; - /** - * 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<void>; /** * Updates the `activeAccount$` observable with the new active account. * @param userId diff --git a/libs/common/src/auth/abstractions/auth.service.ts b/libs/common/src/auth/abstractions/auth.service.ts index de08dbd4e9..36d5d219b2 100644 --- a/libs/common/src/auth/abstractions/auth.service.ts +++ b/libs/common/src/auth/abstractions/auth.service.ts @@ -6,6 +6,8 @@ import { AuthenticationStatus } from "../enums/authentication-status"; export abstract class AuthService { /** Authentication status for the active user */ abstract activeAccountStatus$: Observable<AuthenticationStatus>; + /** Authentication status for all known users */ + abstract authStatuses$: Observable<Record<UserId, AuthenticationStatus>>; /** * Returns an observable authentication status for the given user id. * @note userId is a required parameter, null values will always return `AuthenticationStatus.LoggedOut` 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<Record<UserId, AccountInfo>>; let activeAccountIdState: FakeGlobalState<UserId>; 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<AccountInfo, UserId>( 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<void> { - 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<void> { - 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<void> { await this.activeAccountIdState.update( (_, accounts) => { diff --git a/libs/common/src/auth/services/auth.service.spec.ts b/libs/common/src/auth/services/auth.service.spec.ts index 07e38def4b..3bdf85d3e1 100644 --- a/libs/common/src/auth/services/auth.service.spec.ts +++ b/libs/common/src/auth/services/auth.service.spec.ts @@ -122,6 +122,25 @@ describe("AuthService", () => { }); }); + 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)); diff --git a/libs/common/src/auth/services/auth.service.ts b/libs/common/src/auth/services/auth.service.ts index de5eb66c06..7a29d313e7 100644 --- a/libs/common/src/auth/services/auth.service.ts +++ b/libs/common/src/auth/services/auth.service.ts @@ -21,6 +21,7 @@ import { AuthenticationStatus } from "../enums/authentication-status"; export class AuthService implements AuthServiceAbstraction { activeAccountStatus$: Observable<AuthenticationStatus>; + authStatuses$: Observable<Record<UserId, AuthenticationStatus>>; constructor( protected accountService: AccountService, @@ -36,6 +37,26 @@ export class AuthService implements AuthServiceAbstraction { 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<UserId, AuthenticationStatus>, + ); + }), + ); } authStatusFor$(userId: UserId): Observable<AuthenticationStatus> { 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/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index 227cb43879..3017ae7195 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -33,10 +33,6 @@ export type InitOptions = { export abstract class StateService<T extends Account = Account> { accounts$: Observable<{ [userId: string]: T }>; activeAccount$: Observable<string>; - /** - * @deprecated use accountService.activeAccount$ instead - */ - activeAccountUnlocked$: Observable<boolean>; addAccount: (account: T) => Promise<void>; setActiveUser: (userId: string) => Promise<void>; diff --git a/libs/common/src/platform/services/crypto.service.spec.ts b/libs/common/src/platform/services/crypto.service.spec.ts index 6d0fdb1423..16e6d4aa63 100644 --- a/libs/common/src/platform/services/crypto.service.spec.ts +++ b/libs/common/src/platform/services/crypto.service.spec.ts @@ -4,7 +4,6 @@ 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"; @@ -273,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 = @@ -353,23 +343,6 @@ describe("cryptoService", () => { accountService.activeAccount$ = accountService.activeAccountSubject.asObservable(); }); - it("sets the maximum account status of the active user id to locked when user id is not specified", async () => { - await cryptoService.clearKeys(); - 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 () => { - const userId = "someOtherUser" as UserId; - await cryptoService.clearKeys(userId); - expect(accountService.mock.setMaxAccountStatus).toHaveBeenCalledWith( - userId, - AuthenticationStatus.Locked, - ); - }); - describe.each([ USER_ENCRYPTED_ORGANIZATION_KEYS, USER_ENCRYPTED_PROVIDER_KEYS, diff --git a/libs/common/src/platform/services/crypto.service.ts b/libs/common/src/platform/services/crypto.service.ts index ae588cbc31..c091b6a5a9 100644 --- a/libs/common/src/platform/services/crypto.service.ts +++ b/libs/common/src/platform/services/crypto.service.ts @@ -7,7 +7,6 @@ import { ProfileProviderOrganizationResponse } from "../../admin-console/models/ import { ProfileProviderResponse } from "../../admin-console/models/response/profile-provider.response"; import { AccountService } from "../../auth/abstractions/account.service"; import { InternalMasterPasswordServiceAbstraction } from "../../auth/abstractions/master-password.service.abstraction"; -import { AuthenticationStatus } from "../../auth/enums/authentication-status"; import { KdfConfig } from "../../auth/models/domain/kdf-config"; import { Utils } from "../../platform/misc/utils"; import { CsprngArray } from "../../types/csprng"; @@ -152,8 +151,6 @@ export class CryptoService implements CryptoServiceAbstraction { [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); } @@ -256,14 +253,13 @@ export class CryptoService implements CryptoServiceAbstraction { * Clears the user key. Clears all stored versions of the user keys as well, such as the biometrics key * @param userId The desired user */ - async clearUserKey(userId: UserId): Promise<void> { + private async clearUserKey(userId: UserId): Promise<void> { 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.accountService.setMaxAccountStatus(userId, AuthenticationStatus.Locked); await this.clearAllStoredUserKeys(userId); } 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 a70ab3d179..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,7 +2,6 @@ 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"; @@ -32,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); @@ -50,7 +47,6 @@ describe("EnvironmentService", () => { id: userId, email: "test@example.com", name: `Test Name ${userId}`, - status: AuthenticationStatus.Unlocked, }); await awaitAsync(); }; diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index b3e33cf362..7dbac2b02a 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -1,9 +1,8 @@ -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 { KdfConfig } from "../../auth/models/domain/kdf-config"; import { BiometricKey } from "../../auth/types/biometric-key"; @@ -68,8 +67,6 @@ export class StateService< protected activeAccountSubject = new BehaviorSubject<string | null>(null); activeAccount$ = this.activeAccountSubject.asObservable(); - activeAccountUnlocked$: Observable<boolean>; - private hasBeenInited = false; protected isRecoveredSession = false; @@ -89,13 +86,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<void> { // Deconstruct and apply defaults @@ -151,7 +142,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); @@ -177,16 +167,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, }); @@ -206,7 +187,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, }); @@ -1406,8 +1386,6 @@ 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 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/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/vault-timeout/vault-timeout.service.spec.ts b/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts index 0594de741c..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 @@ -138,7 +138,6 @@ describe("VaultTimeoutService", () => { if (globalSetups?.userId) { accountService.activeAccountSubject.next({ id: globalSetups.userId as UserId, - status: accounts[globalSetups.userId]?.authStatus, email: null, name: null, }); 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 fc793dba67..41183c42af 100644 --- a/libs/common/src/tools/send/services/send.service.spec.ts +++ b/libs/common/src/tools/send/services/send.service.spec.ts @@ -8,7 +8,6 @@ import { awaitAsync, mockAccountServiceWith, } from "../../../../spec"; -import { AuthenticationStatus } from "../../../auth/enums/authentication-status"; import { CryptoService } from "../../../platform/abstractions/crypto.service"; import { EncryptService } from "../../../platform/abstractions/encrypt.service"; import { I18nService } from "../../../platform/abstractions/i18n.service"; @@ -64,7 +63,6 @@ describe("SendService", () => { id: mockUserId, email: "email", name: "name", - status: AuthenticationStatus.Unlocked, }); // Initial encrypted state From bf11b90c43902ac150eef4e35dbc68a9f7f16273 Mon Sep 17 00:00:00 2001 From: Addison Beck <github@addisonbeck.com> Date: Fri, 12 Apr 2024 06:38:53 -0500 Subject: [PATCH 170/351] Use `UserVerificationDialogComponent` for account recovery enrollment (#8632) --- ...nroll-master-password-reset.component.html | 19 ---- .../enroll-master-password-reset.component.ts | 106 ++++++++---------- .../users/organization-user.module.ts | 14 --- apps/web/src/app/oss.module.ts | 2 - .../organization-options.component.ts | 13 ++- 5 files changed, 60 insertions(+), 94 deletions(-) delete mode 100644 apps/web/src/app/admin-console/organizations/users/enroll-master-password-reset.component.html delete mode 100644 apps/web/src/app/admin-console/organizations/users/organization-user.module.ts diff --git a/apps/web/src/app/admin-console/organizations/users/enroll-master-password-reset.component.html b/apps/web/src/app/admin-console/organizations/users/enroll-master-password-reset.component.html deleted file mode 100644 index 613e2a7a92..0000000000 --- a/apps/web/src/app/admin-console/organizations/users/enroll-master-password-reset.component.html +++ /dev/null @@ -1,19 +0,0 @@ -<form [formGroup]="formGroup" [bitSubmit]="submit"> - <bit-dialog> - <span bitDialogTitle>{{ "enrollAccountRecovery" | i18n }}</span> - <div bitDialogContent> - <bit-callout type="warning"> - {{ "resetPasswordEnrollmentWarning" | i18n }} - </bit-callout> - <app-user-verification formControlName="verification"></app-user-verification> - </div> - <ng-container bitDialogFooter> - <button bitButton buttonType="primary" bitFormButton type="submit"> - {{ "submit" | i18n }} - </button> - <button type="button" bitButton buttonType="secondary" bitDialogClose> - {{ "cancel" | i18n }} - </button> - </ng-container> - </bit-dialog> -</form> 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<Verification>(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/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/vault/individual-vault/vault-filter/components/organization-options.component.ts b/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts index fa81abdc54..8dd63e62dd 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts @@ -16,6 +16,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { DialogService } from "@bitwarden/components"; +import { OrganizationUserResetPasswordService } from "../../../../admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service"; import { EnrollMasterPasswordReset } from "../../../../admin-console/organizations/users/enroll-master-password-reset.component"; import { OptionsInput } from "../shared/components/vault-filter-section.component"; import { OrganizationFilter } from "../shared/models/vault-filter.type"; @@ -46,6 +47,7 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy { private organizationUserService: OrganizationUserService, private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, private dialogService: DialogService, + private resetPasswordService: OrganizationUserResetPasswordService, ) {} async ngOnInit() { @@ -144,7 +146,16 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy { async toggleResetPasswordEnrollment(org: Organization) { if (!this.organization.resetPasswordEnrolled) { - EnrollMasterPasswordReset.open(this.dialogService, { organization: org }); + await EnrollMasterPasswordReset.open( + this.dialogService, + { organization: org }, + this.resetPasswordService, + this.organizationUserService, + this.platformUtilsService, + this.i18nService, + this.syncService, + this.logService, + ); } else { // Remove reset password const request = new OrganizationUserResetPasswordEnrollmentRequest(); From d026087bfdbc78e63e3624149947c64c6f297759 Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Fri, 12 Apr 2024 21:57:17 +1000 Subject: [PATCH 171/351] [AC-2443] Update unassigned items banner text for self-hosted (#8719) * Update banner text for self-hosted environments * Fix tests * Fix web vault wording * Actually fix web vault wording --- apps/browser/src/_locales/en/messages.json | 3 +++ .../components/vault/current-tab.component.html | 5 +++-- .../app/layouts/header/web-header.component.html | 6 ++++-- apps/web/src/locales/en/messages.json | 3 +++ .../unassigned-items-banner.service.spec.ts | 9 +++++++-- .../services/unassigned-items-banner.service.ts | 15 ++++++++++++++- 6 files changed, 34 insertions(+), 7 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 9d444ce40e..4108db3996 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -3008,5 +3008,8 @@ }, "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/vault/popup/components/vault/current-tab.component.html b/apps/browser/src/vault/popup/components/vault/current-tab.component.html index 1a42f70701..fc8b4212ba 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 @@ -39,12 +39,13 @@ <app-callout *ngIf=" (unassignedItemsBannerEnabled$ | async) && - (unassignedItemsBannerService.showBanner$ | async) + (unassignedItemsBannerService.showBanner$ | async) && + (unassignedItemsBannerService.bannerText$ | async) " type="info" > <p> - {{ "unassignedItemsBanner" | i18n }} + {{ unassignedItemsBannerService.bannerText$ | async | i18n }} <a href="https://bitwarden.com/help/unassigned-vault-items-moved-to-admin-console" bitLink 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 e1cda607c0..9346763a47 100644 --- a/apps/web/src/app/layouts/header/web-header.component.html +++ b/apps/web/src/app/layouts/header/web-header.component.html @@ -2,10 +2,12 @@ class="-tw-m-6 tw-flex tw-flex-col tw-pb-6" (onClose)="unassignedItemsBannerService.hideBanner()" *ngIf=" - (unassignedItemsBannerEnabled$ | async) && (unassignedItemsBannerService.showBanner$ | async) + (unassignedItemsBannerEnabled$ | async) && + (unassignedItemsBannerService.showBanner$ | async) && + (unassignedItemsBannerService.bannerText$ | async) " > - {{ "unassignedItemsBanner" | i18n }} + {{ unassignedItemsBannerService.bannerText$ | async | i18n }} <a href="https://bitwarden.com/help/unassigned-vault-items-moved-to-admin-console" bitLink diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index e8944471cc..f28bff066a 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now 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 your All Vaults view across devices 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/libs/angular/src/services/unassigned-items-banner.service.spec.ts b/libs/angular/src/services/unassigned-items-banner.service.spec.ts index 9b2ffc1ef9..ca2487a518 100644 --- a/libs/angular/src/services/unassigned-items-banner.service.spec.ts +++ b/libs/angular/src/services/unassigned-items-banner.service.spec.ts @@ -1,6 +1,7 @@ import { MockProxy, mock } from "jest-mock-extended"; -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, of } from "rxjs"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { FakeStateProvider, mockAccountServiceWith } from "@bitwarden/common/spec"; import { UserId } from "@bitwarden/common/types/guid"; @@ -10,13 +11,17 @@ import { SHOW_BANNER_KEY, UnassignedItemsBannerService } from "./unassigned-item describe("UnassignedItemsBanner", () => { let stateProvider: FakeStateProvider; let apiService: MockProxy<UnassignedItemsBannerApiService>; + let environmentService: MockProxy<EnvironmentService>; - const sutFactory = () => new UnassignedItemsBannerService(stateProvider, apiService); + const sutFactory = () => + new UnassignedItemsBannerService(stateProvider, apiService, environmentService); beforeEach(() => { const fakeAccountService = mockAccountServiceWith("userId" as UserId); stateProvider = new FakeStateProvider(fakeAccountService); apiService = mock(); + environmentService = mock(); + environmentService.environment$ = of(null); }); it("shows the banner if showBanner local state is true", async () => { diff --git a/libs/angular/src/services/unassigned-items-banner.service.ts b/libs/angular/src/services/unassigned-items-banner.service.ts index faa766a18a..13a745fb82 100644 --- a/libs/angular/src/services/unassigned-items-banner.service.ts +++ b/libs/angular/src/services/unassigned-items-banner.service.ts @@ -1,6 +1,10 @@ import { Injectable } from "@angular/core"; -import { concatMap } from "rxjs"; +import { concatMap, map } from "rxjs"; +import { + EnvironmentService, + Region, +} from "@bitwarden/common/platform/abstractions/environment.service"; import { StateProvider, UNASSIGNED_ITEMS_BANNER_DISK, @@ -36,9 +40,18 @@ export class UnassignedItemsBannerService { }), ); + bannerText$ = this.environmentService.environment$.pipe( + map((e) => + e?.getRegion() == Region.SelfHosted + ? "unassignedItemsBannerSelfHost" + : "unassignedItemsBanner", + ), + ); + constructor( private stateProvider: StateProvider, private apiService: UnassignedItemsBannerApiService, + private environmentService: EnvironmentService, ) {} async hideBanner() { From a12c7242d624085c807a0fee236e0288db3ecb77 Mon Sep 17 00:00:00 2001 From: SmithThe4th <gsmith@bitwarden.com> Date: Fri, 12 Apr 2024 14:53:26 +0100 Subject: [PATCH 172/351] load collections after ngOninit has run (#8691) --- apps/desktop/src/vault/app/vault/add-edit.component.ts | 1 + libs/angular/src/vault/components/add-edit.component.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) 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 b89beebaa6..86e0b881ee 100644 --- a/apps/desktop/src/vault/app/vault/add-edit.component.ts +++ b/apps/desktop/src/vault/app/vault/add-edit.component.ts @@ -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/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts index 6a0cfde350..ab09d14c86 100644 --- a/libs/angular/src/vault/components/add-edit.component.ts +++ b/libs/angular/src/vault/components/add-edit.component.ts @@ -592,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); From b914260705901ff32783e3eebb5ac8dab4e93a92 Mon Sep 17 00:00:00 2001 From: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com> Date: Fri, 12 Apr 2024 10:17:38 -0400 Subject: [PATCH 173/351] [AC-2319] remove the owned by business checkbox business name (#8674) * Removed business name from organization create/upgrade flows, and org info page * Prefilling the logged in user's email to the billing email when creating an organization --- .../organization-information.component.html | 15 ------------- .../organization-information.component.ts | 21 +++++++++++++++++-- .../settings/account.component.html | 4 ---- .../settings/account.component.ts | 7 ------- .../organization-plans.component.ts | 7 ------- 5 files changed, 19 insertions(+), 35 deletions(-) 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 @@ <input bitInput type="email" formControlName="clientOwnerEmail" /> </bit-form-field> </div> - <div *ngIf="!isProvider && !acceptingSponsorship"> - <input - type="checkbox" - name="businessOwned" - formControlName="businessOwned" - (change)="changedBusinessOwned.emit()" - /> - <bit-label for="businessOwned" class="tw-mb-3">{{ "accountOwnedBusiness" | i18n }}</bit-label> - <div class="tw-mt-4" *ngIf="formGroup.controls['businessOwned'].value"> - <bit-form-field class="tw-w-1/2"> - <bit-label>{{ "businessName" | i18n }}</bit-label> - <input bitInput type="text" formControlName="businessName" /> - </bit-form-field> - </div> - </div> </form> 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<void>(); + + constructor(private accountService: AccountService) {} + + async ngOnInit(): Promise<void> { + 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/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 @@ <bit-label>{{ "billingEmail" | i18n }}</bit-label> <input bitInput id="billingEmail" formControlName="billingEmail" type="email" /> </bit-form-field> - <bit-form-field> - <bit-label>{{ "businessName" | i18n }}</bit-label> - <input bitInput id="businessName" formControlName="businessName" type="text" /> - </bit-form-field> </div> <div> <bit-avatar [text]="org.name" [id]="org.id" size="large"></bit-avatar> 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 b218e680e3..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 @@ -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({ @@ -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/billing/organizations/organization-plans.component.ts b/apps/web/src/app/billing/organizations/organization-plans.component.ts index 17eddbd33d..f2fb296522 100644 --- a/apps/web/src/app/billing/organizations/organization-plans.component.ts +++ b/apps/web/src/app/billing/organizations/organization-plans.component.ts @@ -120,7 +120,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, @@ -596,9 +595,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 = @@ -656,9 +652,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 = From 5f97f4c4a830c3d87a43ab5ce5d08a0454956d00 Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Fri, 12 Apr 2024 10:21:19 -0400 Subject: [PATCH 174/351] Update Organization and Policy Services to allow the passing of a user id and to prevent hangs waiting on an active user (#8712) * OrgSvc - add new observable returning getAll$ method which accepts a required user id * OrgSvc - make user id optional * PolicySvc - getAll$ should use the new OrgSvc.getAll$ method so that it doesn't hang if there isn't an active user yet but a user id was passed in. * Fix policy service tests --- .../organization/organization.service.abstraction.ts | 6 +++++- .../services/organization/organization.service.ts | 4 ++++ .../admin-console/services/policy/policy.service.spec.ts | 7 ++++++- .../src/admin-console/services/policy/policy.service.ts | 2 +- 4 files changed, 16 insertions(+), 3 deletions(-) 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 a1ae64a885..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 @@ -121,7 +121,11 @@ export abstract class OrganizationService { get$: (id: string) => Observable<Organization | undefined>; get: (id: string) => Promise<Organization>; getAll: (userId?: string) => Promise<Organization[]>; - // + + /** + * Publishes state for all organizations for the given user id or the active user. + */ + getAll$: (userId?: UserId) => Observable<Organization[]>; } /** 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 411850fe30..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,6 +73,10 @@ export class OrganizationService implements InternalOrganizationServiceAbstracti return this.organizations$.pipe(mapToSingleOrganization(id)); } + getAll$(userId?: UserId): Observable<Organization[]> { + return this.getOrganizationsFromState$(userId); + } + async getAll(userId?: string): Promise<Organization[]> { return await firstValueFrom(this.getOrganizationsFromState$(userId as UserId)); } 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 b67ef4619f..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<OrganizationService>(); activeUserState = stateProvider.activeUser.getFake(POLICIES); - organizationService.organizations$ = of([ + + const organizations$ = of([ // User organization("org1", true, true, OrganizationUserStatusType.Confirmed, false), // Owner @@ -54,6 +55,10 @@ describe("PolicyService", () => { organization("org6", true, true, OrganizationUserStatusType.Confirmed, true), ]); + organizationService.organizations$ = organizations$; + + organizationService.getAll$.mockReturnValue(organizations$); + policyService = new PolicyService(stateProvider, organizationService); }); 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 a093dad61a..e36902cbf9 100644 --- a/libs/common/src/admin-console/services/policy/policy.service.ts +++ b/libs/common/src/admin-console/services/policy/policy.service.ts @@ -51,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)), ); } From 2e8d1a206150cb911f020f1584cdd59bda516e14 Mon Sep 17 00:00:00 2001 From: Shane Melton <smelton@bitwarden.com> Date: Fri, 12 Apr 2024 08:51:20 -0700 Subject: [PATCH 175/351] [AC-2431] New collection dialog bug (#8648) * [AC-2431] Add null check for convertToPermission helper * [AC-2431] Only attempt to convertToPermission if collectionId has a value --- .../access-selector/access-selector.models.ts | 7 ++++++- .../collection-dialog.component.ts | 16 +++++++++++----- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.models.ts b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.models.ts index 8fbca22bc5..be313c2574 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.models.ts +++ b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.models.ts @@ -101,7 +101,12 @@ export const getPermissionList = (flexibleCollectionsEnabled: boolean): Permissi * for the dropdown in the AccessSelectorComponent * @param value */ -export const convertToPermission = (value: CollectionAccessSelectionView) => { +export const convertToPermission = ( + value: CollectionAccessSelectionView | undefined, +): CollectionPermission | undefined => { + if (value == null) { + return undefined; + } if (value.manage) { return CollectionPermission.Manage; } else if (value.readOnly) { diff --git a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts index 64150245cb..8e0d610c93 100644 --- a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts +++ b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts @@ -29,9 +29,9 @@ import { CollectionView } from "@bitwarden/common/vault/models/view/collection.v import { BitValidators, DialogService } from "@bitwarden/components"; import { + CollectionAccessSelectionView, GroupService, GroupView, - CollectionAccessSelectionView, } from "../../../admin-console/organizations/core"; import { PermissionMode } from "../../../admin-console/organizations/shared/components/access-selector/access-selector.component"; import { @@ -432,7 +432,10 @@ function mapGroupToAccessItemView(group: GroupView, collectionId: string): Acces labelName: group.name, accessAllItems: group.accessAll, readonly: group.accessAll, - readonlyPermission: convertToPermission(group.collections.find((gc) => gc.id == collectionId)), + readonlyPermission: + collectionId != null + ? convertToPermission(group.collections.find((gc) => gc.id == collectionId)) + : undefined, }; } @@ -456,9 +459,12 @@ function mapUserToAccessItemView( status: user.status, accessAllItems: user.accessAll, readonly: user.accessAll, - readonlyPermission: convertToPermission( - new CollectionAccessSelectionView(user.collections.find((uc) => uc.id == collectionId)), - ), + readonlyPermission: + collectionId != null + ? convertToPermission( + new CollectionAccessSelectionView(user.collections.find((uc) => uc.id == collectionId)), + ) + : undefined, }; } From 8a71b50a5e75584822ca1185b58a36d815c39304 Mon Sep 17 00:00:00 2001 From: aj-rosado <109146700+aj-rosado@users.noreply.github.com> Date: Fri, 12 Apr 2024 17:14:48 +0100 Subject: [PATCH 176/351] Initializing masterPasswordService on bw.ts (#8725) --- apps/cli/src/bw.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index ed8c52ed94..ebae308a81 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -35,6 +35,7 @@ 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"; @@ -353,6 +354,8 @@ export class Main { migrationRunner, ); + this.masterPasswordService = new MasterPasswordService(this.stateProvider); + this.cryptoService = new CryptoService( this.masterPasswordService, this.keyGenerationService, From 44d59f0d8cea06bac240764aabcb5a747c5bf2de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=A8=20Audrey=20=E2=9C=A8?= <ajensen@bitwarden.com> Date: Fri, 12 Apr 2024 13:31:58 -0400 Subject: [PATCH 177/351] [PM-7419] add buffered state (#8706) Introduces a state manager that buffers data until an observed dependency signals it should overwrite another state manager with the buffered data. It can be used to implement migrations of encrypted data, edit-apply loops (such as used for save operations), and to map between encryption keys/formats. --- .../state/buffered-key-definition.spec.ts | 119 ++++++ .../state/buffered-key-definition.ts | 100 +++++ .../generator/state/buffered-state.spec.ts | 375 ++++++++++++++++++ .../tools/generator/state/buffered-state.ts | 144 +++++++ 4 files changed, 738 insertions(+) create mode 100644 libs/common/src/tools/generator/state/buffered-key-definition.spec.ts create mode 100644 libs/common/src/tools/generator/state/buffered-key-definition.ts create mode 100644 libs/common/src/tools/generator/state/buffered-state.spec.ts create mode 100644 libs/common/src/tools/generator/state/buffered-state.ts 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<number, number, number>(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<number, number, number>(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<Input, Output, Dependency> = + UserKeyDefinitionOptions<Input> & { + /** 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<boolean>; + + /** 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<Output>; + + /** 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<Input, Output = Input, Dependency = true> { + /** + * 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<Input, Output, Dependency>, + ) {} + + /** Converts the buffered key definition to a state provider + * key definition + */ + toKeyDefinition() { + const bufferedKey = new UserKeyDefinition<Input>(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<SomeType>(GENERATOR_DISK, "fooBar", { + deserializer: (jsonValue) => jsonValue as SomeType, +}); +const BUFFER_KEY = new BufferedKeyDefinition<SomeType>(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<boolean>(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<boolean>(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<SomeType>(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<SomeType>(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<SomeType>(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<boolean>(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<boolean>(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<boolean>(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<SomeType>(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<SomeType>(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<SomeType>(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<Input, Output, Dependency> implements SingleUserState<Output> { + /** + * 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<Input, Output, Dependency>, + private output: SingleUserState<Output>, + dependency$: Observable<Dependency> = 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<Input>; + + 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<Output>; + + /** {@link SingleUserState.combinedState$} */ + readonly combinedState$: Observable<CombinedState<Output>>; + + /** 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<void> { + 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<Input>; + + /** 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<TCombine>( + configureState: (state: Output, dependencies: TCombine) => Output, + options: StateUpdateOptions<Output, TCombine> = null, + ): Promise<Output> { + return this.output.update(configureState, options); + } +} From c701ad9cf29a9efd16cb9bad13ff947909c0b038 Mon Sep 17 00:00:00 2001 From: Jason Ng <jng@bitwarden.com> Date: Fri, 12 Apr 2024 15:41:05 -0400 Subject: [PATCH 178/351] [AC-2330] Updated Cipher Collections Now Sets Readonly Properly (#8549) Update putCipherCollection call to get new cipher with updated edit value --- libs/common/src/abstractions/api.service.ts | 2 +- libs/common/src/services/api.service.ts | 8 ++++++-- libs/common/src/vault/services/cipher.service.ts | 4 ++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/libs/common/src/abstractions/api.service.ts b/libs/common/src/abstractions/api.service.ts index 20ed3216a5..6962a44268 100644 --- a/libs/common/src/abstractions/api.service.ts +++ b/libs/common/src/abstractions/api.service.ts @@ -220,7 +220,7 @@ export abstract class ApiService { putMoveCiphers: (request: CipherBulkMoveRequest) => Promise<any>; putShareCipher: (id: string, request: CipherShareRequest) => Promise<CipherResponse>; putShareCiphers: (request: CipherBulkShareRequest) => Promise<any>; - putCipherCollections: (id: string, request: CipherCollectionsRequest) => Promise<any>; + putCipherCollections: (id: string, request: CipherCollectionsRequest) => Promise<CipherResponse>; putCipherCollectionsAdmin: (id: string, request: CipherCollectionsRequest) => Promise<any>; postPurgeCiphers: (request: SecretVerificationRequest, organizationId?: string) => Promise<any>; putDeleteCipher: (id: string) => Promise<any>; diff --git a/libs/common/src/services/api.service.ts b/libs/common/src/services/api.service.ts index 6306eb1e28..c7a8f3f091 100644 --- a/libs/common/src/services/api.service.ts +++ b/libs/common/src/services/api.service.ts @@ -565,8 +565,12 @@ export class ApiService implements ApiServiceAbstraction { return this.send("PUT", "/ciphers/share", request, true, false); } - putCipherCollections(id: string, request: CipherCollectionsRequest): Promise<any> { - return this.send("PUT", "/ciphers/" + id + "/collections", request, true, false); + async putCipherCollections( + id: string, + request: CipherCollectionsRequest, + ): Promise<CipherResponse> { + const response = await this.send("PUT", "/ciphers/" + id + "/collections", request, true, true); + return new CipherResponse(response); } putCipherCollectionsAdmin(id: string, request: CipherCollectionsRequest): Promise<any> { diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 7d3772f8c5..dffbf5cbbe 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -683,8 +683,8 @@ 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); } From 8162c640f69228f1101854f48eb201f383ba9b97 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 12 Apr 2024 19:51:45 +0000 Subject: [PATCH 179/351] Autosync the updated translations (#8717) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/desktop/src/locales/ca/messages.json | 4 ++-- apps/desktop/src/locales/ru/messages.json | 6 +++--- apps/desktop/src/locales/zh_CN/messages.json | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/locales/ca/messages.json b/apps/desktop/src/locales/ca/messages.json index 9f341f926f..c76111b53c 100644 --- a/apps/desktop/src/locales/ca/messages.json +++ b/apps/desktop/src/locales/ca/messages.json @@ -2698,9 +2698,9 @@ "message": "Activeu l'acceleració i reinicieu el maquinari" }, "removePasskey": { - "message": "Remove passkey" + "message": "Suprimeix la clau de pas" }, "passkeyRemoved": { - "message": "Passkey removed" + "message": "Clau de pas suprimida" } } diff --git a/apps/desktop/src/locales/ru/messages.json b/apps/desktop/src/locales/ru/messages.json index 357a5757ce..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": "Псевдоним домена" diff --git a/apps/desktop/src/locales/zh_CN/messages.json b/apps/desktop/src/locales/zh_CN/messages.json index 2c3401de6b..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": "使用主密码" From 6f31f42755bce3662e94a89b75b34a942a09420e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 12 Apr 2024 19:52:02 +0000 Subject: [PATCH 180/351] Autosync the updated translations (#8716) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/browser/src/_locales/ar/messages.json | 3 ++ apps/browser/src/_locales/az/messages.json | 3 ++ apps/browser/src/_locales/be/messages.json | 3 ++ apps/browser/src/_locales/bg/messages.json | 3 ++ apps/browser/src/_locales/bn/messages.json | 3 ++ apps/browser/src/_locales/bs/messages.json | 3 ++ apps/browser/src/_locales/ca/messages.json | 7 ++-- apps/browser/src/_locales/cs/messages.json | 3 ++ apps/browser/src/_locales/cy/messages.json | 3 ++ apps/browser/src/_locales/da/messages.json | 3 ++ apps/browser/src/_locales/de/messages.json | 5 ++- apps/browser/src/_locales/el/messages.json | 3 ++ apps/browser/src/_locales/en_GB/messages.json | 3 ++ apps/browser/src/_locales/en_IN/messages.json | 3 ++ apps/browser/src/_locales/es/messages.json | 3 ++ apps/browser/src/_locales/et/messages.json | 3 ++ apps/browser/src/_locales/eu/messages.json | 3 ++ apps/browser/src/_locales/fa/messages.json | 3 ++ apps/browser/src/_locales/fi/messages.json | 3 ++ apps/browser/src/_locales/fil/messages.json | 3 ++ apps/browser/src/_locales/fr/messages.json | 3 ++ apps/browser/src/_locales/gl/messages.json | 3 ++ apps/browser/src/_locales/he/messages.json | 3 ++ apps/browser/src/_locales/hi/messages.json | 3 ++ apps/browser/src/_locales/hr/messages.json | 3 ++ apps/browser/src/_locales/hu/messages.json | 3 ++ apps/browser/src/_locales/id/messages.json | 3 ++ apps/browser/src/_locales/it/messages.json | 3 ++ apps/browser/src/_locales/ja/messages.json | 3 ++ apps/browser/src/_locales/ka/messages.json | 3 ++ apps/browser/src/_locales/km/messages.json | 3 ++ apps/browser/src/_locales/kn/messages.json | 3 ++ apps/browser/src/_locales/ko/messages.json | 3 ++ apps/browser/src/_locales/lt/messages.json | 3 ++ apps/browser/src/_locales/lv/messages.json | 3 ++ apps/browser/src/_locales/ml/messages.json | 3 ++ apps/browser/src/_locales/mr/messages.json | 3 ++ apps/browser/src/_locales/my/messages.json | 3 ++ apps/browser/src/_locales/nb/messages.json | 3 ++ apps/browser/src/_locales/ne/messages.json | 3 ++ apps/browser/src/_locales/nl/messages.json | 3 ++ apps/browser/src/_locales/nn/messages.json | 3 ++ apps/browser/src/_locales/or/messages.json | 3 ++ apps/browser/src/_locales/pl/messages.json | 3 ++ apps/browser/src/_locales/pt_BR/messages.json | 3 ++ apps/browser/src/_locales/pt_PT/messages.json | 3 ++ apps/browser/src/_locales/ro/messages.json | 3 ++ apps/browser/src/_locales/ru/messages.json | 33 ++++++++++--------- apps/browser/src/_locales/si/messages.json | 3 ++ apps/browser/src/_locales/sk/messages.json | 3 ++ apps/browser/src/_locales/sl/messages.json | 3 ++ apps/browser/src/_locales/sr/messages.json | 3 ++ apps/browser/src/_locales/sv/messages.json | 3 ++ apps/browser/src/_locales/te/messages.json | 3 ++ apps/browser/src/_locales/th/messages.json | 3 ++ apps/browser/src/_locales/tr/messages.json | 3 ++ apps/browser/src/_locales/uk/messages.json | 3 ++ apps/browser/src/_locales/vi/messages.json | 3 ++ apps/browser/src/_locales/zh_CN/messages.json | 3 ++ apps/browser/src/_locales/zh_TW/messages.json | 3 ++ 60 files changed, 198 insertions(+), 18 deletions(-) diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index 6000df04bb..7b17b114d6 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -3005,5 +3005,8 @@ }, "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." } } diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index f7479ccf18..e12fb0dc88 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -3005,5 +3005,8 @@ }, "passkeyRemoved": { "message": "Parol silindi" + }, + "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." } } diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index 0b11a5e3e6..26ecf49cc7 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -3005,5 +3005,8 @@ }, "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." } } diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index 7ffecb5d1f..2ddaf647d8 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -3005,5 +3005,8 @@ }, "passkeyRemoved": { "message": "Секретният ключ е премахнат" + }, + "unassignedItemsBanner": { + "message": "Забележка: неразпределените елементи на организацията вече не се виждат в изгледа с „Всички трезори“, а са достъпни само през Административната конзола. Добавете тези елементи към някоя колекция в Административната конзола, за да станат видими." } } diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index 63cb122765..4bdf811b3b 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -3005,5 +3005,8 @@ }, "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." } } diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index e0a0633dda..a7f157011e 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -3005,5 +3005,8 @@ }, "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." } } diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index 1572f54c80..147a64233c 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -3001,9 +3001,12 @@ "description": "Notification message for when saving credentials has failed." }, "removePasskey": { - "message": "Remove passkey" + "message": "Suprimeix la clau de pas" }, "passkeyRemoved": { - "message": "Passkey removed" + "message": "Clau de pas suprimida" + }, + "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." } } diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index e818527b51..2b6a8d4f0b 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -3005,5 +3005,8 @@ }, "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é." } } diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index 410c8dbe80..79867bf7bf 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -3005,5 +3005,8 @@ }, "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." } } diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index 4c686aa5ce..26e08741b2 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -3005,5 +3005,8 @@ }, "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." } } diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index 502f5a8833..b542fbfad7 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" @@ -3005,5 +3005,8 @@ }, "passkeyRemoved": { "message": "Passkey gelöscht" + }, + "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." } } diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index a698cf2ec6..7bbc615dae 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -3005,5 +3005,8 @@ }, "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." } } diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index 284d05d7bc..1c24106cdc 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -3005,5 +3005,8 @@ }, "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." } } diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index 77d4b05427..f1a4979766 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -3005,5 +3005,8 @@ }, "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." } } diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index da9d26eb6d..31200031f6 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -3005,5 +3005,8 @@ }, "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." } } diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index fc108fcab5..1f98d15758 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -3005,5 +3005,8 @@ }, "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." } } diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index 2d8286307b..49086462d9 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -3005,5 +3005,8 @@ }, "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." } } diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index fc702246d5..0c44026cec 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -3005,5 +3005,8 @@ }, "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." } } diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index 13c6d119f9..ead3d43236 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -3005,5 +3005,8 @@ }, "passkeyRemoved": { "message": "Suojausavain poistettiin" + }, + "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." } } diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index d1cd0687e8..e9bc8e2c75 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -3005,5 +3005,8 @@ }, "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." } } diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index adfb462a66..452a2b363b 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -3005,5 +3005,8 @@ }, "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." } } diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index 0f2cab77d8..dc91c1e6a9 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -3005,5 +3005,8 @@ }, "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." } } diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index f9c352a683..7a05907228 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -3005,5 +3005,8 @@ }, "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." } } diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index 84bc3461ae..99daecc5f8 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -3005,5 +3005,8 @@ }, "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." } } diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index c98aae3a3b..ec4509dbd4 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -3005,5 +3005,8 @@ }, "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." } } diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index c720d99c71..5c10efc3ae 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -3005,5 +3005,8 @@ }, "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." } } diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index ecc11725e7..810d603047 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -3005,5 +3005,8 @@ }, "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." } } diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index fb87081121..86a6bf054e 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -3005,5 +3005,8 @@ }, "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." } } diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index a247ee29cb..eb78a7205f 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -3005,5 +3005,8 @@ }, "passkeyRemoved": { "message": "パスキーを削除しました" + }, + "unassignedItemsBanner": { + "message": "注意: 割り当てられていない組織項目は、すべての保管庫のビューでは表示されなくなり、管理コンソールからのみアクセスできます。 管理コンソールからコレクションにこれらのアイテムを割り当てると、表示するようにできます。" } } diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index 2559f4a109..66dae59f4c 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -3005,5 +3005,8 @@ }, "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." } } diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index 0f2cab77d8..dc91c1e6a9 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -3005,5 +3005,8 @@ }, "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." } } diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index 01997a462c..68fe45999d 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -3005,5 +3005,8 @@ }, "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." } } diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index a99dd11d2f..689e618929 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -3005,5 +3005,8 @@ }, "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." } } diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index 9537241f0d..c99d128915 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -3005,5 +3005,8 @@ }, "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." } } diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index 492141ff59..c36315d10d 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -3005,5 +3005,8 @@ }, "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." } } diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index b87a8c8ee6..7205a54559 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -3005,5 +3005,8 @@ }, "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." } } diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index c3859f9764..918557feb8 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -3005,5 +3005,8 @@ }, "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." } } diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index 0f2cab77d8..dc91c1e6a9 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -3005,5 +3005,8 @@ }, "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." } } diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index 5256eba72d..d77aaa7d7c 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -3005,5 +3005,8 @@ }, "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." } } diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index 0f2cab77d8..dc91c1e6a9 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -3005,5 +3005,8 @@ }, "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." } } diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index e189f1774f..bbf16b345a 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -3005,5 +3005,8 @@ }, "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." } } diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index 0f2cab77d8..dc91c1e6a9 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -3005,5 +3005,8 @@ }, "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." } } diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index 0f2cab77d8..dc91c1e6a9 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -3005,5 +3005,8 @@ }, "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." } } diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index 4fa5fcb859..98130de3be 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -3005,5 +3005,8 @@ }, "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." } } diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index a21308af6a..6126679902 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -3005,5 +3005,8 @@ }, "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." } } diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index bd42d39535..a904867d5f 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -3005,5 +3005,8 @@ }, "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." } } diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index 49ca701a6f..3e554f4d68 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -3005,5 +3005,8 @@ }, "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." } } diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index 229ab31816..aed3d58b11 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": "Многофакторная аутентификация отменена" @@ -3005,5 +3005,8 @@ }, "passkeyRemoved": { "message": "Passkey удален" + }, + "unassignedItemsBanner": { + "message": "Обратите внимание: неприсвоенные элементы организации больше не видны в представлении \"Все хранилища\" и доступны только через консоль администратора. Назначьте эти элементы коллекции в консоли администратора, чтобы сделать их видимыми." } } diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index 9857b8ca97..3dc18a97fb 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -3005,5 +3005,8 @@ }, "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." } } diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index b6f984a04c..bd4f9f9796 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -3005,5 +3005,8 @@ }, "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." } } diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index a8547066e6..3e2690be30 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -3005,5 +3005,8 @@ }, "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." } } diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index 67ef9eb856..fef1a4eb8a 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -3005,5 +3005,8 @@ }, "passkeyRemoved": { "message": "Приступачни кључ је уклоњен" + }, + "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." } } diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index e37b914b28..0a27862c74 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -3005,5 +3005,8 @@ }, "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." } } diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index 0f2cab77d8..dc91c1e6a9 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -3005,5 +3005,8 @@ }, "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." } } diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index 0ee21fb3ec..ecc346513d 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -3005,5 +3005,8 @@ }, "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." } } diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index d9da7727cc..f6c00a58b8 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -3005,5 +3005,8 @@ }, "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." } } diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index 64d3f62a78..873cfad2c6 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -3005,5 +3005,8 @@ }, "passkeyRemoved": { "message": "Ключ доступу вилучено" + }, + "unassignedItemsBanner": { + "message": "Увага: непризначені елементи організації більше не видимі у поданні \"Усі сховища\" і доступні лише в консолі адміністратора. Щоб зробити їх видимими, призначте ці елементи збірці в консолі адміністратора." } } diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index 7aa43a4491..d71fa3322f 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -3005,5 +3005,8 @@ }, "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." } } diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index 1e31baee60..85fb5f6d2e 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -3005,5 +3005,8 @@ }, "passkeyRemoved": { "message": "通行密钥已移除" + }, + "unassignedItemsBanner": { + "message": "注意:未分配的组织项目在「所有密码库」视图中不再可见,只能通过管理控制台访问。通过管理控制台将这些项目分配给集合以使其可见。" } } diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index c47bf538b8..66d9f7ce62 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -3005,5 +3005,8 @@ }, "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." } } From c8f03a0d46418dc1bbe4613c10f664542fe62bd5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 15 Apr 2024 08:55:30 +0000 Subject: [PATCH 181/351] Autosync the updated translations (#8626) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/web/src/locales/af/messages.json | 260 ++++++++++++++ apps/web/src/locales/ar/messages.json | 260 ++++++++++++++ apps/web/src/locales/az/messages.json | 278 ++++++++++++++- apps/web/src/locales/be/messages.json | 260 ++++++++++++++ apps/web/src/locales/bg/messages.json | 260 ++++++++++++++ apps/web/src/locales/bn/messages.json | 260 ++++++++++++++ apps/web/src/locales/bs/messages.json | 260 ++++++++++++++ apps/web/src/locales/ca/messages.json | 266 ++++++++++++++- apps/web/src/locales/cs/messages.json | 260 ++++++++++++++ apps/web/src/locales/cy/messages.json | 260 ++++++++++++++ apps/web/src/locales/da/messages.json | 260 ++++++++++++++ apps/web/src/locales/de/messages.json | 260 ++++++++++++++ apps/web/src/locales/el/messages.json | 260 ++++++++++++++ apps/web/src/locales/en_GB/messages.json | 260 ++++++++++++++ apps/web/src/locales/en_IN/messages.json | 260 ++++++++++++++ apps/web/src/locales/eo/messages.json | 260 ++++++++++++++ apps/web/src/locales/es/messages.json | 260 ++++++++++++++ apps/web/src/locales/et/messages.json | 260 ++++++++++++++ apps/web/src/locales/eu/messages.json | 260 ++++++++++++++ apps/web/src/locales/fa/messages.json | 260 ++++++++++++++ apps/web/src/locales/fi/messages.json | 264 ++++++++++++++- apps/web/src/locales/fil/messages.json | 260 ++++++++++++++ apps/web/src/locales/fr/messages.json | 260 ++++++++++++++ apps/web/src/locales/gl/messages.json | 260 ++++++++++++++ apps/web/src/locales/he/messages.json | 260 ++++++++++++++ apps/web/src/locales/hi/messages.json | 260 ++++++++++++++ apps/web/src/locales/hr/messages.json | 260 ++++++++++++++ apps/web/src/locales/hu/messages.json | 260 ++++++++++++++ apps/web/src/locales/id/messages.json | 260 ++++++++++++++ apps/web/src/locales/it/messages.json | 260 ++++++++++++++ apps/web/src/locales/ja/messages.json | 260 ++++++++++++++ apps/web/src/locales/ka/messages.json | 260 ++++++++++++++ apps/web/src/locales/km/messages.json | 260 ++++++++++++++ apps/web/src/locales/kn/messages.json | 260 ++++++++++++++ apps/web/src/locales/ko/messages.json | 410 ++++++++++++++++++----- apps/web/src/locales/lv/messages.json | 260 ++++++++++++++ apps/web/src/locales/ml/messages.json | 260 ++++++++++++++ apps/web/src/locales/mr/messages.json | 260 ++++++++++++++ apps/web/src/locales/my/messages.json | 260 ++++++++++++++ apps/web/src/locales/nb/messages.json | 260 ++++++++++++++ apps/web/src/locales/ne/messages.json | 260 ++++++++++++++ apps/web/src/locales/nl/messages.json | 260 ++++++++++++++ apps/web/src/locales/nn/messages.json | 260 ++++++++++++++ apps/web/src/locales/or/messages.json | 260 ++++++++++++++ apps/web/src/locales/pl/messages.json | 260 ++++++++++++++ apps/web/src/locales/pt_BR/messages.json | 260 ++++++++++++++ apps/web/src/locales/pt_PT/messages.json | 284 +++++++++++++++- apps/web/src/locales/ro/messages.json | 260 ++++++++++++++ apps/web/src/locales/ru/messages.json | 300 +++++++++++++++-- apps/web/src/locales/si/messages.json | 260 ++++++++++++++ apps/web/src/locales/sk/messages.json | 260 ++++++++++++++ apps/web/src/locales/sl/messages.json | 260 ++++++++++++++ apps/web/src/locales/sr/messages.json | 260 ++++++++++++++ apps/web/src/locales/sr_CS/messages.json | 260 ++++++++++++++ apps/web/src/locales/sv/messages.json | 260 ++++++++++++++ apps/web/src/locales/te/messages.json | 260 ++++++++++++++ apps/web/src/locales/th/messages.json | 260 ++++++++++++++ apps/web/src/locales/tr/messages.json | 260 ++++++++++++++ apps/web/src/locales/uk/messages.json | 260 ++++++++++++++ apps/web/src/locales/vi/messages.json | 260 ++++++++++++++ apps/web/src/locales/zh_CN/messages.json | 284 +++++++++++++++- apps/web/src/locales/zh_TW/messages.json | 310 +++++++++++++++-- 62 files changed, 16278 insertions(+), 158 deletions(-) diff --git a/apps/web/src/locales/af/messages.json b/apps/web/src/locales/af/messages.json index 3677e54c1c..efd009b3ad 100644 --- a/apps/web/src/locales/af/messages.json +++ b/apps/web/src/locales/af/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Voeg bestaande organisasie toe" }, + "addNewOrganization": { + "message": "Add new organization" + }, "myProvider": { "message": "My aanbieder" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "Collection access is restricted" }, + "readOnlyCollectionAccess": { + "message": "You do not have access to manage this collection." + }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "viewCollection": { + "message": "View collection" + }, "restrictedGroupAccess": { "message": "You cannot add yourself to groups." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Items" + }, + "assignedSeats": { + "message": "Assigned seats" + }, + "assigned": { + "message": "Assigned" + }, + "used": { + "message": "Used" + }, + "remaining": { + "message": "Remaining" + }, + "unlinkOrganization": { + "message": "Unlink organization" + }, + "manageSeats": { + "message": "MANAGE SEATS" + }, + "manageSeatsDescription": { + "message": "Adjustments to seats will be reflected in the next billing cycle." + }, + "unassignedSeatsDescription": { + "message": "Unassigned subscription seats" + }, + "purchaseSeatDescription": { + "message": "Additional seats purchased" + }, + "assignedSeatCannotUpdate": { + "message": "Assigned Seats can not be updated. Please contact your organization owner for assistance." + }, + "subscriptionUpdateFailed": { + "message": "Subscription update failed" + }, + "trial": { + "message": "Trial", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Past due", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Subscription expired", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Unpaid invoice", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "To reactivate your subscription, please resolve the past due invoices.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Cancellation date", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + }, + "machineAccount": { + "message": "Machine account", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Machine accounts", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "New machine account", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Create a new machine account to get started automating secret access.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Nothing to show yet", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Delete machine accounts", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Delete machine account", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "View machine account", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Deleting machine accounts is permanent and irreversible." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Delete $COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Machine account deleted" + }, + "deleteMachineAccountsToast": { + "message": "Machine accounts deleted" + }, + "searchMachineAccounts": { + "message": "Search machine accounts", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Edit machine account", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Machine account name", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Machine account created", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Machine account updated", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Grant machine accounts access to this project." + }, + "projectMachineAccountsSelectHint": { + "message": "Type or select machine accounts" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Add machine accounts to grant access" + }, + "machineAccountPeopleDescription": { + "message": "Grant groups or people access to this machine account." + }, + "machineAccountProjectsDescription": { + "message": "Assign projects to this machine account. " + }, + "createMachineAccount": { + "message": "Create a machine account" + }, + "maPeopleWarningMessage": { + "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Remove access to this machine account" + }, + "smAccessRemovalWarningMaMessage": { + "message": "This action will remove your access to the machine account." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ machine accounts included", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ per month for additional machine accounts", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Additional machine accounts" + }, + "includedMachineAccounts": { + "message": "Your plan comes with $COUNT$ machine accounts.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "You can add additional machine accounts for $COST$ per month.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Limit machine accounts (optional)" + }, + "limitMachineAccountsDesc": { + "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + }, + "machineAccountLimit": { + "message": "Machine account limit (optional)" + }, + "maxMachineAccountCost": { + "message": "Max potential machine account cost" + }, + "machineAccountAccessUpdated": { + "message": "Machine account access updated" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/web/src/locales/ar/messages.json b/apps/web/src/locales/ar/messages.json index f5353f2234..e2ec92c79f 100644 --- a/apps/web/src/locales/ar/messages.json +++ b/apps/web/src/locales/ar/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Add existing organization" }, + "addNewOrganization": { + "message": "Add new organization" + }, "myProvider": { "message": "My Provider" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "Collection access is restricted" }, + "readOnlyCollectionAccess": { + "message": "You do not have access to manage this collection." + }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "viewCollection": { + "message": "View collection" + }, "restrictedGroupAccess": { "message": "You cannot add yourself to groups." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Items" + }, + "assignedSeats": { + "message": "Assigned seats" + }, + "assigned": { + "message": "Assigned" + }, + "used": { + "message": "Used" + }, + "remaining": { + "message": "Remaining" + }, + "unlinkOrganization": { + "message": "Unlink organization" + }, + "manageSeats": { + "message": "MANAGE SEATS" + }, + "manageSeatsDescription": { + "message": "Adjustments to seats will be reflected in the next billing cycle." + }, + "unassignedSeatsDescription": { + "message": "Unassigned subscription seats" + }, + "purchaseSeatDescription": { + "message": "Additional seats purchased" + }, + "assignedSeatCannotUpdate": { + "message": "Assigned Seats can not be updated. Please contact your organization owner for assistance." + }, + "subscriptionUpdateFailed": { + "message": "Subscription update failed" + }, + "trial": { + "message": "Trial", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Past due", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Subscription expired", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Unpaid invoice", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "To reactivate your subscription, please resolve the past due invoices.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Cancellation date", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + }, + "machineAccount": { + "message": "Machine account", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Machine accounts", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "New machine account", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Create a new machine account to get started automating secret access.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Nothing to show yet", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Delete machine accounts", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Delete machine account", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "View machine account", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Deleting machine accounts is permanent and irreversible." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Delete $COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Machine account deleted" + }, + "deleteMachineAccountsToast": { + "message": "Machine accounts deleted" + }, + "searchMachineAccounts": { + "message": "Search machine accounts", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Edit machine account", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Machine account name", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Machine account created", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Machine account updated", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Grant machine accounts access to this project." + }, + "projectMachineAccountsSelectHint": { + "message": "Type or select machine accounts" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Add machine accounts to grant access" + }, + "machineAccountPeopleDescription": { + "message": "Grant groups or people access to this machine account." + }, + "machineAccountProjectsDescription": { + "message": "Assign projects to this machine account. " + }, + "createMachineAccount": { + "message": "Create a machine account" + }, + "maPeopleWarningMessage": { + "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Remove access to this machine account" + }, + "smAccessRemovalWarningMaMessage": { + "message": "This action will remove your access to the machine account." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ machine accounts included", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ per month for additional machine accounts", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Additional machine accounts" + }, + "includedMachineAccounts": { + "message": "Your plan comes with $COUNT$ machine accounts.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "You can add additional machine accounts for $COST$ per month.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Limit machine accounts (optional)" + }, + "limitMachineAccountsDesc": { + "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + }, + "machineAccountLimit": { + "message": "Machine account limit (optional)" + }, + "maxMachineAccountCost": { + "message": "Max potential machine account cost" + }, + "machineAccountAccessUpdated": { + "message": "Machine account access updated" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/web/src/locales/az/messages.json b/apps/web/src/locales/az/messages.json index bc30012efe..6b84306675 100644 --- a/apps/web/src/locales/az/messages.json +++ b/apps/web/src/locales/az/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Mövcud təşkilatı əlavə et" }, + "addNewOrganization": { + "message": "Yeni təşkilat əlavə et" + }, "myProvider": { "message": "Provayderim" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "Kolleksiya müraciəti məhdudlaşdırıldı" }, + "readOnlyCollectionAccess": { + "message": "Bu kolleksiyanı idarə etmək üçün müraciətiniz yoxdur." + }, "grantCollectionAccess": { "message": "Qrup və ya üzvlərin bu kolleksiyaya müraciətinə icazə verin." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Provayder Portal" }, + "viewCollection": { + "message": "Kolleksiyaya bax" + }, "restrictedGroupAccess": { "message": "Özünüzü qruplara əlavə edə bilməzsiniz." }, @@ -7607,28 +7616,28 @@ "message": "Özünüzü kolleksiyalara əlavə edə bilməzsiniz." }, "assign": { - "message": "Assign" + "message": "Təyin et" }, "assignToCollections": { - "message": "Assign to collections" + "message": "Kolleksiyalara təyin et" }, "assignToTheseCollections": { - "message": "Assign to these collections" + "message": "Bu kolleksiyalara təyin et" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Elementlərin paylaşılacağı kolleksiyaları seçin. Bir kolleksiyada bir element güncəlləndikdə, bütün kolleksiyalarda əks olunacaq. Elementləri, yalnız bu kolleksiyalara müraciət edə bilən təşkilat üzvləri görə bilər." }, "selectCollectionsToAssign": { - "message": "Select collections to assign" + "message": "Təyin ediləcək kolleksiyaları seçin" }, "noCollectionsAssigned": { - "message": "No collections have been assigned" + "message": "Heç bir kolleksiya təyin edilmədi" }, "successfullyAssignedCollections": { - "message": "Successfully assigned collections" + "message": "Uğurla təyin edilən kolleksiyalar" }, "bulkCollectionAssignmentWarning": { - "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "message": "$TOTAL_COUNT$ element seçmisiniz. Düzəliş icazəniz olmadığı üçün $READONLY_COUNT$ elementi güncəlləyə bilməzsiniz.", "placeholders": { "total_count": { "content": "$1", @@ -7641,6 +7650,257 @@ } }, "items": { - "message": "Items" + "message": "Elementlər" + }, + "assignedSeats": { + "message": "Təyin edilən yerlər" + }, + "assigned": { + "message": "Təyin edilmiş" + }, + "used": { + "message": "İstifadə edilmiş" + }, + "remaining": { + "message": "Qalan" + }, + "unlinkOrganization": { + "message": "Təşkilatı ayır" + }, + "manageSeats": { + "message": "YERLƏRİ İDARƏ ET" + }, + "manageSeatsDescription": { + "message": "Yerlərə edilən düzəlişlər növbəti hesablaşma dövründə öz əksini tapacaq." + }, + "unassignedSeatsDescription": { + "message": "Təyin edilməmiş abunəlik yerləri" + }, + "purchaseSeatDescription": { + "message": "Əlavə yerlər satın alındı" + }, + "assignedSeatCannotUpdate": { + "message": "Təyin edilən yerlər güncəllənə bilməz. Lütfən kömək üçün təşkilatınızın sahibi ilə əlaqə saxlayın." + }, + "subscriptionUpdateFailed": { + "message": "Abunəlik güncəllənmədi" + }, + "trial": { + "message": "Sınaq", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Vaxtı keçmiş", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Abunəliyin müddəti bitib", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "Abunəliyinizi davam etdirmək üçün abunəliyin bitmə tarixindən etibarən $DAYS$ gün güzəşt dövrünüz var. $SUSPENSION_DATE$ tarixinə qədər vaxtı keçmiş fakturaları həll edin.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "Abunəliyinizi davam etdirmək üçün ilk ödənilməmiş fakturanızın yazıldığı tarixdən etibarən $DAYS$ gün güzəşt dövrünüz var. $SUSPENSION_DATE$ tarixinə qədər vaxtı keçmiş fakturaları həll edin.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Ödənilməmiş faktura", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "Abunəliyi yenidən aktivləşdirmək üçün lütfən vaxtı keçmiş fakturaları həll edin.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Ləğv tarixi", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Maşın hesabları fəaliyyəti dayandırılmış təşkilatlarda yaradıla bilməz. Lütfən kömək üçün təşkilatınızın sahibi ilə əlaqə saxlayın." + }, + "machineAccount": { + "message": "Maşın hesabı", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Maşın hesabları", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "Yeni maşın hesabı", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "\"Sirr\" müraciətini avtomatlaşdırmağa başlamaq üçün yeni bir maşın hesabı yaradın.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Göstəriləcək heç nə yoxdur", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Maşın hesablarını sil", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Maşın hesabını sil", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "Maşın hesabına bax", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "$MACHINE_ACCOUNT$ maşın hesabının silinməsi daimi və geri qaytarıla bilməyən prosesdir.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Maşın hesablarının silinməsi daimi və geri qaytarıla bilməyən prosesdir." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "$COUNT$ maşın hesabını sil", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Maşın hesabı silindi" + }, + "deleteMachineAccountsToast": { + "message": "Maşın hesabları silindi" + }, + "searchMachineAccounts": { + "message": "Maşın hesablarını axtar", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Maşın hesabına düzəliş et", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Maşın hesabının adı", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Maşın hesabı yaradıldı", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Maşın hesabı güncəlləndi", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Maşın hesablarının bu layihəyə müraciətinə icazə verin." + }, + "projectMachineAccountsSelectHint": { + "message": "Maşın hesablarını yazın və ya seçin" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Müraciət icazəsi veriləcək maşın hesablarını əlavə edin" + }, + "machineAccountPeopleDescription": { + "message": "Qrup və ya insanların bu maşın hesabına müraciətinə icazə verin." + }, + "machineAccountProjectsDescription": { + "message": "Bu maşın hesabına layihələr təyin edin. " + }, + "createMachineAccount": { + "message": "Bir maşın hesabı yarat" + }, + "maPeopleWarningMessage": { + "message": "İnsanları maşın hesabından silsəniz belə, yaratdıqları müraciət tokenləri silinmir. Güvənliyin ən yaxşı təcrübəsi üçün, maşın hesabından silinmiş insanlar tərəfindən yaradılan müraciət tokenlərinin ləğv edilməsi tövsiyə olunur." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Bu maşın hesabına müraciəti sil" + }, + "smAccessRemovalWarningMaMessage": { + "message": "Bu əməliyyat, maşın hesabına olan müraciətinizi götürəcək." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ maşın hesabı daxildir", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "Əlavə maşın hesabları üçün aylıq $COST$", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Əlavə maşın hesabları" + }, + "includedMachineAccounts": { + "message": "Planınız, $COUNT$ maşın hesabı ilə gəlir.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "Aylıq $COST$ qarşılığında əlavə maşın hesablarını əlavə edə bilərsiniz.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Maşın hesablarını limitlə (ixtiyari)" + }, + "limitMachineAccountsDesc": { + "message": "Maşın hesablarınız üçün bir limit müəyyən edin. Bu limitə çatanda, yeni maşın hesabı yarada bilməyəcəksiniz." + }, + "machineAccountLimit": { + "message": "Maşın hesabı limiti (ixtiyari)" + }, + "maxMachineAccountCost": { + "message": "Maksimal mümkün maşın hesabı qiyməti" + }, + "machineAccountAccessUpdated": { + "message": "Maşın hesabına müraciət güncəlləndi" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/web/src/locales/be/messages.json b/apps/web/src/locales/be/messages.json index 2ec27ae4be..756b87237f 100644 --- a/apps/web/src/locales/be/messages.json +++ b/apps/web/src/locales/be/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Дадаць існуючую арганізацыю" }, + "addNewOrganization": { + "message": "Add new organization" + }, "myProvider": { "message": "Мой пастаўшчык" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "Collection access is restricted" }, + "readOnlyCollectionAccess": { + "message": "You do not have access to manage this collection." + }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "viewCollection": { + "message": "View collection" + }, "restrictedGroupAccess": { "message": "You cannot add yourself to groups." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Items" + }, + "assignedSeats": { + "message": "Assigned seats" + }, + "assigned": { + "message": "Assigned" + }, + "used": { + "message": "Used" + }, + "remaining": { + "message": "Remaining" + }, + "unlinkOrganization": { + "message": "Unlink organization" + }, + "manageSeats": { + "message": "MANAGE SEATS" + }, + "manageSeatsDescription": { + "message": "Adjustments to seats will be reflected in the next billing cycle." + }, + "unassignedSeatsDescription": { + "message": "Unassigned subscription seats" + }, + "purchaseSeatDescription": { + "message": "Additional seats purchased" + }, + "assignedSeatCannotUpdate": { + "message": "Assigned Seats can not be updated. Please contact your organization owner for assistance." + }, + "subscriptionUpdateFailed": { + "message": "Subscription update failed" + }, + "trial": { + "message": "Trial", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Past due", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Subscription expired", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Unpaid invoice", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "To reactivate your subscription, please resolve the past due invoices.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Cancellation date", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + }, + "machineAccount": { + "message": "Machine account", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Machine accounts", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "New machine account", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Create a new machine account to get started automating secret access.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Nothing to show yet", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Delete machine accounts", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Delete machine account", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "View machine account", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Deleting machine accounts is permanent and irreversible." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Delete $COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Machine account deleted" + }, + "deleteMachineAccountsToast": { + "message": "Machine accounts deleted" + }, + "searchMachineAccounts": { + "message": "Search machine accounts", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Edit machine account", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Machine account name", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Machine account created", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Machine account updated", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Grant machine accounts access to this project." + }, + "projectMachineAccountsSelectHint": { + "message": "Type or select machine accounts" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Add machine accounts to grant access" + }, + "machineAccountPeopleDescription": { + "message": "Grant groups or people access to this machine account." + }, + "machineAccountProjectsDescription": { + "message": "Assign projects to this machine account. " + }, + "createMachineAccount": { + "message": "Create a machine account" + }, + "maPeopleWarningMessage": { + "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Remove access to this machine account" + }, + "smAccessRemovalWarningMaMessage": { + "message": "This action will remove your access to the machine account." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ machine accounts included", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ per month for additional machine accounts", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Additional machine accounts" + }, + "includedMachineAccounts": { + "message": "Your plan comes with $COUNT$ machine accounts.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "You can add additional machine accounts for $COST$ per month.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Limit machine accounts (optional)" + }, + "limitMachineAccountsDesc": { + "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + }, + "machineAccountLimit": { + "message": "Machine account limit (optional)" + }, + "maxMachineAccountCost": { + "message": "Max potential machine account cost" + }, + "machineAccountAccessUpdated": { + "message": "Machine account access updated" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/web/src/locales/bg/messages.json b/apps/web/src/locales/bg/messages.json index a0d1781736..c4a6597bf1 100644 --- a/apps/web/src/locales/bg/messages.json +++ b/apps/web/src/locales/bg/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Добавяне на съществуваща организация" }, + "addNewOrganization": { + "message": "Добавяне на нова организация" + }, "myProvider": { "message": "Моят доставчик" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "Достъпът до колекцията е ограничен" }, + "readOnlyCollectionAccess": { + "message": "Нямате достъп за управление на тази колекция." + }, "grantCollectionAccess": { "message": "Дайте права на групи и членове до тази колекция." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Портал за доставчици" }, + "viewCollection": { + "message": "Преглед на колекцията" + }, "restrictedGroupAccess": { "message": "Не може да добавяте себе си към групи." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Елементи" + }, + "assignedSeats": { + "message": "Назначени места" + }, + "assigned": { + "message": "Назначени" + }, + "used": { + "message": "Използвани" + }, + "remaining": { + "message": "Оставащи" + }, + "unlinkOrganization": { + "message": "Разкачане на организацията" + }, + "manageSeats": { + "message": "УПРАВЛЕНИЕ НА МЕСТАТА" + }, + "manageSeatsDescription": { + "message": "Промените по местата ще се отразят в следващия платежен цикъл." + }, + "unassignedSeatsDescription": { + "message": "Неназначени места от абонамента" + }, + "purchaseSeatDescription": { + "message": "Закупени са нови места" + }, + "assignedSeatCannotUpdate": { + "message": "Назначените места не могат да бъдат променени. Свържете се със собственика на организацията си за помощ." + }, + "subscriptionUpdateFailed": { + "message": "Промяната на абонамента беше неуспешна" + }, + "trial": { + "message": "Пробен период", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Просрочено", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Абонаментът е изтекъл", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "След като просрочите периода на абонамента си, разполагате с още $DAYS$ дни, за да го подновите. Моля, заплатете старите фактури до $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "След като просрочите срока на първата си неплатена фактура, разполагате с още $DAYS$ дни, за да подновите абонамента си. Моля, заплатете старите фактури до $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Неплатена фактура", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "За да подновите абонамента си, моля, заплатете фактурите с изтекъл срок.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Дата на прекратяване", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "В изключени организации не могат да се създават машинни акаунти. Свържете се със собственика на организацията си за помощ." + }, + "machineAccount": { + "message": "Машинен акаунт", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Машинни акаунти", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "Нов машинен акаунт", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Създайте нов машинен акаунт, за да можете да автоматизирате достъпа до тайните.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Все още няма нищо за показване", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Изтриване на машинните акаунти", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Изтриване на машинния акаунт", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "Преглед на машинния акаунт", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Изтриването на машинния акаунт $MACHINE_ACCOUNT$ е окончателно и необратимо.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Изтриването на машинни акаунти е окончателно и необратимо." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Изтриване $COUNT$ машинни акаунти", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Машинният акаунт е изтрит" + }, + "deleteMachineAccountsToast": { + "message": "Машинните акаунти са изтрити" + }, + "searchMachineAccounts": { + "message": "Търсене на машинни акаунти", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Редактиране на машинния акаунт", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Име на машинния акаунт", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Машинният акаунт е създаден", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Машинният акаунт е обновен", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Дайте достъп на машинните акаунти до този проект." + }, + "projectMachineAccountsSelectHint": { + "message": "Пишете тук или изберете машинни акаунти" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Добавете машинни акаунти, за да дадете достъп" + }, + "machineAccountPeopleDescription": { + "message": "Дайте права на групи и хора до този машинен акаунт." + }, + "machineAccountProjectsDescription": { + "message": "Назначете проекти към този машинен акаунт. " + }, + "createMachineAccount": { + "message": "Създаване на машинен акаунт" + }, + "maPeopleWarningMessage": { + "message": "Премахването на хора от даден машинен акаунт не премахва идентификаторите за достъп, които те са създали. От гледна точка на сигурността е препоръчително да отнемете достъпа чрез идентификаторите създадени от хора, които са били премахнати от машинен акаунт." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Премахване на достъпа до този машинен акаунт" + }, + "smAccessRemovalWarningMaMessage": { + "message": "Това действие ще премахне достъпа Ви до машинния акаунт." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ включени машинни акаунта", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ на месец за допълнителни машинни акаунти", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Допълнителни машинни акаунти" + }, + "includedMachineAccounts": { + "message": "В плана Ви са включени $COUNT$ машинни акаунта.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "Може да добавите още машинни акаунти за $COST$ на месец.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Ограничаване на машинните акаунти (незадължително)" + }, + "limitMachineAccountsDesc": { + "message": "Задайте ограничение за броя на машинните акаунти. Когато то бъде достигнато, няма да можете да създавате нови машинни акаунти." + }, + "machineAccountLimit": { + "message": "Ограничение на машинните акаунти (незадължително)" + }, + "maxMachineAccountCost": { + "message": "Максимална възможна цена за машинни акаунти" + }, + "machineAccountAccessUpdated": { + "message": "Достъпът на машинния акаунт е променен" + }, + "unassignedItemsBanner": { + "message": "Забележка: неразпределените елементи на организацията вече не се виждат в изгледа с „Всички трезори“ на различните устройства, а са достъпни само през Административната конзола. Добавете тези елементи към някоя колекция в Административната конзола, за да станат видими." } } diff --git a/apps/web/src/locales/bn/messages.json b/apps/web/src/locales/bn/messages.json index 9d0ccd3b50..14457f7ddc 100644 --- a/apps/web/src/locales/bn/messages.json +++ b/apps/web/src/locales/bn/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Add existing organization" }, + "addNewOrganization": { + "message": "Add new organization" + }, "myProvider": { "message": "My Provider" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "Collection access is restricted" }, + "readOnlyCollectionAccess": { + "message": "You do not have access to manage this collection." + }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "viewCollection": { + "message": "View collection" + }, "restrictedGroupAccess": { "message": "You cannot add yourself to groups." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Items" + }, + "assignedSeats": { + "message": "Assigned seats" + }, + "assigned": { + "message": "Assigned" + }, + "used": { + "message": "Used" + }, + "remaining": { + "message": "Remaining" + }, + "unlinkOrganization": { + "message": "Unlink organization" + }, + "manageSeats": { + "message": "MANAGE SEATS" + }, + "manageSeatsDescription": { + "message": "Adjustments to seats will be reflected in the next billing cycle." + }, + "unassignedSeatsDescription": { + "message": "Unassigned subscription seats" + }, + "purchaseSeatDescription": { + "message": "Additional seats purchased" + }, + "assignedSeatCannotUpdate": { + "message": "Assigned Seats can not be updated. Please contact your organization owner for assistance." + }, + "subscriptionUpdateFailed": { + "message": "Subscription update failed" + }, + "trial": { + "message": "Trial", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Past due", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Subscription expired", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Unpaid invoice", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "To reactivate your subscription, please resolve the past due invoices.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Cancellation date", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + }, + "machineAccount": { + "message": "Machine account", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Machine accounts", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "New machine account", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Create a new machine account to get started automating secret access.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Nothing to show yet", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Delete machine accounts", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Delete machine account", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "View machine account", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Deleting machine accounts is permanent and irreversible." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Delete $COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Machine account deleted" + }, + "deleteMachineAccountsToast": { + "message": "Machine accounts deleted" + }, + "searchMachineAccounts": { + "message": "Search machine accounts", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Edit machine account", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Machine account name", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Machine account created", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Machine account updated", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Grant machine accounts access to this project." + }, + "projectMachineAccountsSelectHint": { + "message": "Type or select machine accounts" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Add machine accounts to grant access" + }, + "machineAccountPeopleDescription": { + "message": "Grant groups or people access to this machine account." + }, + "machineAccountProjectsDescription": { + "message": "Assign projects to this machine account. " + }, + "createMachineAccount": { + "message": "Create a machine account" + }, + "maPeopleWarningMessage": { + "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Remove access to this machine account" + }, + "smAccessRemovalWarningMaMessage": { + "message": "This action will remove your access to the machine account." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ machine accounts included", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ per month for additional machine accounts", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Additional machine accounts" + }, + "includedMachineAccounts": { + "message": "Your plan comes with $COUNT$ machine accounts.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "You can add additional machine accounts for $COST$ per month.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Limit machine accounts (optional)" + }, + "limitMachineAccountsDesc": { + "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + }, + "machineAccountLimit": { + "message": "Machine account limit (optional)" + }, + "maxMachineAccountCost": { + "message": "Max potential machine account cost" + }, + "machineAccountAccessUpdated": { + "message": "Machine account access updated" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/web/src/locales/bs/messages.json b/apps/web/src/locales/bs/messages.json index 84084847d4..a0033e67fe 100644 --- a/apps/web/src/locales/bs/messages.json +++ b/apps/web/src/locales/bs/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Add existing organization" }, + "addNewOrganization": { + "message": "Add new organization" + }, "myProvider": { "message": "My Provider" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "Collection access is restricted" }, + "readOnlyCollectionAccess": { + "message": "You do not have access to manage this collection." + }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "viewCollection": { + "message": "View collection" + }, "restrictedGroupAccess": { "message": "You cannot add yourself to groups." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Items" + }, + "assignedSeats": { + "message": "Assigned seats" + }, + "assigned": { + "message": "Assigned" + }, + "used": { + "message": "Used" + }, + "remaining": { + "message": "Remaining" + }, + "unlinkOrganization": { + "message": "Unlink organization" + }, + "manageSeats": { + "message": "MANAGE SEATS" + }, + "manageSeatsDescription": { + "message": "Adjustments to seats will be reflected in the next billing cycle." + }, + "unassignedSeatsDescription": { + "message": "Unassigned subscription seats" + }, + "purchaseSeatDescription": { + "message": "Additional seats purchased" + }, + "assignedSeatCannotUpdate": { + "message": "Assigned Seats can not be updated. Please contact your organization owner for assistance." + }, + "subscriptionUpdateFailed": { + "message": "Subscription update failed" + }, + "trial": { + "message": "Trial", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Past due", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Subscription expired", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Unpaid invoice", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "To reactivate your subscription, please resolve the past due invoices.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Cancellation date", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + }, + "machineAccount": { + "message": "Machine account", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Machine accounts", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "New machine account", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Create a new machine account to get started automating secret access.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Nothing to show yet", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Delete machine accounts", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Delete machine account", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "View machine account", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Deleting machine accounts is permanent and irreversible." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Delete $COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Machine account deleted" + }, + "deleteMachineAccountsToast": { + "message": "Machine accounts deleted" + }, + "searchMachineAccounts": { + "message": "Search machine accounts", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Edit machine account", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Machine account name", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Machine account created", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Machine account updated", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Grant machine accounts access to this project." + }, + "projectMachineAccountsSelectHint": { + "message": "Type or select machine accounts" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Add machine accounts to grant access" + }, + "machineAccountPeopleDescription": { + "message": "Grant groups or people access to this machine account." + }, + "machineAccountProjectsDescription": { + "message": "Assign projects to this machine account. " + }, + "createMachineAccount": { + "message": "Create a machine account" + }, + "maPeopleWarningMessage": { + "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Remove access to this machine account" + }, + "smAccessRemovalWarningMaMessage": { + "message": "This action will remove your access to the machine account." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ machine accounts included", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ per month for additional machine accounts", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Additional machine accounts" + }, + "includedMachineAccounts": { + "message": "Your plan comes with $COUNT$ machine accounts.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "You can add additional machine accounts for $COST$ per month.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Limit machine accounts (optional)" + }, + "limitMachineAccountsDesc": { + "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + }, + "machineAccountLimit": { + "message": "Machine account limit (optional)" + }, + "maxMachineAccountCost": { + "message": "Max potential machine account cost" + }, + "machineAccountAccessUpdated": { + "message": "Machine account access updated" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/web/src/locales/ca/messages.json b/apps/web/src/locales/ca/messages.json index e23f7acec4..97d870febc 100644 --- a/apps/web/src/locales/ca/messages.json +++ b/apps/web/src/locales/ca/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Afig organització existent" }, + "addNewOrganization": { + "message": "Afegeix organització nova" + }, "myProvider": { "message": "El meu proveïdor" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "L'accés a la col·lecció està restringit" }, + "readOnlyCollectionAccess": { + "message": "No teniu accés per gestionar aquesta col·lecció." + }, "grantCollectionAccess": { "message": "Concedeix als grups o membres l'accés a aquesta col·lecció." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Portal del proveïdor" }, + "viewCollection": { + "message": "Mostra col·lecció" + }, "restrictedGroupAccess": { "message": "No podeu afegir-vos als grups." }, @@ -7607,13 +7616,13 @@ "message": "No podeu afegir-vos a les col·leccions." }, "assign": { - "message": "Assign" + "message": "Assigna" }, "assignToCollections": { - "message": "Assign to collections" + "message": "Assigna a col·leccions" }, "assignToTheseCollections": { - "message": "Assign to these collections" + "message": "Assigna a aquestes col·leccions" }, "bulkCollectionAssignmentDialogDescription": { "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." @@ -7642,5 +7651,256 @@ }, "items": { "message": "Items" + }, + "assignedSeats": { + "message": "Assigned seats" + }, + "assigned": { + "message": "Assigned" + }, + "used": { + "message": "Used" + }, + "remaining": { + "message": "Remaining" + }, + "unlinkOrganization": { + "message": "Unlink organization" + }, + "manageSeats": { + "message": "MANAGE SEATS" + }, + "manageSeatsDescription": { + "message": "Adjustments to seats will be reflected in the next billing cycle." + }, + "unassignedSeatsDescription": { + "message": "Unassigned subscription seats" + }, + "purchaseSeatDescription": { + "message": "Additional seats purchased" + }, + "assignedSeatCannotUpdate": { + "message": "Assigned Seats can not be updated. Please contact your organization owner for assistance." + }, + "subscriptionUpdateFailed": { + "message": "Subscription update failed" + }, + "trial": { + "message": "Trial", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Past due", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Subscription expired", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Unpaid invoice", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "To reactivate your subscription, please resolve the past due invoices.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Cancellation date", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + }, + "machineAccount": { + "message": "Machine account", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Machine accounts", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "New machine account", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Create a new machine account to get started automating secret access.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Nothing to show yet", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Delete machine accounts", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Delete machine account", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "View machine account", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Deleting machine accounts is permanent and irreversible." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Delete $COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Machine account deleted" + }, + "deleteMachineAccountsToast": { + "message": "Machine accounts deleted" + }, + "searchMachineAccounts": { + "message": "Search machine accounts", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Edit machine account", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Machine account name", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Machine account created", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Machine account updated", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Grant machine accounts access to this project." + }, + "projectMachineAccountsSelectHint": { + "message": "Type or select machine accounts" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Add machine accounts to grant access" + }, + "machineAccountPeopleDescription": { + "message": "Grant groups or people access to this machine account." + }, + "machineAccountProjectsDescription": { + "message": "Assign projects to this machine account. " + }, + "createMachineAccount": { + "message": "Create a machine account" + }, + "maPeopleWarningMessage": { + "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Remove access to this machine account" + }, + "smAccessRemovalWarningMaMessage": { + "message": "This action will remove your access to the machine account." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ machine accounts included", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ per month for additional machine accounts", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Additional machine accounts" + }, + "includedMachineAccounts": { + "message": "Your plan comes with $COUNT$ machine accounts.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "You can add additional machine accounts for $COST$ per month.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Limit machine accounts (optional)" + }, + "limitMachineAccountsDesc": { + "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + }, + "machineAccountLimit": { + "message": "Machine account limit (optional)" + }, + "maxMachineAccountCost": { + "message": "Max potential machine account cost" + }, + "machineAccountAccessUpdated": { + "message": "Machine account access updated" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/web/src/locales/cs/messages.json b/apps/web/src/locales/cs/messages.json index 523f1faa10..e8816e9dd6 100644 --- a/apps/web/src/locales/cs/messages.json +++ b/apps/web/src/locales/cs/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Přidat existující organizaci" }, + "addNewOrganization": { + "message": "Přidat novou organizaci" + }, "myProvider": { "message": "Můj poskytovatel" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "Přístup ke kolekci je omezen" }, + "readOnlyCollectionAccess": { + "message": "Nemáte přístup ke správě této kolekce." + }, "grantCollectionAccess": { "message": "Udělí skupinám nebo členům přístup k této kolekci." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Portál poskytovatele" }, + "viewCollection": { + "message": "Zobrazit kolekci" + }, "restrictedGroupAccess": { "message": "Do skupin nemůžete přidat sami sebe." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Položky" + }, + "assignedSeats": { + "message": "Přiřazení uživatelé" + }, + "assigned": { + "message": "Přiřazeno" + }, + "used": { + "message": "Využito" + }, + "remaining": { + "message": "Zbývá" + }, + "unlinkOrganization": { + "message": "Odpojit organizaci" + }, + "manageSeats": { + "message": "SPRAVOVAT UŽIVATELE" + }, + "manageSeatsDescription": { + "message": "Nastavení uživatelů bude započítáno v dalším fakturačním cyklu." + }, + "unassignedSeatsDescription": { + "message": "Nepřiřazení uživatelé k odběru" + }, + "purchaseSeatDescription": { + "message": "Další zakoupení uživatelé" + }, + "assignedSeatCannotUpdate": { + "message": "Přiřazení uživatelé nemohou být aktualizováni. Požádejte o pomoc vlastníka organizace." + }, + "subscriptionUpdateFailed": { + "message": "Aktualizace předplatného se nezdařila" + }, + "trial": { + "message": "Zkušební verze", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Po splatnosti", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Předplatné vypršelo", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "Máte lhůtu k odkladu $DAYS$ dnů od data vypršení předplatného pro zachování předplatného. Vyřešte poslední splatné faktury od $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "Máte lhůtu k odkladu $DAYS$ dnů od data, kdy byla Vaše první nezaplacená faktura udržována v předplatném. Vyřešte poslední splatné faktury od $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Nezaplacená faktura", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "Chcete-li znovu aktivovat předplatné, vyřešte poslední splatné faktury.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Datum zrušení", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Strojové účty nelze vytvořit v pozastavených organizacích. Požádejte o pomoc vlastníka organizace." + }, + "machineAccount": { + "message": "Strojový účet", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Strojové účty", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "Nový strojový účet", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Abyste mohli začít automatizovat přístup k tajnému kliči, vytvořte nový strojový účet.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Nyní není nic k zobrazení", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Smazat strojové účty", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Smazat strojový účet", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "Zobrazit strojový účet", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Smazání strojového účtu $MACHINE_ACCOUNT$ je trvalé a nevratné.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Smazání strojových účtů je trvalé a nevratné." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Smazat $COUNT$ strojových účtů", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Strojový účet byl smazán" + }, + "deleteMachineAccountsToast": { + "message": "Strojové účty byly smazány" + }, + "searchMachineAccounts": { + "message": "Prohledat strojové účty", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Upravit strojový účet", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Název strojového účtu", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Strojový účet byl vytvořen", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Strojový účet byl aktualizován", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Udělí strojovým účtům přístup k tomuto projektu." + }, + "projectMachineAccountsSelectHint": { + "message": "Napište nebo vyberte strojové účty" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Přidejte strojové účty pro udělení přístupu" + }, + "machineAccountPeopleDescription": { + "message": "Udělí skupinám nebo lidem přístup k tomuto strojovému účtu." + }, + "machineAccountProjectsDescription": { + "message": "Přiřaďte projekty k tomuto strojovému účtu. " + }, + "createMachineAccount": { + "message": "Vytvořit strojový účet" + }, + "maPeopleWarningMessage": { + "message": "Odebráním osob ze strojového účtu se neodeberou přístupové tokeny, které tyto osoby vytvořily. V rámci osvědčených bezpečnostních postupů se doporučuje zrušit přístupové tokeny vytvořené osobami odebranými ze strojového účtu." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Odebrat přístup k tomuto strojovému účtu" + }, + "smAccessRemovalWarningMaMessage": { + "message": "Tato akce odebere Váš přístup ke strojovému účtu." + }, + "machineAccountsIncluded": { + "message": "Zahrnuto $COUNT$ strojových účtů", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "Pro další strojové účty $COST$ měsíčně", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Další strojové účty" + }, + "includedMachineAccounts": { + "message": "Váš plán již obsahuje $COUNT$ strojových účtů.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "Za $COST$ měsíčně můžete přidat další strojové účty.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Omezit strojové účty (volitelné)" + }, + "limitMachineAccountsDesc": { + "message": "Nastavte limit pro Vaše strojové účty. Jakmile tohoto limitu dosáhnete, nebudete moci vytvářet nové strojové účty." + }, + "machineAccountLimit": { + "message": "Limit strojových účtů (volitelné)" + }, + "maxMachineAccountCost": { + "message": "Max. potenciální náklady na strojový účet" + }, + "machineAccountAccessUpdated": { + "message": "Přístup strojového účtu byl aktualizován" + }, + "unassignedItemsBanner": { + "message": "Upozornění: Nepřiřazené položky organizace již nejsou viditelné ve Vašem zobrazení všech trezorů napříč zařízeními 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é." } } diff --git a/apps/web/src/locales/cy/messages.json b/apps/web/src/locales/cy/messages.json index 4781d9d3b6..8883fd78cb 100644 --- a/apps/web/src/locales/cy/messages.json +++ b/apps/web/src/locales/cy/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Add existing organization" }, + "addNewOrganization": { + "message": "Add new organization" + }, "myProvider": { "message": "My Provider" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "Collection access is restricted" }, + "readOnlyCollectionAccess": { + "message": "You do not have access to manage this collection." + }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "viewCollection": { + "message": "View collection" + }, "restrictedGroupAccess": { "message": "You cannot add yourself to groups." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Items" + }, + "assignedSeats": { + "message": "Assigned seats" + }, + "assigned": { + "message": "Assigned" + }, + "used": { + "message": "Used" + }, + "remaining": { + "message": "Remaining" + }, + "unlinkOrganization": { + "message": "Unlink organization" + }, + "manageSeats": { + "message": "MANAGE SEATS" + }, + "manageSeatsDescription": { + "message": "Adjustments to seats will be reflected in the next billing cycle." + }, + "unassignedSeatsDescription": { + "message": "Unassigned subscription seats" + }, + "purchaseSeatDescription": { + "message": "Additional seats purchased" + }, + "assignedSeatCannotUpdate": { + "message": "Assigned Seats can not be updated. Please contact your organization owner for assistance." + }, + "subscriptionUpdateFailed": { + "message": "Subscription update failed" + }, + "trial": { + "message": "Trial", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Past due", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Subscription expired", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Unpaid invoice", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "To reactivate your subscription, please resolve the past due invoices.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Cancellation date", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + }, + "machineAccount": { + "message": "Machine account", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Machine accounts", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "New machine account", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Create a new machine account to get started automating secret access.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Nothing to show yet", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Delete machine accounts", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Delete machine account", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "View machine account", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Deleting machine accounts is permanent and irreversible." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Delete $COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Machine account deleted" + }, + "deleteMachineAccountsToast": { + "message": "Machine accounts deleted" + }, + "searchMachineAccounts": { + "message": "Search machine accounts", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Edit machine account", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Machine account name", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Machine account created", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Machine account updated", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Grant machine accounts access to this project." + }, + "projectMachineAccountsSelectHint": { + "message": "Type or select machine accounts" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Add machine accounts to grant access" + }, + "machineAccountPeopleDescription": { + "message": "Grant groups or people access to this machine account." + }, + "machineAccountProjectsDescription": { + "message": "Assign projects to this machine account. " + }, + "createMachineAccount": { + "message": "Create a machine account" + }, + "maPeopleWarningMessage": { + "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Remove access to this machine account" + }, + "smAccessRemovalWarningMaMessage": { + "message": "This action will remove your access to the machine account." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ machine accounts included", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ per month for additional machine accounts", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Additional machine accounts" + }, + "includedMachineAccounts": { + "message": "Your plan comes with $COUNT$ machine accounts.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "You can add additional machine accounts for $COST$ per month.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Limit machine accounts (optional)" + }, + "limitMachineAccountsDesc": { + "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + }, + "machineAccountLimit": { + "message": "Machine account limit (optional)" + }, + "maxMachineAccountCost": { + "message": "Max potential machine account cost" + }, + "machineAccountAccessUpdated": { + "message": "Machine account access updated" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/web/src/locales/da/messages.json b/apps/web/src/locales/da/messages.json index bcb1c16235..683c1e7862 100644 --- a/apps/web/src/locales/da/messages.json +++ b/apps/web/src/locales/da/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Tilføj eksisterende organisation" }, + "addNewOrganization": { + "message": "Tilføj ny organisation" + }, "myProvider": { "message": "Min udbyder" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "Adgang til samling er begrænset" }, + "readOnlyCollectionAccess": { + "message": "Du har ikke adgang til at håndtere denne samling." + }, "grantCollectionAccess": { "message": "Tildel grupper eller medlemmer adgang til denne samling." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Udbyderportal" }, + "viewCollection": { + "message": "Vis samling" + }, "restrictedGroupAccess": { "message": "Man kan ikke føje sig selv til grupper." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Emner" + }, + "assignedSeats": { + "message": "Tildelte pladser" + }, + "assigned": { + "message": "Tildelte" + }, + "used": { + "message": "Brugte" + }, + "remaining": { + "message": "Resterer" + }, + "unlinkOrganization": { + "message": "Fjern associering til organisation" + }, + "manageSeats": { + "message": "HÅNDTERING AF PLADSER" + }, + "manageSeatsDescription": { + "message": "Justeringer af pladser afspejles i den næste faktureringsperiode." + }, + "unassignedSeatsDescription": { + "message": "Ikke-tildelte abonnementspladser" + }, + "purchaseSeatDescription": { + "message": "Yderligere pladser tilkøbt" + }, + "assignedSeatCannotUpdate": { + "message": "Tildelte pladser kan ikke opdateres. Kontakt organisationsejeren for assistance." + }, + "subscriptionUpdateFailed": { + "message": "Abonnementsopdatering mislykkedes" + }, + "trial": { + "message": "Prøveperiode", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Forfalden", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Abonnement udløbet", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "Der er en respitperiode på $DAYS$ dage fra abonnementsudløbsdatoen til at bevare abonnementet. Afregn venligst de forfaldne fakturaer inden $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "Der er en respitperiode på $DAYS$ dage fra første fakturaforfaldsdato til at bevare abonnementet. Afregn venligst de forfaldne fakturaer inden $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Ubetalt faktura", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "Afregn de tidligere forfaldne fakturaer for at genaktivere abonnementet.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Cancellation date", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Maskinekonti kan ikke oprettes i suspenderede organisationer. Kontakt organisationsejeren for hjælp." + }, + "machineAccount": { + "message": "Maskinekonto", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Maskinekonti", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "Ny maskinekonto", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Opret en ny maskinekonto for at komme i gang med at automatisere hemmelig adgang.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Intet at vise endnu", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Slet maskinekonti", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Slet maskinekonto", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "Vis maskinekonto", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Sletning af maskinekontoen $MACHINE_ACCOUNT$ er permanent og irreversibel.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Sletning af maskinekonti er permanent og irreversibel." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Slet $COUNT$ maskinekonti", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Maskinekonto slettet" + }, + "deleteMachineAccountsToast": { + "message": "Maskinekonti slettet" + }, + "searchMachineAccounts": { + "message": "Søg efter maskinekonti", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Redigér maskinekonto", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Maskinekontonavn", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Maskinekonto oprettet", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Maskinekonto opdateret", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Tildel maskinkonti adgang til dette projekt." + }, + "projectMachineAccountsSelectHint": { + "message": "Angiv eller vælg maskinekonti" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Tilføj maskinekonti for at tildele adgang" + }, + "machineAccountPeopleDescription": { + "message": "Tildel grupper eller personer adgang til denne maskinekonto." + }, + "machineAccountProjectsDescription": { + "message": "Tildel projekter til denne maskinekonto. " + }, + "createMachineAccount": { + "message": "Opret en maskinekonto" + }, + "maPeopleWarningMessage": { + "message": "Fjernelse af personer fra en maskinekonto fjerner ikke deres oprettede adgangstokener. For bedste sikkerhedspraksis anbefales ophævelse af adgangtokener oprettet af personer fjernet fra en maskinekonto." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Fjern adgang til denne maskinekonto" + }, + "smAccessRemovalWarningMaMessage": { + "message": "Denne handling fjerner maskinekontoadgangen." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ maskinekonti inkluderet", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ pr. måned for yderligere maskinekonti", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Yderligere maskinekonti" + }, + "includedMachineAccounts": { + "message": "Abonnementstypen er inkl. $COUNT$ maskinekonti.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "Yderligere maskinekonti kan tilføjes for $COST$ pr. måned.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Begræns maskinekonti (valgfrit)" + }, + "limitMachineAccountsDesc": { + "message": "Opsæt kvote for maskinekonti. Når kvoten er opbrugt, vil nye maskinekonti ikke kunne oprettes." + }, + "machineAccountLimit": { + "message": "Maskinekontokvote (valgfrit)" + }, + "maxMachineAccountCost": { + "message": "Maks. potentiel maskinekontoomkostning" + }, + "machineAccountAccessUpdated": { + "message": "Maskinekontoadgang opdateret" + }, + "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." } } diff --git a/apps/web/src/locales/de/messages.json b/apps/web/src/locales/de/messages.json index c3e5a9bba5..2679bc189e 100644 --- a/apps/web/src/locales/de/messages.json +++ b/apps/web/src/locales/de/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Bestehende Organisation hinzufügen" }, + "addNewOrganization": { + "message": "Neue Organisation erstellen" + }, "myProvider": { "message": "Mein Anbieter" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "Zugriff auf Sammlungen ist eingeschränkt" }, + "readOnlyCollectionAccess": { + "message": "Du hast keinen Zugriff, um diese Sammlung zu verwalten." + }, "grantCollectionAccess": { "message": "Gewähre Gruppen oder Mitgliedern Zugriff auf diese Sammlung." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Anbieterportal" }, + "viewCollection": { + "message": "Sammlung anzeigen" + }, "restrictedGroupAccess": { "message": "Du kannst dich nicht selbst zu Gruppen hinzufügen." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Einträge" + }, + "assignedSeats": { + "message": "Zugewiesene Benutzerplätze" + }, + "assigned": { + "message": "Zugewiesen" + }, + "used": { + "message": "Belegt" + }, + "remaining": { + "message": "Verbleibend" + }, + "unlinkOrganization": { + "message": "Organisations-Verknüpfung aufheben" + }, + "manageSeats": { + "message": "Benutzerpätze verwalten" + }, + "manageSeatsDescription": { + "message": "Die Anpassung der Benutzerplätze wird im nächsten Abrechnungszeitraum berücksichtigt." + }, + "unassignedSeatsDescription": { + "message": "Nicht zugewiesene Abonnement-Benutzerplätze" + }, + "purchaseSeatDescription": { + "message": "Zusätzliche gekaufte Benutzerplätze" + }, + "assignedSeatCannotUpdate": { + "message": "Zugewiesene Benutzerplätze können nicht aktualisiert werden. Bitte kontaktiere deinen Organisationseigentümer, um Unterstützung zu erhalten." + }, + "subscriptionUpdateFailed": { + "message": "Aktualisierung des Abonnement fehlgeschlagen" + }, + "trial": { + "message": "Testversion", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Überfällig", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Abonnement abgelaufen", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "Du hast eine Übergangsfrist von $DAYS$ Tagen ab dem Ablaufdatum deines Abonnements, um dein Abonnement weiterzuführen. Bitte begleiche die letzten fälligen Rechnungen bis zum $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "Du hast eine Übergangsfrist von $DAYS$ Tagen ab dem Fälligkeitsdatum deiner ersten unbezahlten Rechnung, um dein Abonnement weiterzuführen. Bitte begleiche die überfälligen Rechnungen bis zum $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Unbezahlte Rechnung", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "Um dein Abonnement zu reaktivieren, begleiche bitte die letzten überfälligen Rechnungen.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Kündigungsdatum", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Gerätekonten können in deaktivierten Organisationen nicht erstellt werden. Bitte kontaktiere deinen Organisationseigentümer, um Unterstützung zu erhalten." + }, + "machineAccount": { + "message": "Gerätekonto", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Gerätekonten", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "Neues Gerätekonto", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Erstelle ein neues Gerätekonto, um mit der Automation für Zugriffe auf Geheimnisse zu beginnen.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Hier gibt es noch nichts", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Gerätekonten löschen", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Gerätekonto löschen", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "Gerätekonto anzeigen", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Das Löschen des Gerätekontos $MACHINE_ACCOUNT$ ist dauerhaft und unwiderruflich.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Das Löschen von Gerätekonten ist dauerhaft und unwiderruflich." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "$COUNT$ Gerätekonten löschen", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Gerätekonto gelöscht" + }, + "deleteMachineAccountsToast": { + "message": "Gerätekonten gelöscht" + }, + "searchMachineAccounts": { + "message": "Gerätekonten suchen", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Gerätekonto bearbeiten", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Name des Gerätekontos", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Neues Gerätekonto erstellt", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Gerätekonto aktualisiert", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Gerätekonten Zugriff auf dieses Projekt erlauben." + }, + "projectMachineAccountsSelectHint": { + "message": "Gerätekonten suchen oder auswählen" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Gerätekonten hinzufügen, um Zugriff zu gewähren" + }, + "machineAccountPeopleDescription": { + "message": "Gruppen oder Personen Zugriff auf dieses Gerätekonto gewähren." + }, + "machineAccountProjectsDescription": { + "message": "Diesem Gerätekonto Projekte zuweisen. " + }, + "createMachineAccount": { + "message": "Gerätekonto erstellen" + }, + "maPeopleWarningMessage": { + "message": "Das Entfernen von Personen aus einem Gerätekonto entfernt nicht die von ihnen erstellten Zugriffstoken. Aus Sicherheitsgründen wird empfohlen, Zugriffstoken zu widerrufen, die von Personen erstellt wurden, die aus einem Gerätekonto entfernt wurden." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Zugriff auf dieses Gerätekonto entfernen" + }, + "smAccessRemovalWarningMaMessage": { + "message": "Diese Aktion wird deinen Zugriff auf das Gerätekonto entfernen." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ Gerätekonten enthalten", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ pro Monat für zusätzliche Gerätekonten", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Zusätzliche Gerätekonten" + }, + "includedMachineAccounts": { + "message": "Dein Tarif enthält $COUNT$ Gerätekonten.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "Du kannst zusätzliche Gerätekonten für $COST$ pro Monat hinzufügen.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Gerätekonten begrenzen (optional)" + }, + "limitMachineAccountsDesc": { + "message": "Lege ein Limit für deine Gerätekonten fest. Sobald dieses Limit erreicht ist, kannst du keine neuen Gerätekonten mehr erstellen." + }, + "machineAccountLimit": { + "message": "Gerätekonten-Limit (optional)" + }, + "maxMachineAccountCost": { + "message": "Maximal mögliche Kosten des Gerätekontos" + }, + "machineAccountAccessUpdated": { + "message": "Zugriff auf Gerätekonto aktualisiert" + }, + "unassignedItemsBanner": { + "message": "Hinweis: Nicht zugewiesene Organisationseinträge sind nicht mehr geräteübergreifend in der Ansicht aller Tresore sichtbar und sind nun nur über die Administrator-Konsole zugänglich. Weise diese Einträge einer Sammlung aus der Administrator-Konsole zu, um sie sichtbar zu machen." } } diff --git a/apps/web/src/locales/el/messages.json b/apps/web/src/locales/el/messages.json index 0babeee15d..4502d940b2 100644 --- a/apps/web/src/locales/el/messages.json +++ b/apps/web/src/locales/el/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Προσθήκη Υπάρχοντος Οργανισμού" }, + "addNewOrganization": { + "message": "Add new organization" + }, "myProvider": { "message": "Ο Πάροχος Μου" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "Collection access is restricted" }, + "readOnlyCollectionAccess": { + "message": "You do not have access to manage this collection." + }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "viewCollection": { + "message": "View collection" + }, "restrictedGroupAccess": { "message": "You cannot add yourself to groups." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Items" + }, + "assignedSeats": { + "message": "Assigned seats" + }, + "assigned": { + "message": "Assigned" + }, + "used": { + "message": "Used" + }, + "remaining": { + "message": "Remaining" + }, + "unlinkOrganization": { + "message": "Unlink organization" + }, + "manageSeats": { + "message": "MANAGE SEATS" + }, + "manageSeatsDescription": { + "message": "Adjustments to seats will be reflected in the next billing cycle." + }, + "unassignedSeatsDescription": { + "message": "Unassigned subscription seats" + }, + "purchaseSeatDescription": { + "message": "Additional seats purchased" + }, + "assignedSeatCannotUpdate": { + "message": "Assigned Seats can not be updated. Please contact your organization owner for assistance." + }, + "subscriptionUpdateFailed": { + "message": "Subscription update failed" + }, + "trial": { + "message": "Trial", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Past due", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Subscription expired", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Unpaid invoice", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "To reactivate your subscription, please resolve the past due invoices.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Cancellation date", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + }, + "machineAccount": { + "message": "Machine account", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Machine accounts", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "New machine account", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Create a new machine account to get started automating secret access.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Nothing to show yet", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Delete machine accounts", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Delete machine account", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "View machine account", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Deleting machine accounts is permanent and irreversible." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Delete $COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Machine account deleted" + }, + "deleteMachineAccountsToast": { + "message": "Machine accounts deleted" + }, + "searchMachineAccounts": { + "message": "Search machine accounts", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Edit machine account", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Machine account name", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Machine account created", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Machine account updated", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Grant machine accounts access to this project." + }, + "projectMachineAccountsSelectHint": { + "message": "Type or select machine accounts" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Add machine accounts to grant access" + }, + "machineAccountPeopleDescription": { + "message": "Grant groups or people access to this machine account." + }, + "machineAccountProjectsDescription": { + "message": "Assign projects to this machine account. " + }, + "createMachineAccount": { + "message": "Create a machine account" + }, + "maPeopleWarningMessage": { + "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Remove access to this machine account" + }, + "smAccessRemovalWarningMaMessage": { + "message": "This action will remove your access to the machine account." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ machine accounts included", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ per month for additional machine accounts", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Additional machine accounts" + }, + "includedMachineAccounts": { + "message": "Your plan comes with $COUNT$ machine accounts.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "You can add additional machine accounts for $COST$ per month.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Limit machine accounts (optional)" + }, + "limitMachineAccountsDesc": { + "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + }, + "machineAccountLimit": { + "message": "Machine account limit (optional)" + }, + "maxMachineAccountCost": { + "message": "Max potential machine account cost" + }, + "machineAccountAccessUpdated": { + "message": "Machine account access updated" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/web/src/locales/en_GB/messages.json b/apps/web/src/locales/en_GB/messages.json index d70d6cff23..88d67c51b4 100644 --- a/apps/web/src/locales/en_GB/messages.json +++ b/apps/web/src/locales/en_GB/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Add existing organisation" }, + "addNewOrganization": { + "message": "Add new organization" + }, "myProvider": { "message": "My Provider" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "Collection access is restricted" }, + "readOnlyCollectionAccess": { + "message": "You do not have access to manage this collection." + }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "viewCollection": { + "message": "View collection" + }, "restrictedGroupAccess": { "message": "You cannot add yourself to groups." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Items" + }, + "assignedSeats": { + "message": "Assigned seats" + }, + "assigned": { + "message": "Assigned" + }, + "used": { + "message": "Used" + }, + "remaining": { + "message": "Remaining" + }, + "unlinkOrganization": { + "message": "Unlink organization" + }, + "manageSeats": { + "message": "MANAGE SEATS" + }, + "manageSeatsDescription": { + "message": "Adjustments to seats will be reflected in the next billing cycle." + }, + "unassignedSeatsDescription": { + "message": "Unassigned subscription seats" + }, + "purchaseSeatDescription": { + "message": "Additional seats purchased" + }, + "assignedSeatCannotUpdate": { + "message": "Assigned Seats can not be updated. Please contact your organization owner for assistance." + }, + "subscriptionUpdateFailed": { + "message": "Subscription update failed" + }, + "trial": { + "message": "Trial", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Past due", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Subscription expired", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Unpaid invoice", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "To reactivate your subscription, please resolve the past due invoices.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Cancellation date", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + }, + "machineAccount": { + "message": "Machine account", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Machine accounts", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "New machine account", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Create a new machine account to get started automating secret access.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Nothing to show yet", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Delete machine accounts", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Delete machine account", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "View machine account", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Deleting machine accounts is permanent and irreversible." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Delete $COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Machine account deleted" + }, + "deleteMachineAccountsToast": { + "message": "Machine accounts deleted" + }, + "searchMachineAccounts": { + "message": "Search machine accounts", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Edit machine account", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Machine account name", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Machine account created", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Machine account updated", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Grant machine accounts access to this project." + }, + "projectMachineAccountsSelectHint": { + "message": "Type or select machine accounts" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Add machine accounts to grant access" + }, + "machineAccountPeopleDescription": { + "message": "Grant groups or people access to this machine account." + }, + "machineAccountProjectsDescription": { + "message": "Assign projects to this machine account. " + }, + "createMachineAccount": { + "message": "Create a machine account" + }, + "maPeopleWarningMessage": { + "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Remove access to this machine account" + }, + "smAccessRemovalWarningMaMessage": { + "message": "This action will remove your access to the machine account." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ machine accounts included", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ per month for additional machine accounts", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Additional machine accounts" + }, + "includedMachineAccounts": { + "message": "Your plan comes with $COUNT$ machine accounts.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "You can add additional machine accounts for $COST$ per month.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Limit machine accounts (optional)" + }, + "limitMachineAccountsDesc": { + "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + }, + "machineAccountLimit": { + "message": "Machine account limit (optional)" + }, + "maxMachineAccountCost": { + "message": "Max potential machine account cost" + }, + "machineAccountAccessUpdated": { + "message": "Machine account access updated" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/web/src/locales/en_IN/messages.json b/apps/web/src/locales/en_IN/messages.json index 561d267cab..ea3ff3f395 100644 --- a/apps/web/src/locales/en_IN/messages.json +++ b/apps/web/src/locales/en_IN/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Add existing organization" }, + "addNewOrganization": { + "message": "Add new organization" + }, "myProvider": { "message": "My Provider" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "Collection access is restricted" }, + "readOnlyCollectionAccess": { + "message": "You do not have access to manage this collection." + }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "viewCollection": { + "message": "View collection" + }, "restrictedGroupAccess": { "message": "You cannot add yourself to groups." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Items" + }, + "assignedSeats": { + "message": "Assigned seats" + }, + "assigned": { + "message": "Assigned" + }, + "used": { + "message": "Used" + }, + "remaining": { + "message": "Remaining" + }, + "unlinkOrganization": { + "message": "Unlink organization" + }, + "manageSeats": { + "message": "MANAGE SEATS" + }, + "manageSeatsDescription": { + "message": "Adjustments to seats will be reflected in the next billing cycle." + }, + "unassignedSeatsDescription": { + "message": "Unassigned subscription seats" + }, + "purchaseSeatDescription": { + "message": "Additional seats purchased" + }, + "assignedSeatCannotUpdate": { + "message": "Assigned Seats can not be updated. Please contact your organization owner for assistance." + }, + "subscriptionUpdateFailed": { + "message": "Subscription update failed" + }, + "trial": { + "message": "Trial", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Past due", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Subscription expired", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Unpaid invoice", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "To reactivate your subscription, please resolve the past due invoices.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Cancellation date", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + }, + "machineAccount": { + "message": "Machine account", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Machine accounts", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "New machine account", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Create a new machine account to get started automating secret access.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Nothing to show yet", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Delete machine accounts", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Delete machine account", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "View machine account", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Deleting machine accounts is permanent and irreversible." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Delete $COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Machine account deleted" + }, + "deleteMachineAccountsToast": { + "message": "Machine accounts deleted" + }, + "searchMachineAccounts": { + "message": "Search machine accounts", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Edit machine account", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Machine account name", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Machine account created", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Machine account updated", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Grant machine accounts access to this project." + }, + "projectMachineAccountsSelectHint": { + "message": "Type or select machine accounts" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Add machine accounts to grant access" + }, + "machineAccountPeopleDescription": { + "message": "Grant groups or people access to this machine account." + }, + "machineAccountProjectsDescription": { + "message": "Assign projects to this machine account. " + }, + "createMachineAccount": { + "message": "Create a machine account" + }, + "maPeopleWarningMessage": { + "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Remove access to this machine account" + }, + "smAccessRemovalWarningMaMessage": { + "message": "This action will remove your access to the machine account." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ machine accounts included", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ per month for additional machine accounts", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Additional machine accounts" + }, + "includedMachineAccounts": { + "message": "Your plan comes with $COUNT$ machine accounts.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "You can add additional machine accounts for $COST$ per month.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Limit machine accounts (optional)" + }, + "limitMachineAccountsDesc": { + "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + }, + "machineAccountLimit": { + "message": "Machine account limit (optional)" + }, + "maxMachineAccountCost": { + "message": "Max potential machine account cost" + }, + "machineAccountAccessUpdated": { + "message": "Machine account access updated" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/web/src/locales/eo/messages.json b/apps/web/src/locales/eo/messages.json index d3a9ad414a..929a83d4fa 100644 --- a/apps/web/src/locales/eo/messages.json +++ b/apps/web/src/locales/eo/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Add existing organization" }, + "addNewOrganization": { + "message": "Add new organization" + }, "myProvider": { "message": "My Provider" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "Collection access is restricted" }, + "readOnlyCollectionAccess": { + "message": "You do not have access to manage this collection." + }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "viewCollection": { + "message": "View collection" + }, "restrictedGroupAccess": { "message": "You cannot add yourself to groups." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Items" + }, + "assignedSeats": { + "message": "Assigned seats" + }, + "assigned": { + "message": "Assigned" + }, + "used": { + "message": "Used" + }, + "remaining": { + "message": "Remaining" + }, + "unlinkOrganization": { + "message": "Unlink organization" + }, + "manageSeats": { + "message": "MANAGE SEATS" + }, + "manageSeatsDescription": { + "message": "Adjustments to seats will be reflected in the next billing cycle." + }, + "unassignedSeatsDescription": { + "message": "Unassigned subscription seats" + }, + "purchaseSeatDescription": { + "message": "Additional seats purchased" + }, + "assignedSeatCannotUpdate": { + "message": "Assigned Seats can not be updated. Please contact your organization owner for assistance." + }, + "subscriptionUpdateFailed": { + "message": "Subscription update failed" + }, + "trial": { + "message": "Trial", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Past due", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Subscription expired", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Unpaid invoice", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "To reactivate your subscription, please resolve the past due invoices.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Cancellation date", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + }, + "machineAccount": { + "message": "Machine account", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Machine accounts", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "New machine account", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Create a new machine account to get started automating secret access.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Nothing to show yet", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Delete machine accounts", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Delete machine account", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "View machine account", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Deleting machine accounts is permanent and irreversible." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Delete $COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Machine account deleted" + }, + "deleteMachineAccountsToast": { + "message": "Machine accounts deleted" + }, + "searchMachineAccounts": { + "message": "Search machine accounts", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Edit machine account", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Machine account name", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Machine account created", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Machine account updated", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Grant machine accounts access to this project." + }, + "projectMachineAccountsSelectHint": { + "message": "Type or select machine accounts" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Add machine accounts to grant access" + }, + "machineAccountPeopleDescription": { + "message": "Grant groups or people access to this machine account." + }, + "machineAccountProjectsDescription": { + "message": "Assign projects to this machine account. " + }, + "createMachineAccount": { + "message": "Create a machine account" + }, + "maPeopleWarningMessage": { + "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Remove access to this machine account" + }, + "smAccessRemovalWarningMaMessage": { + "message": "This action will remove your access to the machine account." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ machine accounts included", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ per month for additional machine accounts", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Additional machine accounts" + }, + "includedMachineAccounts": { + "message": "Your plan comes with $COUNT$ machine accounts.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "You can add additional machine accounts for $COST$ per month.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Limit machine accounts (optional)" + }, + "limitMachineAccountsDesc": { + "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + }, + "machineAccountLimit": { + "message": "Machine account limit (optional)" + }, + "maxMachineAccountCost": { + "message": "Max potential machine account cost" + }, + "machineAccountAccessUpdated": { + "message": "Machine account access updated" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/web/src/locales/es/messages.json b/apps/web/src/locales/es/messages.json index 250614565d..8f9d2993eb 100644 --- a/apps/web/src/locales/es/messages.json +++ b/apps/web/src/locales/es/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Añadir una organización existente" }, + "addNewOrganization": { + "message": "Add new organization" + }, "myProvider": { "message": "Mi proveedor" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "Collection access is restricted" }, + "readOnlyCollectionAccess": { + "message": "You do not have access to manage this collection." + }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "viewCollection": { + "message": "View collection" + }, "restrictedGroupAccess": { "message": "You cannot add yourself to groups." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Items" + }, + "assignedSeats": { + "message": "Assigned seats" + }, + "assigned": { + "message": "Assigned" + }, + "used": { + "message": "Used" + }, + "remaining": { + "message": "Remaining" + }, + "unlinkOrganization": { + "message": "Unlink organization" + }, + "manageSeats": { + "message": "MANAGE SEATS" + }, + "manageSeatsDescription": { + "message": "Adjustments to seats will be reflected in the next billing cycle." + }, + "unassignedSeatsDescription": { + "message": "Unassigned subscription seats" + }, + "purchaseSeatDescription": { + "message": "Additional seats purchased" + }, + "assignedSeatCannotUpdate": { + "message": "Assigned Seats can not be updated. Please contact your organization owner for assistance." + }, + "subscriptionUpdateFailed": { + "message": "Subscription update failed" + }, + "trial": { + "message": "Trial", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Past due", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Subscription expired", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Unpaid invoice", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "To reactivate your subscription, please resolve the past due invoices.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Cancellation date", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + }, + "machineAccount": { + "message": "Machine account", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Machine accounts", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "New machine account", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Create a new machine account to get started automating secret access.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Nothing to show yet", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Delete machine accounts", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Delete machine account", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "View machine account", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Deleting machine accounts is permanent and irreversible." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Delete $COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Machine account deleted" + }, + "deleteMachineAccountsToast": { + "message": "Machine accounts deleted" + }, + "searchMachineAccounts": { + "message": "Search machine accounts", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Edit machine account", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Machine account name", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Machine account created", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Machine account updated", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Grant machine accounts access to this project." + }, + "projectMachineAccountsSelectHint": { + "message": "Type or select machine accounts" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Add machine accounts to grant access" + }, + "machineAccountPeopleDescription": { + "message": "Grant groups or people access to this machine account." + }, + "machineAccountProjectsDescription": { + "message": "Assign projects to this machine account. " + }, + "createMachineAccount": { + "message": "Create a machine account" + }, + "maPeopleWarningMessage": { + "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Remove access to this machine account" + }, + "smAccessRemovalWarningMaMessage": { + "message": "This action will remove your access to the machine account." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ machine accounts included", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ per month for additional machine accounts", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Additional machine accounts" + }, + "includedMachineAccounts": { + "message": "Your plan comes with $COUNT$ machine accounts.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "You can add additional machine accounts for $COST$ per month.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Limit machine accounts (optional)" + }, + "limitMachineAccountsDesc": { + "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + }, + "machineAccountLimit": { + "message": "Machine account limit (optional)" + }, + "maxMachineAccountCost": { + "message": "Max potential machine account cost" + }, + "machineAccountAccessUpdated": { + "message": "Machine account access updated" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/web/src/locales/et/messages.json b/apps/web/src/locales/et/messages.json index 1866c649e4..b41732103f 100644 --- a/apps/web/src/locales/et/messages.json +++ b/apps/web/src/locales/et/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Add existing organization" }, + "addNewOrganization": { + "message": "Add new organization" + }, "myProvider": { "message": "My Provider" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "Collection access is restricted" }, + "readOnlyCollectionAccess": { + "message": "You do not have access to manage this collection." + }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "viewCollection": { + "message": "View collection" + }, "restrictedGroupAccess": { "message": "You cannot add yourself to groups." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Items" + }, + "assignedSeats": { + "message": "Assigned seats" + }, + "assigned": { + "message": "Assigned" + }, + "used": { + "message": "Used" + }, + "remaining": { + "message": "Remaining" + }, + "unlinkOrganization": { + "message": "Unlink organization" + }, + "manageSeats": { + "message": "MANAGE SEATS" + }, + "manageSeatsDescription": { + "message": "Adjustments to seats will be reflected in the next billing cycle." + }, + "unassignedSeatsDescription": { + "message": "Unassigned subscription seats" + }, + "purchaseSeatDescription": { + "message": "Additional seats purchased" + }, + "assignedSeatCannotUpdate": { + "message": "Assigned Seats can not be updated. Please contact your organization owner for assistance." + }, + "subscriptionUpdateFailed": { + "message": "Subscription update failed" + }, + "trial": { + "message": "Trial", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Past due", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Subscription expired", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Unpaid invoice", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "To reactivate your subscription, please resolve the past due invoices.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Cancellation date", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + }, + "machineAccount": { + "message": "Machine account", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Machine accounts", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "New machine account", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Create a new machine account to get started automating secret access.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Nothing to show yet", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Delete machine accounts", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Delete machine account", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "View machine account", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Deleting machine accounts is permanent and irreversible." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Delete $COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Machine account deleted" + }, + "deleteMachineAccountsToast": { + "message": "Machine accounts deleted" + }, + "searchMachineAccounts": { + "message": "Search machine accounts", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Edit machine account", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Machine account name", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Machine account created", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Machine account updated", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Grant machine accounts access to this project." + }, + "projectMachineAccountsSelectHint": { + "message": "Type or select machine accounts" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Add machine accounts to grant access" + }, + "machineAccountPeopleDescription": { + "message": "Grant groups or people access to this machine account." + }, + "machineAccountProjectsDescription": { + "message": "Assign projects to this machine account. " + }, + "createMachineAccount": { + "message": "Create a machine account" + }, + "maPeopleWarningMessage": { + "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Remove access to this machine account" + }, + "smAccessRemovalWarningMaMessage": { + "message": "This action will remove your access to the machine account." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ machine accounts included", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ per month for additional machine accounts", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Additional machine accounts" + }, + "includedMachineAccounts": { + "message": "Your plan comes with $COUNT$ machine accounts.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "You can add additional machine accounts for $COST$ per month.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Limit machine accounts (optional)" + }, + "limitMachineAccountsDesc": { + "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + }, + "machineAccountLimit": { + "message": "Machine account limit (optional)" + }, + "maxMachineAccountCost": { + "message": "Max potential machine account cost" + }, + "machineAccountAccessUpdated": { + "message": "Machine account access updated" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/web/src/locales/eu/messages.json b/apps/web/src/locales/eu/messages.json index 0c35039424..a57e36221a 100644 --- a/apps/web/src/locales/eu/messages.json +++ b/apps/web/src/locales/eu/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Gehitu existitzen den erakunde bat" }, + "addNewOrganization": { + "message": "Add new organization" + }, "myProvider": { "message": "Nire hornitzailea" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "Collection access is restricted" }, + "readOnlyCollectionAccess": { + "message": "You do not have access to manage this collection." + }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "viewCollection": { + "message": "View collection" + }, "restrictedGroupAccess": { "message": "You cannot add yourself to groups." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Items" + }, + "assignedSeats": { + "message": "Assigned seats" + }, + "assigned": { + "message": "Assigned" + }, + "used": { + "message": "Used" + }, + "remaining": { + "message": "Remaining" + }, + "unlinkOrganization": { + "message": "Unlink organization" + }, + "manageSeats": { + "message": "MANAGE SEATS" + }, + "manageSeatsDescription": { + "message": "Adjustments to seats will be reflected in the next billing cycle." + }, + "unassignedSeatsDescription": { + "message": "Unassigned subscription seats" + }, + "purchaseSeatDescription": { + "message": "Additional seats purchased" + }, + "assignedSeatCannotUpdate": { + "message": "Assigned Seats can not be updated. Please contact your organization owner for assistance." + }, + "subscriptionUpdateFailed": { + "message": "Subscription update failed" + }, + "trial": { + "message": "Trial", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Past due", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Subscription expired", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Unpaid invoice", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "To reactivate your subscription, please resolve the past due invoices.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Cancellation date", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + }, + "machineAccount": { + "message": "Machine account", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Machine accounts", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "New machine account", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Create a new machine account to get started automating secret access.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Nothing to show yet", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Delete machine accounts", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Delete machine account", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "View machine account", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Deleting machine accounts is permanent and irreversible." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Delete $COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Machine account deleted" + }, + "deleteMachineAccountsToast": { + "message": "Machine accounts deleted" + }, + "searchMachineAccounts": { + "message": "Search machine accounts", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Edit machine account", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Machine account name", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Machine account created", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Machine account updated", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Grant machine accounts access to this project." + }, + "projectMachineAccountsSelectHint": { + "message": "Type or select machine accounts" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Add machine accounts to grant access" + }, + "machineAccountPeopleDescription": { + "message": "Grant groups or people access to this machine account." + }, + "machineAccountProjectsDescription": { + "message": "Assign projects to this machine account. " + }, + "createMachineAccount": { + "message": "Create a machine account" + }, + "maPeopleWarningMessage": { + "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Remove access to this machine account" + }, + "smAccessRemovalWarningMaMessage": { + "message": "This action will remove your access to the machine account." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ machine accounts included", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ per month for additional machine accounts", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Additional machine accounts" + }, + "includedMachineAccounts": { + "message": "Your plan comes with $COUNT$ machine accounts.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "You can add additional machine accounts for $COST$ per month.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Limit machine accounts (optional)" + }, + "limitMachineAccountsDesc": { + "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + }, + "machineAccountLimit": { + "message": "Machine account limit (optional)" + }, + "maxMachineAccountCost": { + "message": "Max potential machine account cost" + }, + "machineAccountAccessUpdated": { + "message": "Machine account access updated" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/web/src/locales/fa/messages.json b/apps/web/src/locales/fa/messages.json index 2c3d10c6a8..0298bbc64b 100644 --- a/apps/web/src/locales/fa/messages.json +++ b/apps/web/src/locales/fa/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "سازمان موجود را اضافه کنید" }, + "addNewOrganization": { + "message": "Add new organization" + }, "myProvider": { "message": "ارائه دهنده‌ی من" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "Collection access is restricted" }, + "readOnlyCollectionAccess": { + "message": "You do not have access to manage this collection." + }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "viewCollection": { + "message": "View collection" + }, "restrictedGroupAccess": { "message": "You cannot add yourself to groups." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Items" + }, + "assignedSeats": { + "message": "Assigned seats" + }, + "assigned": { + "message": "Assigned" + }, + "used": { + "message": "Used" + }, + "remaining": { + "message": "Remaining" + }, + "unlinkOrganization": { + "message": "Unlink organization" + }, + "manageSeats": { + "message": "MANAGE SEATS" + }, + "manageSeatsDescription": { + "message": "Adjustments to seats will be reflected in the next billing cycle." + }, + "unassignedSeatsDescription": { + "message": "Unassigned subscription seats" + }, + "purchaseSeatDescription": { + "message": "Additional seats purchased" + }, + "assignedSeatCannotUpdate": { + "message": "Assigned Seats can not be updated. Please contact your organization owner for assistance." + }, + "subscriptionUpdateFailed": { + "message": "Subscription update failed" + }, + "trial": { + "message": "Trial", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Past due", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Subscription expired", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Unpaid invoice", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "To reactivate your subscription, please resolve the past due invoices.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Cancellation date", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + }, + "machineAccount": { + "message": "Machine account", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Machine accounts", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "New machine account", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Create a new machine account to get started automating secret access.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Nothing to show yet", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Delete machine accounts", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Delete machine account", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "View machine account", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Deleting machine accounts is permanent and irreversible." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Delete $COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Machine account deleted" + }, + "deleteMachineAccountsToast": { + "message": "Machine accounts deleted" + }, + "searchMachineAccounts": { + "message": "Search machine accounts", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Edit machine account", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Machine account name", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Machine account created", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Machine account updated", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Grant machine accounts access to this project." + }, + "projectMachineAccountsSelectHint": { + "message": "Type or select machine accounts" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Add machine accounts to grant access" + }, + "machineAccountPeopleDescription": { + "message": "Grant groups or people access to this machine account." + }, + "machineAccountProjectsDescription": { + "message": "Assign projects to this machine account. " + }, + "createMachineAccount": { + "message": "Create a machine account" + }, + "maPeopleWarningMessage": { + "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Remove access to this machine account" + }, + "smAccessRemovalWarningMaMessage": { + "message": "This action will remove your access to the machine account." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ machine accounts included", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ per month for additional machine accounts", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Additional machine accounts" + }, + "includedMachineAccounts": { + "message": "Your plan comes with $COUNT$ machine accounts.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "You can add additional machine accounts for $COST$ per month.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Limit machine accounts (optional)" + }, + "limitMachineAccountsDesc": { + "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + }, + "machineAccountLimit": { + "message": "Machine account limit (optional)" + }, + "maxMachineAccountCost": { + "message": "Max potential machine account cost" + }, + "machineAccountAccessUpdated": { + "message": "Machine account access updated" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/web/src/locales/fi/messages.json b/apps/web/src/locales/fi/messages.json index 43bbf01847..8875133d01 100644 --- a/apps/web/src/locales/fi/messages.json +++ b/apps/web/src/locales/fi/messages.json @@ -603,7 +603,7 @@ "message": "Ei" }, "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." }, "loginWithDevice": { "message": "Laitteella kirjautuminen" @@ -3168,7 +3168,7 @@ } }, "unlinkedSsoUser": { - "message": "Irrotti käyttäjän \"$ID$\" kertakirjautumisesta.", + "message": "Poisti kertakirjautumisen käyttäjältä \"$ID$\".", "placeholders": { "id": { "content": "$1", @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Lisää olemassa oleva organisaatio" }, + "addNewOrganization": { + "message": "Lisää uusi organisaatio" + }, "myProvider": { "message": "Oma toimittaja" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "Kokoelman käyttöä on rajoitettu" }, + "readOnlyCollectionAccess": { + "message": "Oikeutesi eivät riitä kokoelman hallintaan." + }, "grantCollectionAccess": { "message": "Myönnä ryhmille tai henkilöille kokoelman käyttöoikeus." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Toimittajaportaali" }, + "viewCollection": { + "message": "Tarkastele kokoelmaa" + }, "restrictedGroupAccess": { "message": "Et voi lisätä itseäsi ryhmiin." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Kohteet" + }, + "assignedSeats": { + "message": "Määritetyt käyttäjäpaikat" + }, + "assigned": { + "message": "Määritetty" + }, + "used": { + "message": "Käytetty" + }, + "remaining": { + "message": "Jäljellä" + }, + "unlinkOrganization": { + "message": "Irrota organisaatio" + }, + "manageSeats": { + "message": "HALLITSE KÄYTTÄJÄPAIKKOJA" + }, + "manageSeatsDescription": { + "message": "Käyttäjäpaikkojen muutokset näkyvät seuraavalla laskutuskaudella." + }, + "unassignedSeatsDescription": { + "message": "Tilauksen määrittämättömät käyttäjäpaikat" + }, + "purchaseSeatDescription": { + "message": "Käyttäjäpaikkoja ostettu lisää" + }, + "assignedSeatCannotUpdate": { + "message": "Määritettyjä käyttäjäpaikkoja ei ole mahdollista päivittää. Ole yhteydessä organisaatiosi omistajaan saadaksesi apua." + }, + "subscriptionUpdateFailed": { + "message": "Tilausmuutos epäonnistui" + }, + "trial": { + "message": "Kokeilu", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Erääntynyt", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Tilaus on päättynyt", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "Sinulla on $DAYS$ päivän varoaika tilauksesi säilyttämiseksi. Maksa erääntyneet laskut $SUSPENSION_DATE$ mennessä.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "Sinulla on $DAYS$ päivän varoaika ensimmäisen erääntyneen laskusi eräpäivästä tilauksesi säilyttämiseksi. Maksa erääntyneet laskut $SUSPENSION_DATE$ mennessä.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Maksamaton lasku", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "Palauta tilauksesi käyttöön maksamalla erääntyneet laskut.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Peruutuspäivä", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Konetilien luonti ei ole mahdollista jäädytetyissä organisaatioissa. Ole yhteydessä organisaatiosi omistajaan saadaksesi apua." + }, + "machineAccount": { + "message": "Konetili", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Konetilit", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "Uusi konetili", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Aloita salaisen käytön automatisointi luomalla uusi konetili.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Ei vielä mitään näytettävää", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Poista konetilit", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Poista konetili", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "Näytä konetili", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Konetilin $MACHINE_ACCOUNT$ poisto on pysyvää ja peruuttamatonta.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Konetilien poisto on pysyvää ja peruuttamatonta." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Poista $COUNT$ konetiliä", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Konetili poistettiin" + }, + "deleteMachineAccountsToast": { + "message": "Konetilit poistettiin" + }, + "searchMachineAccounts": { + "message": "Etsi konetileistä", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Muokkaa konetiliä", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Konetilin nimi", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Konetili luotiin", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Konetili päivitettiin", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Myönnä konetileille käyttöoikeus projektiin." + }, + "projectMachineAccountsSelectHint": { + "message": "Syötä tai valitse konetilit" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Myönnä käyttöoikeuksia lisäämällä konetilejä" + }, + "machineAccountPeopleDescription": { + "message": "Myönnä ryhmille tai henkilöille tämän konetilin käyttöoikeus." + }, + "machineAccountProjectsDescription": { + "message": "Määritä konetilille projekteja. " + }, + "createMachineAccount": { + "message": "Luo konetili" + }, + "maPeopleWarningMessage": { + "message": "Henkilöiden poistaminen konetililtä ei poista heidän luomiaan käyttötunnisteita. Parasta suojauskäytäntöä varten on suositeltavaa mitätöidä konetililtä poistettujen henkilöiden luomat tunnukset." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Poista konetilin käyttöoikeus" + }, + "smAccessRemovalWarningMaMessage": { + "message": "Tämä poistaa käyttöoikeutesi konetiliin." + }, + "machineAccountsIncluded": { + "message": "Sisältää $COUNT$ konetiliä", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "Jokainen lisätty konetili kustantaa $COST$ kuukaudessa", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Lisättävät konetilit" + }, + "includedMachineAccounts": { + "message": "Tilaukseesi sisältyy $COUNT$ konetiliä.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "Voit hankkia lisää konetilejä hintaan $COST$/tili/kuukausi.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Rajoita konetilien määrää (valinnainen)" + }, + "limitMachineAccountsDesc": { + "message": "Aseta tilaukseesi sisältyvä konetilien määrä. Määrän täytyttyä et voi luoda uusia konetilejä." + }, + "machineAccountLimit": { + "message": "Konetilien enimmäismäärä (valinnainen)" + }, + "maxMachineAccountCost": { + "message": "Konetilin mahdollinen enimmäiskustannus" + }, + "machineAccountAccessUpdated": { + "message": "Konetilin oikeuksia muutettiin" + }, + "unassignedItemsBanner": { + "message": "Huomautus: Organisaation kohteita, joita ei ole lisätty kokoelmaan, ei enää näytetä laitteiden \"Kaikki holvit\" -näkymissä, ja jatkossa ne näkyvät vain hallintakonsolin kautta. Lisää kohteet kokoelmiin, jotta ne ovat laitteilla käytettävissä." } } diff --git a/apps/web/src/locales/fil/messages.json b/apps/web/src/locales/fil/messages.json index bfcdccc7a2..7664321333 100644 --- a/apps/web/src/locales/fil/messages.json +++ b/apps/web/src/locales/fil/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Magdagdag ng umiiral na organisasyon" }, + "addNewOrganization": { + "message": "Add new organization" + }, "myProvider": { "message": "Ang Aking Provider" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "Collection access is restricted" }, + "readOnlyCollectionAccess": { + "message": "You do not have access to manage this collection." + }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "viewCollection": { + "message": "View collection" + }, "restrictedGroupAccess": { "message": "You cannot add yourself to groups." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Items" + }, + "assignedSeats": { + "message": "Assigned seats" + }, + "assigned": { + "message": "Assigned" + }, + "used": { + "message": "Used" + }, + "remaining": { + "message": "Remaining" + }, + "unlinkOrganization": { + "message": "Unlink organization" + }, + "manageSeats": { + "message": "MANAGE SEATS" + }, + "manageSeatsDescription": { + "message": "Adjustments to seats will be reflected in the next billing cycle." + }, + "unassignedSeatsDescription": { + "message": "Unassigned subscription seats" + }, + "purchaseSeatDescription": { + "message": "Additional seats purchased" + }, + "assignedSeatCannotUpdate": { + "message": "Assigned Seats can not be updated. Please contact your organization owner for assistance." + }, + "subscriptionUpdateFailed": { + "message": "Subscription update failed" + }, + "trial": { + "message": "Trial", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Past due", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Subscription expired", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Unpaid invoice", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "To reactivate your subscription, please resolve the past due invoices.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Cancellation date", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + }, + "machineAccount": { + "message": "Machine account", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Machine accounts", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "New machine account", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Create a new machine account to get started automating secret access.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Nothing to show yet", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Delete machine accounts", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Delete machine account", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "View machine account", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Deleting machine accounts is permanent and irreversible." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Delete $COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Machine account deleted" + }, + "deleteMachineAccountsToast": { + "message": "Machine accounts deleted" + }, + "searchMachineAccounts": { + "message": "Search machine accounts", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Edit machine account", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Machine account name", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Machine account created", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Machine account updated", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Grant machine accounts access to this project." + }, + "projectMachineAccountsSelectHint": { + "message": "Type or select machine accounts" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Add machine accounts to grant access" + }, + "machineAccountPeopleDescription": { + "message": "Grant groups or people access to this machine account." + }, + "machineAccountProjectsDescription": { + "message": "Assign projects to this machine account. " + }, + "createMachineAccount": { + "message": "Create a machine account" + }, + "maPeopleWarningMessage": { + "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Remove access to this machine account" + }, + "smAccessRemovalWarningMaMessage": { + "message": "This action will remove your access to the machine account." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ machine accounts included", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ per month for additional machine accounts", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Additional machine accounts" + }, + "includedMachineAccounts": { + "message": "Your plan comes with $COUNT$ machine accounts.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "You can add additional machine accounts for $COST$ per month.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Limit machine accounts (optional)" + }, + "limitMachineAccountsDesc": { + "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + }, + "machineAccountLimit": { + "message": "Machine account limit (optional)" + }, + "maxMachineAccountCost": { + "message": "Max potential machine account cost" + }, + "machineAccountAccessUpdated": { + "message": "Machine account access updated" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/web/src/locales/fr/messages.json b/apps/web/src/locales/fr/messages.json index 9cc40e4133..57132fbe9a 100644 --- a/apps/web/src/locales/fr/messages.json +++ b/apps/web/src/locales/fr/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Ajouter une organisation existante" }, + "addNewOrganization": { + "message": "Ajouter une nouvelle organisation" + }, "myProvider": { "message": "Mon fournisseur" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "L'accès à la collection est restreint" }, + "readOnlyCollectionAccess": { + "message": "Vous n'avez pas accès à la gestion de cette collection." + }, "grantCollectionAccess": { "message": "Accorder l'accès à cette collection aux groupes ou aux membres." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Portail fournisseur" }, + "viewCollection": { + "message": "Afficher la Collection" + }, "restrictedGroupAccess": { "message": "Vous ne pouvez vous ajoutez vous-même aux groupes." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Éléments" + }, + "assignedSeats": { + "message": "Places assignées" + }, + "assigned": { + "message": "Assigné" + }, + "used": { + "message": "Utilisé" + }, + "remaining": { + "message": "Restant" + }, + "unlinkOrganization": { + "message": "Délier l'organisation" + }, + "manageSeats": { + "message": "GÉRER LES PLACES" + }, + "manageSeatsDescription": { + "message": "Les ajustements aux licences seront reflétés lors du prochain cycle de facturation." + }, + "unassignedSeatsDescription": { + "message": "Licenses d'abonnement non assignés" + }, + "purchaseSeatDescription": { + "message": "Licenses additionnelles achetées" + }, + "assignedSeatCannotUpdate": { + "message": "Les Licences assignées ne peuvent être mises à jour. Veuillez contacter le propriétaire de votre organisation pour assistance." + }, + "subscriptionUpdateFailed": { + "message": "Mise à jour de l'abonnement échouée" + }, + "trial": { + "message": "Essai", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Échéance dépassée", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Abonnement expiré", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "Vous bénéficiez d'une période de grace de $DAYS$ jours suivants la date d'expiration de votre abonnement pour le maintenir. Veuillez adresser les paiements en souffrance pour les factures dont les échéances sont passées d'ici le $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "Vous bénéficiez d'une période de grace de $DAYS$ jours à partir de la première journée de votre première facture impayée pour maintenir votre abonnement. Veuillez adresser les paiements en souffrance pour les factures dont les échéances sont passées d'ici le $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Facture impayée", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "Pour réactiver votre abonnement, veuillez adresser les paiements en souffrance pour les factures dont les échéances sont passées.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Date d'annulation", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Les comptes de machine ne peuvent pas être créés dans les organisations suspendues. Veuillez contacter le propriétaire de votre organisation pour obtenir de l'aide." + }, + "machineAccount": { + "message": "Compte machine", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Comptes de machine", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "Nouveau compte machine", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Créez un nouveau compte machine pour commencer à automatiser l'accès secret.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Rien à afficher pour l'instant", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Supprimer les comptes de machine", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Supprimer le compte machine", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "Afficher le compte machine", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "La suppression du compte machine $MACHINE_ACCOUNT$ est permanente et irréversible.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "La suppression des comptes de la machine est permanente et irréversible." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Supprimer $COUNT$ comptes machines", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Compte de machine supprimé" + }, + "deleteMachineAccountsToast": { + "message": "Comptes de machine supprimés" + }, + "searchMachineAccounts": { + "message": "Rechercher des comptes de machine", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Éditer le compte machine", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Nom du compte machine", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Compte de machine créé", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Compte de la machine mis à jour", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Accorder l'accès à ce projet aux comptes de machine." + }, + "projectMachineAccountsSelectHint": { + "message": "Saisissez ou sélectionnez les comptes de machine" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Ajouter des comptes de machine pour accorder l'accès" + }, + "machineAccountPeopleDescription": { + "message": "Accorder l'accès à ce compte à des groupes ou à des personnes." + }, + "machineAccountProjectsDescription": { + "message": "Assigner des projets à ce compte machine. " + }, + "createMachineAccount": { + "message": "Créer un compte machine" + }, + "maPeopleWarningMessage": { + "message": "La suppression de personnes d'un compte machine ne supprime pas les jetons d'accès qu'ils ont créés. Pour de meilleures pratiques en matière de sécurité, il est recommandé de révoquer les jetons d'accès créés par des personnes supprimées d'un compte machine." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Retirer l'accès à ce compte machine" + }, + "smAccessRemovalWarningMaMessage": { + "message": "Cette action supprimera votre accès au compte de la machine." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ comptes machines inclus", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ par mois pour des comptes machines supplémentaires", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Comptes de machine supplémentaires" + }, + "includedMachineAccounts": { + "message": "Votre forfait est livré avec $COUNT$ comptes machines.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "Vous pouvez ajouter des comptes machines supplémentaires pour $COST$ par mois.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Limiter les comptes de la machine (facultatif)" + }, + "limitMachineAccountsDesc": { + "message": "Définissez une limite pour vos comptes machines. Une fois cette limite atteinte, vous ne pourrez plus créer de nouveaux comptes machines." + }, + "machineAccountLimit": { + "message": "Limite du compte de la machine (facultatif)" + }, + "maxMachineAccountCost": { + "message": "Coût potentiel maximal de compte machine" + }, + "machineAccountAccessUpdated": { + "message": "Accès au compte machine mis à jour" + }, + "unassignedItemsBanner": { + "message": "Remarque : Les éléments d'organisation non assignés ne sont plus visibles dans la vue tous les coffres sur les appareils et ne sont maintenant accessibles que via la Console Admin. Assigner ces éléments à une collection de la Console Admin pour les rendre visibles." } } diff --git a/apps/web/src/locales/gl/messages.json b/apps/web/src/locales/gl/messages.json index ad51cf605f..abda98207b 100644 --- a/apps/web/src/locales/gl/messages.json +++ b/apps/web/src/locales/gl/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Add existing organization" }, + "addNewOrganization": { + "message": "Add new organization" + }, "myProvider": { "message": "My Provider" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "Collection access is restricted" }, + "readOnlyCollectionAccess": { + "message": "You do not have access to manage this collection." + }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "viewCollection": { + "message": "View collection" + }, "restrictedGroupAccess": { "message": "You cannot add yourself to groups." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Items" + }, + "assignedSeats": { + "message": "Assigned seats" + }, + "assigned": { + "message": "Assigned" + }, + "used": { + "message": "Used" + }, + "remaining": { + "message": "Remaining" + }, + "unlinkOrganization": { + "message": "Unlink organization" + }, + "manageSeats": { + "message": "MANAGE SEATS" + }, + "manageSeatsDescription": { + "message": "Adjustments to seats will be reflected in the next billing cycle." + }, + "unassignedSeatsDescription": { + "message": "Unassigned subscription seats" + }, + "purchaseSeatDescription": { + "message": "Additional seats purchased" + }, + "assignedSeatCannotUpdate": { + "message": "Assigned Seats can not be updated. Please contact your organization owner for assistance." + }, + "subscriptionUpdateFailed": { + "message": "Subscription update failed" + }, + "trial": { + "message": "Trial", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Past due", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Subscription expired", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Unpaid invoice", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "To reactivate your subscription, please resolve the past due invoices.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Cancellation date", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + }, + "machineAccount": { + "message": "Machine account", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Machine accounts", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "New machine account", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Create a new machine account to get started automating secret access.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Nothing to show yet", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Delete machine accounts", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Delete machine account", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "View machine account", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Deleting machine accounts is permanent and irreversible." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Delete $COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Machine account deleted" + }, + "deleteMachineAccountsToast": { + "message": "Machine accounts deleted" + }, + "searchMachineAccounts": { + "message": "Search machine accounts", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Edit machine account", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Machine account name", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Machine account created", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Machine account updated", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Grant machine accounts access to this project." + }, + "projectMachineAccountsSelectHint": { + "message": "Type or select machine accounts" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Add machine accounts to grant access" + }, + "machineAccountPeopleDescription": { + "message": "Grant groups or people access to this machine account." + }, + "machineAccountProjectsDescription": { + "message": "Assign projects to this machine account. " + }, + "createMachineAccount": { + "message": "Create a machine account" + }, + "maPeopleWarningMessage": { + "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Remove access to this machine account" + }, + "smAccessRemovalWarningMaMessage": { + "message": "This action will remove your access to the machine account." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ machine accounts included", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ per month for additional machine accounts", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Additional machine accounts" + }, + "includedMachineAccounts": { + "message": "Your plan comes with $COUNT$ machine accounts.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "You can add additional machine accounts for $COST$ per month.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Limit machine accounts (optional)" + }, + "limitMachineAccountsDesc": { + "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + }, + "machineAccountLimit": { + "message": "Machine account limit (optional)" + }, + "maxMachineAccountCost": { + "message": "Max potential machine account cost" + }, + "machineAccountAccessUpdated": { + "message": "Machine account access updated" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/web/src/locales/he/messages.json b/apps/web/src/locales/he/messages.json index 0c8316fe49..bcbea04c28 100644 --- a/apps/web/src/locales/he/messages.json +++ b/apps/web/src/locales/he/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Add existing organization" }, + "addNewOrganization": { + "message": "Add new organization" + }, "myProvider": { "message": "My Provider" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "Collection access is restricted" }, + "readOnlyCollectionAccess": { + "message": "You do not have access to manage this collection." + }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "viewCollection": { + "message": "View collection" + }, "restrictedGroupAccess": { "message": "You cannot add yourself to groups." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Items" + }, + "assignedSeats": { + "message": "Assigned seats" + }, + "assigned": { + "message": "Assigned" + }, + "used": { + "message": "Used" + }, + "remaining": { + "message": "Remaining" + }, + "unlinkOrganization": { + "message": "Unlink organization" + }, + "manageSeats": { + "message": "MANAGE SEATS" + }, + "manageSeatsDescription": { + "message": "Adjustments to seats will be reflected in the next billing cycle." + }, + "unassignedSeatsDescription": { + "message": "Unassigned subscription seats" + }, + "purchaseSeatDescription": { + "message": "Additional seats purchased" + }, + "assignedSeatCannotUpdate": { + "message": "Assigned Seats can not be updated. Please contact your organization owner for assistance." + }, + "subscriptionUpdateFailed": { + "message": "Subscription update failed" + }, + "trial": { + "message": "Trial", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Past due", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Subscription expired", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Unpaid invoice", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "To reactivate your subscription, please resolve the past due invoices.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Cancellation date", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + }, + "machineAccount": { + "message": "Machine account", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Machine accounts", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "New machine account", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Create a new machine account to get started automating secret access.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Nothing to show yet", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Delete machine accounts", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Delete machine account", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "View machine account", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Deleting machine accounts is permanent and irreversible." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Delete $COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Machine account deleted" + }, + "deleteMachineAccountsToast": { + "message": "Machine accounts deleted" + }, + "searchMachineAccounts": { + "message": "Search machine accounts", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Edit machine account", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Machine account name", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Machine account created", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Machine account updated", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Grant machine accounts access to this project." + }, + "projectMachineAccountsSelectHint": { + "message": "Type or select machine accounts" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Add machine accounts to grant access" + }, + "machineAccountPeopleDescription": { + "message": "Grant groups or people access to this machine account." + }, + "machineAccountProjectsDescription": { + "message": "Assign projects to this machine account. " + }, + "createMachineAccount": { + "message": "Create a machine account" + }, + "maPeopleWarningMessage": { + "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Remove access to this machine account" + }, + "smAccessRemovalWarningMaMessage": { + "message": "This action will remove your access to the machine account." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ machine accounts included", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ per month for additional machine accounts", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Additional machine accounts" + }, + "includedMachineAccounts": { + "message": "Your plan comes with $COUNT$ machine accounts.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "You can add additional machine accounts for $COST$ per month.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Limit machine accounts (optional)" + }, + "limitMachineAccountsDesc": { + "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + }, + "machineAccountLimit": { + "message": "Machine account limit (optional)" + }, + "maxMachineAccountCost": { + "message": "Max potential machine account cost" + }, + "machineAccountAccessUpdated": { + "message": "Machine account access updated" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/web/src/locales/hi/messages.json b/apps/web/src/locales/hi/messages.json index b00c912d7a..892db00010 100644 --- a/apps/web/src/locales/hi/messages.json +++ b/apps/web/src/locales/hi/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Add existing organization" }, + "addNewOrganization": { + "message": "Add new organization" + }, "myProvider": { "message": "My Provider" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "Collection access is restricted" }, + "readOnlyCollectionAccess": { + "message": "You do not have access to manage this collection." + }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "viewCollection": { + "message": "View collection" + }, "restrictedGroupAccess": { "message": "You cannot add yourself to groups." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Items" + }, + "assignedSeats": { + "message": "Assigned seats" + }, + "assigned": { + "message": "Assigned" + }, + "used": { + "message": "Used" + }, + "remaining": { + "message": "Remaining" + }, + "unlinkOrganization": { + "message": "Unlink organization" + }, + "manageSeats": { + "message": "MANAGE SEATS" + }, + "manageSeatsDescription": { + "message": "Adjustments to seats will be reflected in the next billing cycle." + }, + "unassignedSeatsDescription": { + "message": "Unassigned subscription seats" + }, + "purchaseSeatDescription": { + "message": "Additional seats purchased" + }, + "assignedSeatCannotUpdate": { + "message": "Assigned Seats can not be updated. Please contact your organization owner for assistance." + }, + "subscriptionUpdateFailed": { + "message": "Subscription update failed" + }, + "trial": { + "message": "Trial", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Past due", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Subscription expired", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Unpaid invoice", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "To reactivate your subscription, please resolve the past due invoices.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Cancellation date", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + }, + "machineAccount": { + "message": "Machine account", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Machine accounts", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "New machine account", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Create a new machine account to get started automating secret access.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Nothing to show yet", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Delete machine accounts", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Delete machine account", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "View machine account", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Deleting machine accounts is permanent and irreversible." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Delete $COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Machine account deleted" + }, + "deleteMachineAccountsToast": { + "message": "Machine accounts deleted" + }, + "searchMachineAccounts": { + "message": "Search machine accounts", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Edit machine account", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Machine account name", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Machine account created", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Machine account updated", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Grant machine accounts access to this project." + }, + "projectMachineAccountsSelectHint": { + "message": "Type or select machine accounts" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Add machine accounts to grant access" + }, + "machineAccountPeopleDescription": { + "message": "Grant groups or people access to this machine account." + }, + "machineAccountProjectsDescription": { + "message": "Assign projects to this machine account. " + }, + "createMachineAccount": { + "message": "Create a machine account" + }, + "maPeopleWarningMessage": { + "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Remove access to this machine account" + }, + "smAccessRemovalWarningMaMessage": { + "message": "This action will remove your access to the machine account." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ machine accounts included", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ per month for additional machine accounts", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Additional machine accounts" + }, + "includedMachineAccounts": { + "message": "Your plan comes with $COUNT$ machine accounts.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "You can add additional machine accounts for $COST$ per month.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Limit machine accounts (optional)" + }, + "limitMachineAccountsDesc": { + "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + }, + "machineAccountLimit": { + "message": "Machine account limit (optional)" + }, + "maxMachineAccountCost": { + "message": "Max potential machine account cost" + }, + "machineAccountAccessUpdated": { + "message": "Machine account access updated" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/web/src/locales/hr/messages.json b/apps/web/src/locales/hr/messages.json index 4d47c12e86..0915a1a818 100644 --- a/apps/web/src/locales/hr/messages.json +++ b/apps/web/src/locales/hr/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Dodaj postojeću organizaciju" }, + "addNewOrganization": { + "message": "Add new organization" + }, "myProvider": { "message": "Moj davatelj usluga" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "Collection access is restricted" }, + "readOnlyCollectionAccess": { + "message": "You do not have access to manage this collection." + }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "viewCollection": { + "message": "View collection" + }, "restrictedGroupAccess": { "message": "You cannot add yourself to groups." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Items" + }, + "assignedSeats": { + "message": "Assigned seats" + }, + "assigned": { + "message": "Assigned" + }, + "used": { + "message": "Used" + }, + "remaining": { + "message": "Remaining" + }, + "unlinkOrganization": { + "message": "Unlink organization" + }, + "manageSeats": { + "message": "MANAGE SEATS" + }, + "manageSeatsDescription": { + "message": "Adjustments to seats will be reflected in the next billing cycle." + }, + "unassignedSeatsDescription": { + "message": "Unassigned subscription seats" + }, + "purchaseSeatDescription": { + "message": "Additional seats purchased" + }, + "assignedSeatCannotUpdate": { + "message": "Assigned Seats can not be updated. Please contact your organization owner for assistance." + }, + "subscriptionUpdateFailed": { + "message": "Subscription update failed" + }, + "trial": { + "message": "Trial", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Past due", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Subscription expired", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Unpaid invoice", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "To reactivate your subscription, please resolve the past due invoices.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Cancellation date", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + }, + "machineAccount": { + "message": "Machine account", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Machine accounts", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "New machine account", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Create a new machine account to get started automating secret access.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Nothing to show yet", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Delete machine accounts", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Delete machine account", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "View machine account", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Deleting machine accounts is permanent and irreversible." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Delete $COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Machine account deleted" + }, + "deleteMachineAccountsToast": { + "message": "Machine accounts deleted" + }, + "searchMachineAccounts": { + "message": "Search machine accounts", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Edit machine account", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Machine account name", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Machine account created", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Machine account updated", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Grant machine accounts access to this project." + }, + "projectMachineAccountsSelectHint": { + "message": "Type or select machine accounts" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Add machine accounts to grant access" + }, + "machineAccountPeopleDescription": { + "message": "Grant groups or people access to this machine account." + }, + "machineAccountProjectsDescription": { + "message": "Assign projects to this machine account. " + }, + "createMachineAccount": { + "message": "Create a machine account" + }, + "maPeopleWarningMessage": { + "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Remove access to this machine account" + }, + "smAccessRemovalWarningMaMessage": { + "message": "This action will remove your access to the machine account." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ machine accounts included", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ per month for additional machine accounts", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Additional machine accounts" + }, + "includedMachineAccounts": { + "message": "Your plan comes with $COUNT$ machine accounts.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "You can add additional machine accounts for $COST$ per month.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Limit machine accounts (optional)" + }, + "limitMachineAccountsDesc": { + "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + }, + "machineAccountLimit": { + "message": "Machine account limit (optional)" + }, + "maxMachineAccountCost": { + "message": "Max potential machine account cost" + }, + "machineAccountAccessUpdated": { + "message": "Machine account access updated" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/web/src/locales/hu/messages.json b/apps/web/src/locales/hu/messages.json index e1f3682bbd..16ed9d34d9 100644 --- a/apps/web/src/locales/hu/messages.json +++ b/apps/web/src/locales/hu/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Létező fordítások hozzáadása" }, + "addNewOrganization": { + "message": "Új szervezet hozzáadása" + }, "myProvider": { "message": "Saját szolgáltató" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "A gyűjtemény elérése korlátozott." }, + "readOnlyCollectionAccess": { + "message": "Nincs jogosultság ennek a gyűjteménynek a kezelésére." + }, "grantCollectionAccess": { "message": "Adjunk hozzáférést csoportoknak vagy személyeknek eennél a gyűjteménynél." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Szolgáltató portál" }, + "viewCollection": { + "message": "Gyűjtemény megtekintése" + }, "restrictedGroupAccess": { "message": "Nem adhadjuk magunkat a csoporthoz." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Elemek" + }, + "assignedSeats": { + "message": "Hozzárendelt helyek" + }, + "assigned": { + "message": "Hozzárendelve" + }, + "used": { + "message": "Felhasznált" + }, + "remaining": { + "message": "Maradt" + }, + "unlinkOrganization": { + "message": "Szervezet leválasztása" + }, + "manageSeats": { + "message": "HELYEK KEZELÉSE" + }, + "manageSeatsDescription": { + "message": "A helyek módosításai a következő számlázási ciklusban jelennek meg." + }, + "unassignedSeatsDescription": { + "message": "Nem hozzárendelt előfizetési helyek" + }, + "purchaseSeatDescription": { + "message": "További megvásárolt helyek" + }, + "assignedSeatCannotUpdate": { + "message": "A hozzárendelt helyek nem frissíthetők. Segítségért forduljunk a szervezet tulajdonosához." + }, + "subscriptionUpdateFailed": { + "message": "Az előfizetés frissítés sikertelen volt." + }, + "trial": { + "message": "Próba", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Határidőn túl", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Az előfizetés lejárt.", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "Az előfizetés lejárati dátumától számítva $DAYS$ nap türelmi időszak áll rendelkezésére az előfizetés fenntartásához. Rendezzük a lejárt számlákat a következő időpontig: $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "Az első kifizetetlen számla esedékességétől számítva $DAYS$ nap türelmi időszak áll rendelkezésére az előfizetés fenntartására. Rendezzük a lejárt számlákat a következő időpontig: $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Kifizetetlen számla", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "Az előfizetés újraaktiválásához oldjuk meg a lejárt számlákat.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Lemondási dátum", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "A felfüggesztett szervezetekben nem hozhatók létre szolgáltatásfiókok. Segítségért forduljunk a szervezet tulajdonosához." + }, + "machineAccount": { + "message": "Gépi fiók", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Gépi fiókok", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "Új gépi fiók", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Új gépi fiók létrehozása a titkos kód automatikus hozzáféréséhez.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Nincs megjeleníthető tartalom.", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Gépi fiókok törlése", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Gépi fiók törlése", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "Gépi fiók mgtekintése", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "$MACHINE_ACCOUNT$ gépi fiók törlése végleges és visszafordíthatatlan.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "A gépi fiókok törlése végleges és visszafordíthatatlan." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "$COUNT$ gépi fiókok törlése", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "A gépi fiók törlésre került." + }, + "deleteMachineAccountsToast": { + "message": "A gépi fiókok törlésre kerültek." + }, + "searchMachineAccounts": { + "message": "Gépi fiókok keresése", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Gépi fiók szerkesztése", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Gépi fióknév", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "A gépi fiók létrehozásra került.", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "A gépi fiók frissítésre került.", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Gépi fiókok elérésének engedélyezése ehhez a projekthez." + }, + "projectMachineAccountsSelectHint": { + "message": "Írjuk be vagy válasszuk ki a gépi fiókokat." + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Adjunk hozzá gépi fiókokat a hozzáférés biztosításához" + }, + "machineAccountPeopleDescription": { + "message": "Adjunk hozzáférést csoportoknak vagy személyeknek ehhez a gépi fiókhoz." + }, + "machineAccountProjectsDescription": { + "message": "Projektek hozzárendelése ehhez a gépi fiókhoz. " + }, + "createMachineAccount": { + "message": "Gépi fiók létrehozása" + }, + "maPeopleWarningMessage": { + "message": "Az emberek gépi fiókból való eltávolítása nem távolítja el az általuk létrehozott elérési vezérjeleket. A legjobb biztonsági gyakorlat érdekében javasolt visszavonni a gépi fiókból eltávolított személyek által létrehozott elérési vezérjeleket." + }, + "smAccessRemovalWarningMaTitle": { + "message": "A gépi fiók elérés visszavonása" + }, + "smAccessRemovalWarningMaMessage": { + "message": "Ez a művelet eltávolítja a gépi fiók elérési lehetőségét." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ gépi fiók belefoglalva", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ havonta kiegészítő gépi fiókokhoz", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Kiegészítő gépi fiókok" + }, + "includedMachineAccounts": { + "message": "A csomag $COUNT$ gépi fiókot tartalmaz.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "További gépi fiókok adhatók hozzá havi $COST$ áron.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Gépi fiókok korlátozása (opcionális)" + }, + "limitMachineAccountsDesc": { + "message": "Állítsunk be korlátot a gépi fiókokhoz. Ha elérjük ezt a korlátot, nem tudunk új gépi fiókokat létrehozni." + }, + "machineAccountLimit": { + "message": "Gépi fiók korlát (opcionális)" + }, + "maxMachineAccountCost": { + "message": "Maximális lehetséges gépi fiókköltség" + }, + "machineAccountAccessUpdated": { + "message": "A gépi fiók elérése frissítésre került." + }, + "unassignedItemsBanner": { + "message": "Megjegyzés: A nem hozzá rendelt szervezeti elemek már nem láthatók az Összes széf nézetben a különböző eszközökön és mostantól csak a Felügyeleti konzolon keresztül érhetők el. Rendeljük ezeket az elemeket egy gyűjteményhez az Adminisztrátori konzolból, hogy láthatóvá tegyeük azokat." } } diff --git a/apps/web/src/locales/id/messages.json b/apps/web/src/locales/id/messages.json index 83d6c42673..ee0830e6ff 100644 --- a/apps/web/src/locales/id/messages.json +++ b/apps/web/src/locales/id/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Tambah Organisasi yang sudah ada" }, + "addNewOrganization": { + "message": "Add new organization" + }, "myProvider": { "message": "Provider Saya" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "Collection access is restricted" }, + "readOnlyCollectionAccess": { + "message": "You do not have access to manage this collection." + }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "viewCollection": { + "message": "View collection" + }, "restrictedGroupAccess": { "message": "You cannot add yourself to groups." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Items" + }, + "assignedSeats": { + "message": "Assigned seats" + }, + "assigned": { + "message": "Assigned" + }, + "used": { + "message": "Used" + }, + "remaining": { + "message": "Remaining" + }, + "unlinkOrganization": { + "message": "Unlink organization" + }, + "manageSeats": { + "message": "MANAGE SEATS" + }, + "manageSeatsDescription": { + "message": "Adjustments to seats will be reflected in the next billing cycle." + }, + "unassignedSeatsDescription": { + "message": "Unassigned subscription seats" + }, + "purchaseSeatDescription": { + "message": "Additional seats purchased" + }, + "assignedSeatCannotUpdate": { + "message": "Assigned Seats can not be updated. Please contact your organization owner for assistance." + }, + "subscriptionUpdateFailed": { + "message": "Subscription update failed" + }, + "trial": { + "message": "Trial", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Past due", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Subscription expired", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Unpaid invoice", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "To reactivate your subscription, please resolve the past due invoices.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Cancellation date", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + }, + "machineAccount": { + "message": "Machine account", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Machine accounts", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "New machine account", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Create a new machine account to get started automating secret access.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Nothing to show yet", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Delete machine accounts", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Delete machine account", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "View machine account", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Deleting machine accounts is permanent and irreversible." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Delete $COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Machine account deleted" + }, + "deleteMachineAccountsToast": { + "message": "Machine accounts deleted" + }, + "searchMachineAccounts": { + "message": "Search machine accounts", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Edit machine account", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Machine account name", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Machine account created", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Machine account updated", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Grant machine accounts access to this project." + }, + "projectMachineAccountsSelectHint": { + "message": "Type or select machine accounts" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Add machine accounts to grant access" + }, + "machineAccountPeopleDescription": { + "message": "Grant groups or people access to this machine account." + }, + "machineAccountProjectsDescription": { + "message": "Assign projects to this machine account. " + }, + "createMachineAccount": { + "message": "Create a machine account" + }, + "maPeopleWarningMessage": { + "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Remove access to this machine account" + }, + "smAccessRemovalWarningMaMessage": { + "message": "This action will remove your access to the machine account." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ machine accounts included", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ per month for additional machine accounts", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Additional machine accounts" + }, + "includedMachineAccounts": { + "message": "Your plan comes with $COUNT$ machine accounts.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "You can add additional machine accounts for $COST$ per month.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Limit machine accounts (optional)" + }, + "limitMachineAccountsDesc": { + "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + }, + "machineAccountLimit": { + "message": "Machine account limit (optional)" + }, + "maxMachineAccountCost": { + "message": "Max potential machine account cost" + }, + "machineAccountAccessUpdated": { + "message": "Machine account access updated" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/web/src/locales/it/messages.json b/apps/web/src/locales/it/messages.json index c31ff1a5a2..0ae1f9ae41 100644 --- a/apps/web/src/locales/it/messages.json +++ b/apps/web/src/locales/it/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Aggiungi organizzazione esistente" }, + "addNewOrganization": { + "message": "Aggiungi nuova organizzazione" + }, "myProvider": { "message": "Il mio fornitore" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "L'accesso alla raccolta è limitato" }, + "readOnlyCollectionAccess": { + "message": "Non hai accesso alla gestione di questa raccolta." + }, "grantCollectionAccess": { "message": "Consenti a gruppi o membri di accedere a questa raccolta." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Portale Fornitori" }, + "viewCollection": { + "message": "Visualizza raccolta" + }, "restrictedGroupAccess": { "message": "Non puoi aggiungerti da solo ai gruppi." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Elementi" + }, + "assignedSeats": { + "message": "Slot assegnati" + }, + "assigned": { + "message": "Assegnato" + }, + "used": { + "message": "Usato" + }, + "remaining": { + "message": "Rimanenti" + }, + "unlinkOrganization": { + "message": "Scollega organizzazione" + }, + "manageSeats": { + "message": "GESTISCI SLOT" + }, + "manageSeatsDescription": { + "message": "Le modifiche agli slot si rifletteranno nel prossimo ciclo di fatturazione." + }, + "unassignedSeatsDescription": { + "message": "Posti in abbonamento non assegnati" + }, + "purchaseSeatDescription": { + "message": "Slot aggiuntivi acquistati" + }, + "assignedSeatCannotUpdate": { + "message": "Gli slot assegnati non possono essere aggiornati. Contatta il proprietario della tua organizzazione per ricevere assistenza." + }, + "subscriptionUpdateFailed": { + "message": "Aggiornamento dell'abbonamento fallito" + }, + "trial": { + "message": "Prova", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Arretrato", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Abbonamento scaduto", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "Hai un periodo di grazia di $DAYS$ giorni dalla data di scadenza dell'abbonamento per mantenerlo. Risolvi le fatture scadute entro il $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "Hai un periodo di grazia di $DAYS$ giorni dalla data di scadenza della prima fattura non pagata per mantenere il tuo abbonamento. Risolvi le fatture scadute entro il $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Fattura non pagata", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "Per riattivare il tuo abbonamento, risolvi le fatture arretrate.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Data di cancellazione", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Non è possibile creare account macchina nelle organizzazioni disabilitate. Contatta il proprietario della tua organizzazione per assistenza." + }, + "machineAccount": { + "message": "Account macchina", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Account macchina", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "Nuovo account macchina", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Crea un nuovo account macchina per iniziare ad automatizzare le credenziali dei segreti.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Niente da mostrare per ora", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Elimina account macchina", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Elimina account macchina", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "Visualizza account macchina", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "L'eliminazione dell'account macchina $MACHINE_ACCOUNT$ è permanente e irreversibile.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "L'eliminazione degli account macchina è permanente e irreversibile." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Elimina $COUNT$ account macchina", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Account macchina eliminato" + }, + "deleteMachineAccountsToast": { + "message": "Account macchina eliminati" + }, + "searchMachineAccounts": { + "message": "Cerca account macchina", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Modifica account macchina", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Nome account macchina", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Account macchina creato", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Account macchina aggiornato", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Concedi agli account macchina di accedere a questo progetto." + }, + "projectMachineAccountsSelectHint": { + "message": "Digita o seleziona account macchina" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Aggiungi account macchina per concedere l'accesso" + }, + "machineAccountPeopleDescription": { + "message": "Consenti a gruppi o persone di accedere a questo account macchina." + }, + "machineAccountProjectsDescription": { + "message": "Assegna progetti a questo account macchina. " + }, + "createMachineAccount": { + "message": "Crea un account macchina" + }, + "maPeopleWarningMessage": { + "message": "Rimuovere persone da un account macchina non rimuove i token di accesso che hanno creato. Per ragioni di sicurezza, consigliamo di revocare i token di accesso creati da persone rimosse da un account macchina." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Rimuovi accesso a questo account macchina" + }, + "smAccessRemovalWarningMaMessage": { + "message": "Questa azione rimuoverà il tuo accesso all'account macchina." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ account macchina inclusi", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ al mese per account macchina aggiuntivi", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Account macchina aggiuntivi" + }, + "includedMachineAccounts": { + "message": "Il tuo piano include $COUNT$ account macchina.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "Puoi aggiungere più account macchina a $COST$ al mese.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Limita gli account macchina (facoltativo)" + }, + "limitMachineAccountsDesc": { + "message": "Imposta un limite per i tuoi account macchina. Una volta raggiunto questo limite, non potrai creare nuovi account macchina." + }, + "machineAccountLimit": { + "message": "Limite account macchina (facoltativo)" + }, + "maxMachineAccountCost": { + "message": "Costo massimo potenziale dell'account macchina" + }, + "machineAccountAccessUpdated": { + "message": "Accesso all'account macchina aggiornato" + }, + "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." } } diff --git a/apps/web/src/locales/ja/messages.json b/apps/web/src/locales/ja/messages.json index c2e3e77b24..2691faff33 100644 --- a/apps/web/src/locales/ja/messages.json +++ b/apps/web/src/locales/ja/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "既存の組織を追加する" }, + "addNewOrganization": { + "message": "新しい組織を追加" + }, "myProvider": { "message": "プロバイダー" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "コレクションへのアクセスが制限されています" }, + "readOnlyCollectionAccess": { + "message": "このコレクションを管理する権限がありません。" + }, "grantCollectionAccess": { "message": "グループまたはメンバーにこのコレクションへのアクセスを許可します。" }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "プロバイダーポータル" }, + "viewCollection": { + "message": "コレクションを表示" + }, "restrictedGroupAccess": { "message": "あなた自身をグループに追加することはできません。" }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "アイテム" + }, + "assignedSeats": { + "message": "割り当て済のシート" + }, + "assigned": { + "message": "割り当て済" + }, + "used": { + "message": "使用中" + }, + "remaining": { + "message": "残り" + }, + "unlinkOrganization": { + "message": "組織のリンクを解除" + }, + "manageSeats": { + "message": "シートの管理" + }, + "manageSeatsDescription": { + "message": "調整したシートは、次の請求サイクルで反映されます。" + }, + "unassignedSeatsDescription": { + "message": "未割り当てのサブスクリプションシート" + }, + "purchaseSeatDescription": { + "message": "追加シートを購入しました" + }, + "assignedSeatCannotUpdate": { + "message": "割り当て済のシートは更新できません。組織の所有者に連絡してください。" + }, + "subscriptionUpdateFailed": { + "message": "サブスクリプションの更新に失敗しました" + }, + "trial": { + "message": "お試し", + "description": "A subscription status label." + }, + "pastDue": { + "message": "期限切れ", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "サブスクリプションの有効期限が切れました", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "期限が切れた日から$DAYS$日間の猶予期間があり、支払いを済ませればサブスクリプションを継続できます。期限切れになった請求書の支払いを $SUSPENSION_DATE$ までに済ませてください。", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "最初の未払いの請求書の期限日から$DAYS$日間の猶予期間があり、支払いを済ませればサブスクリプションを継続できます。期限切れになった請求書の支払いを $SUSPENSION_DATE$ までに済ませてください。", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "未払い請求書", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "サブスクリプションを再度有効にするには、期限切れになった請求書の支払いをしてください。", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "解約日", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "一時停止中の組織ではマシンアカウントを作成できません。組織の所有者に問い合わせてください。" + }, + "machineAccount": { + "message": "マシンアカウント", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "マシンアカウント", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "新しいマシンアカウント", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "シークレットアクセスを自動化するためには、新しいマシンアカウントを作成してください。", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "まだ表示するものはありません", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "マシンアカウントの削除", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "マシンアカウントの削除", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "マシンアカウントを表示", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "マシンアカウント $MACHINE_ACCOUNT$ を削除すると元に戻すことはできません。", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "マシンアカウントを削除すると元に戻すことはできません。" + }, + "deleteMachineAccountsConfirmMessage": { + "message": "$COUNT$ 個のマシンアカウントの削除", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "マシンアカウントを削除しました" + }, + "deleteMachineAccountsToast": { + "message": "マシンアカウントを削除しました" + }, + "searchMachineAccounts": { + "message": "マシンアカウントの検索", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "マシンアカウントの編集", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "マシンアカウント名", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "マシンアカウントの作成", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "マシンアカウントを更新しました", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "このプロジェクトへのマシンアカウントのアクセスを許可します。" + }, + "projectMachineAccountsSelectHint": { + "message": "マシンアカウントを入力または選択" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "アクセスを許可するマシンアカウントを追加" + }, + "machineAccountPeopleDescription": { + "message": "グループまたはメンバーにこのマシンアカウントへのアクセスを許可します。" + }, + "machineAccountProjectsDescription": { + "message": "このマシンアカウントにプロジェクトを割り当てます。 " + }, + "createMachineAccount": { + "message": "マシンアカウントを作成" + }, + "maPeopleWarningMessage": { + "message": "マシンアカウントからユーザーを削除しても、作成したアクセストークンは削除されません。 セキュリティ向上のために、マシンアカウントから削除されたユーザーによって作成されたアクセストークンを取り消すことをお勧めします。" + }, + "smAccessRemovalWarningMaTitle": { + "message": "このマシンアカウントへのアクセスを削除" + }, + "smAccessRemovalWarningMaMessage": { + "message": "この操作により、マシンアカウントへのあなたのアクセス権限が削除されます。" + }, + "machineAccountsIncluded": { + "message": "$COUNT$ 個のマシンアカウントが含まれています", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "追加のマシンアカウントは月額 $COST$", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "追加のマシンアカウント" + }, + "includedMachineAccounts": { + "message": "お客様のプランには $COUNT$ 個のマシンアカウントが付属しています。", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "月額 $COST$ でマシンアカウントを追加できます。", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "マシンアカウントの制限 (オプション)" + }, + "limitMachineAccountsDesc": { + "message": "マシンアカウントの上限を設定します。この制限に達すると、新しいマシンアカウントを作成できなくなります。" + }, + "machineAccountLimit": { + "message": "マシンアカウントの上限(オプション)" + }, + "maxMachineAccountCost": { + "message": "マシンアカウントのコストの最大値" + }, + "machineAccountAccessUpdated": { + "message": "マシンアカウントへのアクセス権限を更新しました" + }, + "unassignedItemsBanner": { + "message": "注意: 割り当てられていない組織項目は、デバイス間のすべての保管庫のビューでは表示されなくなり、管理コンソールからのみアクセスできます。 管理コンソールからコレクションにこれらのアイテムを割り当てると、表示するようにできます。" } } diff --git a/apps/web/src/locales/ka/messages.json b/apps/web/src/locales/ka/messages.json index e288311956..25a97a05a3 100644 --- a/apps/web/src/locales/ka/messages.json +++ b/apps/web/src/locales/ka/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Add existing organization" }, + "addNewOrganization": { + "message": "Add new organization" + }, "myProvider": { "message": "My Provider" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "Collection access is restricted" }, + "readOnlyCollectionAccess": { + "message": "You do not have access to manage this collection." + }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "viewCollection": { + "message": "View collection" + }, "restrictedGroupAccess": { "message": "You cannot add yourself to groups." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Items" + }, + "assignedSeats": { + "message": "Assigned seats" + }, + "assigned": { + "message": "Assigned" + }, + "used": { + "message": "Used" + }, + "remaining": { + "message": "Remaining" + }, + "unlinkOrganization": { + "message": "Unlink organization" + }, + "manageSeats": { + "message": "MANAGE SEATS" + }, + "manageSeatsDescription": { + "message": "Adjustments to seats will be reflected in the next billing cycle." + }, + "unassignedSeatsDescription": { + "message": "Unassigned subscription seats" + }, + "purchaseSeatDescription": { + "message": "Additional seats purchased" + }, + "assignedSeatCannotUpdate": { + "message": "Assigned Seats can not be updated. Please contact your organization owner for assistance." + }, + "subscriptionUpdateFailed": { + "message": "Subscription update failed" + }, + "trial": { + "message": "Trial", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Past due", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Subscription expired", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Unpaid invoice", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "To reactivate your subscription, please resolve the past due invoices.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Cancellation date", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + }, + "machineAccount": { + "message": "Machine account", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Machine accounts", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "New machine account", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Create a new machine account to get started automating secret access.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Nothing to show yet", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Delete machine accounts", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Delete machine account", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "View machine account", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Deleting machine accounts is permanent and irreversible." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Delete $COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Machine account deleted" + }, + "deleteMachineAccountsToast": { + "message": "Machine accounts deleted" + }, + "searchMachineAccounts": { + "message": "Search machine accounts", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Edit machine account", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Machine account name", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Machine account created", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Machine account updated", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Grant machine accounts access to this project." + }, + "projectMachineAccountsSelectHint": { + "message": "Type or select machine accounts" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Add machine accounts to grant access" + }, + "machineAccountPeopleDescription": { + "message": "Grant groups or people access to this machine account." + }, + "machineAccountProjectsDescription": { + "message": "Assign projects to this machine account. " + }, + "createMachineAccount": { + "message": "Create a machine account" + }, + "maPeopleWarningMessage": { + "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Remove access to this machine account" + }, + "smAccessRemovalWarningMaMessage": { + "message": "This action will remove your access to the machine account." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ machine accounts included", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ per month for additional machine accounts", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Additional machine accounts" + }, + "includedMachineAccounts": { + "message": "Your plan comes with $COUNT$ machine accounts.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "You can add additional machine accounts for $COST$ per month.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Limit machine accounts (optional)" + }, + "limitMachineAccountsDesc": { + "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + }, + "machineAccountLimit": { + "message": "Machine account limit (optional)" + }, + "maxMachineAccountCost": { + "message": "Max potential machine account cost" + }, + "machineAccountAccessUpdated": { + "message": "Machine account access updated" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/web/src/locales/km/messages.json b/apps/web/src/locales/km/messages.json index ad51cf605f..abda98207b 100644 --- a/apps/web/src/locales/km/messages.json +++ b/apps/web/src/locales/km/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Add existing organization" }, + "addNewOrganization": { + "message": "Add new organization" + }, "myProvider": { "message": "My Provider" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "Collection access is restricted" }, + "readOnlyCollectionAccess": { + "message": "You do not have access to manage this collection." + }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "viewCollection": { + "message": "View collection" + }, "restrictedGroupAccess": { "message": "You cannot add yourself to groups." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Items" + }, + "assignedSeats": { + "message": "Assigned seats" + }, + "assigned": { + "message": "Assigned" + }, + "used": { + "message": "Used" + }, + "remaining": { + "message": "Remaining" + }, + "unlinkOrganization": { + "message": "Unlink organization" + }, + "manageSeats": { + "message": "MANAGE SEATS" + }, + "manageSeatsDescription": { + "message": "Adjustments to seats will be reflected in the next billing cycle." + }, + "unassignedSeatsDescription": { + "message": "Unassigned subscription seats" + }, + "purchaseSeatDescription": { + "message": "Additional seats purchased" + }, + "assignedSeatCannotUpdate": { + "message": "Assigned Seats can not be updated. Please contact your organization owner for assistance." + }, + "subscriptionUpdateFailed": { + "message": "Subscription update failed" + }, + "trial": { + "message": "Trial", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Past due", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Subscription expired", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Unpaid invoice", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "To reactivate your subscription, please resolve the past due invoices.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Cancellation date", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + }, + "machineAccount": { + "message": "Machine account", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Machine accounts", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "New machine account", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Create a new machine account to get started automating secret access.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Nothing to show yet", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Delete machine accounts", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Delete machine account", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "View machine account", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Deleting machine accounts is permanent and irreversible." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Delete $COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Machine account deleted" + }, + "deleteMachineAccountsToast": { + "message": "Machine accounts deleted" + }, + "searchMachineAccounts": { + "message": "Search machine accounts", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Edit machine account", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Machine account name", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Machine account created", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Machine account updated", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Grant machine accounts access to this project." + }, + "projectMachineAccountsSelectHint": { + "message": "Type or select machine accounts" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Add machine accounts to grant access" + }, + "machineAccountPeopleDescription": { + "message": "Grant groups or people access to this machine account." + }, + "machineAccountProjectsDescription": { + "message": "Assign projects to this machine account. " + }, + "createMachineAccount": { + "message": "Create a machine account" + }, + "maPeopleWarningMessage": { + "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Remove access to this machine account" + }, + "smAccessRemovalWarningMaMessage": { + "message": "This action will remove your access to the machine account." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ machine accounts included", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ per month for additional machine accounts", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Additional machine accounts" + }, + "includedMachineAccounts": { + "message": "Your plan comes with $COUNT$ machine accounts.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "You can add additional machine accounts for $COST$ per month.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Limit machine accounts (optional)" + }, + "limitMachineAccountsDesc": { + "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + }, + "machineAccountLimit": { + "message": "Machine account limit (optional)" + }, + "maxMachineAccountCost": { + "message": "Max potential machine account cost" + }, + "machineAccountAccessUpdated": { + "message": "Machine account access updated" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/web/src/locales/kn/messages.json b/apps/web/src/locales/kn/messages.json index 02631198e3..1dbb90ad26 100644 --- a/apps/web/src/locales/kn/messages.json +++ b/apps/web/src/locales/kn/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Add existing organization" }, + "addNewOrganization": { + "message": "Add new organization" + }, "myProvider": { "message": "My Provider" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "Collection access is restricted" }, + "readOnlyCollectionAccess": { + "message": "You do not have access to manage this collection." + }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "viewCollection": { + "message": "View collection" + }, "restrictedGroupAccess": { "message": "You cannot add yourself to groups." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Items" + }, + "assignedSeats": { + "message": "Assigned seats" + }, + "assigned": { + "message": "Assigned" + }, + "used": { + "message": "Used" + }, + "remaining": { + "message": "Remaining" + }, + "unlinkOrganization": { + "message": "Unlink organization" + }, + "manageSeats": { + "message": "MANAGE SEATS" + }, + "manageSeatsDescription": { + "message": "Adjustments to seats will be reflected in the next billing cycle." + }, + "unassignedSeatsDescription": { + "message": "Unassigned subscription seats" + }, + "purchaseSeatDescription": { + "message": "Additional seats purchased" + }, + "assignedSeatCannotUpdate": { + "message": "Assigned Seats can not be updated. Please contact your organization owner for assistance." + }, + "subscriptionUpdateFailed": { + "message": "Subscription update failed" + }, + "trial": { + "message": "Trial", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Past due", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Subscription expired", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Unpaid invoice", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "To reactivate your subscription, please resolve the past due invoices.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Cancellation date", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + }, + "machineAccount": { + "message": "Machine account", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Machine accounts", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "New machine account", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Create a new machine account to get started automating secret access.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Nothing to show yet", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Delete machine accounts", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Delete machine account", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "View machine account", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Deleting machine accounts is permanent and irreversible." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Delete $COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Machine account deleted" + }, + "deleteMachineAccountsToast": { + "message": "Machine accounts deleted" + }, + "searchMachineAccounts": { + "message": "Search machine accounts", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Edit machine account", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Machine account name", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Machine account created", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Machine account updated", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Grant machine accounts access to this project." + }, + "projectMachineAccountsSelectHint": { + "message": "Type or select machine accounts" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Add machine accounts to grant access" + }, + "machineAccountPeopleDescription": { + "message": "Grant groups or people access to this machine account." + }, + "machineAccountProjectsDescription": { + "message": "Assign projects to this machine account. " + }, + "createMachineAccount": { + "message": "Create a machine account" + }, + "maPeopleWarningMessage": { + "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Remove access to this machine account" + }, + "smAccessRemovalWarningMaMessage": { + "message": "This action will remove your access to the machine account." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ machine accounts included", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ per month for additional machine accounts", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Additional machine accounts" + }, + "includedMachineAccounts": { + "message": "Your plan comes with $COUNT$ machine accounts.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "You can add additional machine accounts for $COST$ per month.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Limit machine accounts (optional)" + }, + "limitMachineAccountsDesc": { + "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + }, + "machineAccountLimit": { + "message": "Machine account limit (optional)" + }, + "maxMachineAccountCost": { + "message": "Max potential machine account cost" + }, + "machineAccountAccessUpdated": { + "message": "Machine account access updated" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/web/src/locales/ko/messages.json b/apps/web/src/locales/ko/messages.json index 1867603a0d..c01a591b63 100644 --- a/apps/web/src/locales/ko/messages.json +++ b/apps/web/src/locales/ko/messages.json @@ -395,7 +395,7 @@ "message": "항목 보기" }, "new": { - "message": "New", + "message": "새 항목", "description": "for adding new items" }, "item": { @@ -609,7 +609,7 @@ "message": "기기로 로그인" }, "loginWithDeviceEnabledNote": { - "message": "Log in with device must be set up in the settings of the Bitwarden app. Need another option?" + "message": "기기로 로그인은 Bitwarden 앱 설정에서 구성돼야 합니다. 다른 방식이 필요하신가요?" }, "loginWithMasterPassword": { "message": "마스터 비밀번호로 로그인" @@ -624,7 +624,7 @@ "message": "Use a different log in method" }, "loginWithPasskey": { - "message": "Log in with passkey" + "message": "패스키로 로그인" }, "invalidPasskeyPleaseTryAgain": { "message": "Invalid Passkey. Please try again." @@ -636,7 +636,7 @@ "message": "Use a generated passkey that will automatically log you in without a password. Biometrics, like facial recognition or fingerprint, or another FIDO2 security method will verify your identity." }, "newPasskey": { - "message": "New passkey" + "message": "새 패스키" }, "learnMoreAboutPasswordless": { "message": "Learn more about passwordless" @@ -654,7 +654,7 @@ "message": "There was a problem creating your passkey." }, "passkeySuccessfullyCreated": { - "message": "Passkey successfully created!" + "message": "패스키가 생성되었습니다!" }, "customPasskeyNameInfo": { "message": "Name your passkey to help you identify it." @@ -711,7 +711,7 @@ "message": "새로 찾아오셨나요?" }, "startTrial": { - "message": "평가판 시작" + "message": "\b무료 체험 시작" }, "logIn": { "message": "로그인" @@ -793,7 +793,7 @@ "message": "계정 생성이 완료되었습니다! 이제 로그인하실 수 있습니다." }, "trialAccountCreated": { - "message": "계정 생성에 성공했습니다." + "message": "계정이 생성되었습니다." }, "masterPassSent": { "message": "마스터 비밀번호 힌트가 담긴 이메일을 보냈습니다." @@ -905,7 +905,7 @@ "message": "인증 코드 이메일 다시 보내기" }, "useAnotherTwoStepMethod": { - "message": "다른 2단계 인증 사용" + "message": "다른 2단계 로그인 방법 사용" }, "insertYubiKey": { "message": "YubiKey를 컴퓨터의 USB 포트에 삽입하고 버튼을 누르세요." @@ -1045,7 +1045,7 @@ "message": "인증 코드 복사" }, "copyUuid": { - "message": "Copy UUID" + "message": "UUID 복사" }, "warning": { "message": "경고" @@ -1054,7 +1054,7 @@ "message": "보관함 내보내기 확인" }, "confirmSecretsExport": { - "message": "Confirm secrets export" + "message": "비밀 데이터 내보내기 확인" }, "exportWarningDesc": { "message": "내보내기는 보관함 데이터가 암호화되지 않은 형식으로 포함됩니다. 내보낸 파일을 안전하지 않은 채널(예: 이메일)을 통해 저장하거나 보내지 마십시오. 사용이 끝난 후에는 즉시 삭제하십시오." @@ -1078,7 +1078,7 @@ "message": "보관함 내보내기" }, "exportSecrets": { - "message": "Export secrets" + "message": "비밀 데이터 내보내기" }, "fileFormat": { "message": "파일 형식" @@ -1108,13 +1108,13 @@ "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." }, "exportTypeHeading": { - "message": "Export type" + "message": "내보내기 유형" }, "accountRestricted": { - "message": "Account restricted" + "message": "계정 제한됨" }, "passwordProtected": { - "message": "Password protected" + "message": "비밀번호로 보호됨" }, "filePasswordAndConfirmFilePasswordDoNotMatch": { "message": "'파일 암호'와 '파일 암호 확인'이 일치하지 않습니다." @@ -1151,7 +1151,7 @@ "message": "길이" }, "passwordMinLength": { - "message": "Minimum password length" + "message": "최소 비밀번호 길이" }, "uppercase": { "message": "대문자 (A-Z)", @@ -1351,19 +1351,19 @@ "message": "데이터 가져오기" }, "onboardingImportDataDetailsPartOne": { - "message": "If you don't have any data to import, you can create a ", + "message": "가져오기할 데이터가 없으면, 대신 ", "description": "This will be part of a larger sentence, that will read like this: If you don't have any data to import, you can create a new item instead. (Optional second half: You may need to wait until your administrator confirms your organization membership.)" }, "onboardingImportDataDetailsLink": { - "message": "new item", + "message": "새 항목", "description": "This will be part of a larger sentence, that will read like this: If you don't have any data to import, you can create a new item instead. (Optional second half: You may need to wait until your administrator confirms your organization membership.)" }, "onboardingImportDataDetailsPartTwoNoOrgs": { - "message": " instead.", + "message": "을 생성할 수 있습니다.", "description": "This will be part of a larger sentence, that will read like this: If you don't have any data to import, you can create a new item instead." }, "onboardingImportDataDetailsPartTwoWithOrgs": { - "message": " instead. You may need to wait until your administrator confirms your organization membership.", + "message": "을 생성할 수 있습니다. 관리자가 귀하의 조직을 확인할 때까지 기다려야 할 수 있습니다.", "description": "This will be part of a larger sentence, that will read like this: If you don't have any data to import, you can create a new item instead. You may need to wait until your administrator confirms your organization membership." }, "importError": { @@ -1385,7 +1385,7 @@ } }, "dataExportSuccess": { - "message": "Data successfully exported" + "message": "데이터를 내보내기했습니다" }, "importWarning": { "message": "$ORGANIZATION$ 조직으로 데이터를 가져오려고 합니다. 데이터가 이 조직의 구성원과 공유될 수 있습니다. 계속하시겠습니까?", @@ -1409,13 +1409,13 @@ "message": "Import destination" }, "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$", @@ -1456,7 +1456,7 @@ } }, "options": { - "message": "옵션" + "message": "설정" }, "preferences": { "message": "설정" @@ -1522,10 +1522,10 @@ "message": "도메인 업데이트됨" }, "twoStepLogin": { - "message": "2단계 인증" + "message": "2단계 로그인" }, "twoStepLoginEnforcement": { - "message": "Two-step Login Enforcement" + "message": "2단계 로그인 필수 설정" }, "twoStepLoginDesc": { "message": "로그인할 때 추가 단계를 요구하여 계정을 보호하십시오." @@ -1534,11 +1534,11 @@ "message": "Enable two-step login for your organization." }, "twoStepLoginEnterpriseDescStart": { - "message": "Enforce Bitwarden Two-step Login options for members by using the ", + "message": "을 사용해 구성원에게 Bitwarden 2단계 로그인 설정을 필수로 설정합니다.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Enforce Bitwarden Two-step Login options for members by using the Two-step Login Policy.'" }, "twoStepLoginPolicy": { - "message": "2단계 인증 정책" + "message": "2단계 로그인 정책" }, "twoStepLoginOrganizationDuoDesc": { "message": "To enforce Two-step Login through Duo, use the options below." @@ -1735,7 +1735,7 @@ "message": "계정에 FIDO U2F 보안 키 추가" }, "removeU2fConfirmation": { - "message": "정말로 이 보안 키를 제거하시겠습니까?" + "message": "이 보안 키를 삭제하시겠습니까?" }, "twoFactorWebAuthnAdd": { "message": "계정에 WebAuthn 보안 키 추가" @@ -1992,7 +1992,7 @@ "message": "결제" }, "billingPlanLabel": { - "message": "요금제" + "message": "청구 계획" }, "paymentType": { "message": "결제 수단" @@ -2036,7 +2036,7 @@ "message": "1GB의 암호화된 파일 저장소." }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "YubiKey, Duo와 같은 \b상용 2단계 로그인 방법." }, "premiumSignUpEmergency": { "message": "긴급 접근" @@ -2104,7 +2104,7 @@ "message": "# 의 추가 GB" }, "additionalStorageIntervalDesc": { - "message": "귀하의 플랜은 $SIZE$의 암호화된 파일 저장소가 제공됩니다. GB / $INTERVAL$당 $PRICE$로 저장소용량을 추가할 수 있습니다.", + "message": "귀하의 요금제는 $SIZE$의 암호화 파일 저장소\u001d를 포함합니다. 추가 1GB당 매$INTERVAL$ $PRICE$로 저장소를 추가하실 수 있습니다.", "placeholders": { "size": { "content": "$1", @@ -2155,7 +2155,7 @@ "message": "Your payment method will be charged for any unpaid subscriptions." }, "paymentChargedWithTrial": { - "message": "귀하의 플랜은 7일 무료 평가판입니다. 평가 기간이 만료될 때까지 카드에서 대금이 지불되지 않습니다. 이후 정기적으로 매 $INTERVAL$ 청구됩니다. 언제든지 취소할 수 있습니다." + "message": "\b귀하의 요금제는 7일 무료 체험을 포함합니다. 체험 기간이 끝날 때까지 등록한 결제 수단에 요금이 청구되지 않습니다. 언제든지 취소하실 수 있습니다." }, "paymentInformation": { "message": "결제 정보" @@ -2302,7 +2302,7 @@ "message": "추가되는 저장소 용량 (GB)" }, "gbStorageRemove": { - "message": "삭제되는 저장소 용량 (GB)" + "message": "제거할 저장소 (GB)" }, "storageAddNote": { "message": "저장소 용량을 추가하면 청구 총계가 조정되고 파일에 즉시 지불 방법이 청구됩니다. 첫 번째 요금은 현재 청구 주기의 나머지 기간 동안 적립될 것입니다." @@ -2326,7 +2326,7 @@ "message": "Contact Support" }, "updatedPaymentMethod": { - "message": "결제방식 업데이트됨." + "message": "결제 수단이 업데이트되었습니다." }, "purchasePremium": { "message": "프리미엄 구매" @@ -2371,7 +2371,7 @@ "message": "기업 이름" }, "chooseYourPlan": { - "message": "플랜을 선택하십시오" + "message": "요금제를 선택하세요" }, "users": { "message": "사용자" @@ -2386,7 +2386,7 @@ "message": "# 의 사용자 수" }, "userSeatsAdditionalDesc": { - "message": "귀하의 플랜은 $BASE_SEATS$개의 사용자 수가 제공됩니다. 사용자 당 월 $SEAT_PRICE$로 사용자를 추가할 수 있습니다.", + "message": "귀하의 요금제는 $BASE_SEATS$명의 사용자\u001d를 포함합니다. 추가 사용자 1명 당 매월 $SEAT_PRICE$로 사용자를 추가하실 수 있습니다.", "placeholders": { "base_seats": { "content": "$1", @@ -2415,7 +2415,7 @@ } }, "planNameFamilies": { - "message": "가정" + "message": "가족" }, "planDescFamilies": { "message": "개인적인 사용을 위해 가족과 친구들에게 공유하세요." @@ -2427,7 +2427,7 @@ "message": "기업 및 기타 팀 조직용." }, "planNameTeamsStarter": { - "message": "Teams Starter" + "message": "팀 스타터" }, "planNameEnterprise": { "message": "기업" @@ -2523,7 +2523,7 @@ "message": "우선 고객 지원" }, "xDayFreeTrial": { - "message": "$COUNT$일간 무료 평가, 언제든지 취소", + "message": "$COUNT$일 무료 \b체험. 언제든지 취소하실 수 있습니다.", "placeholders": { "count": { "content": "$1", @@ -2532,7 +2532,7 @@ } }, "trialThankYou": { - "message": "Bitwarden의 $PLAN$에 가입해 주셔서 감사합니다!", + "message": "Bitwarden $PLAN$에 가입해 주셔서 고맙습니다!", "placeholders": { "plan": { "content": "$1", @@ -2541,7 +2541,7 @@ } }, "trialSecretsManagerThankYou": { - "message": "Thanks for signing up for Bitwarden Secrets Manager for $PLAN$!", + "message": "Bitwarden 비밀 데이터 관리자 $PLAN$에 가입해 주셔서 고맙습니다!", "placeholders": { "plan": { "content": "$1", @@ -2643,7 +2643,7 @@ } }, "removeUserConfirmation": { - "message": "정말 이 사용자를 제거하시겠습니까?" + "message": "이 사용자를 삭제하시겠습니까?" }, "removeOrgUserConfirmation": { "message": "구성원이 제거되면 해당 구성원은 그룹 데이터에 접근할 수 없으며 이 작업은 되돌릴 수 없습니다. 구성원을 다시 그룹에 추가하려면 해당 구성원을 다시 초대하여야 합니다." @@ -2802,10 +2802,10 @@ "message": "CLI" }, "bitWebVault": { - "message": "Bitwarden Web vault" + "message": "Bitwarden 웹 보관함" }, "bitSecretsManager": { - "message": "Bitwarden Secrets Manager" + "message": "Birwarden 비밀 데이터 관리자" }, "loggedIn": { "message": "로그인됨." @@ -2940,7 +2940,7 @@ } }, "editItemWithName": { - "message": "Edit item - $NAME$", + "message": "항목 편집 - $NAME$", "placeholders": { "name": { "content": "$1", @@ -3384,7 +3384,7 @@ "message": "User accounts will remain active after deletion but will no longer be associated to this organization." }, "deletingOrganizationIsPermanentWarning": { - "message": "Deleting $ORGANIZATION$ is permanent and irreversible.", + "message": "$ORGANIZATION$ 삭제는 영구적이며 되돌릴 수 없습니다.", "placeholders": { "organization": { "content": "$1", @@ -3408,7 +3408,7 @@ "message": "청구서에 대한 세금 정보를 제공(또는 업데이트) 하려면 지원팀에 문의하십시오." }, "billingPlan": { - "message": "플랜", + "message": "요금제", "description": "A billing plan/package. For example: Families, Teams, Enterprise, etc." }, "changeBillingPlan": { @@ -3531,7 +3531,7 @@ "message": "Subscription updated. You now have access to Secrets Manager." }, "additionalOptions": { - "message": "추가 옵션" + "message": "추가 설정" }, "additionalOptionsDesc": { "message": "구독과 관련하여 추가적인 도움이 필요한 경우 고객 지원에 문의하십시오." @@ -3642,7 +3642,7 @@ "message": "조직 업그레이드" }, "upgradeOrganizationDesc": { - "message": "이 기능은 무료 조직에서는 사용할 수 없습니다. 더 많은 기능을 이용하려면 유료 플랜으로 전환하십시오." + "message": "이 기능은 무료 조직에서 사용하실 수 없습니다. 더 많은 기능을 잠금 해제하시려면 유료 요금제로 전환하세요." }, "createOrganizationStep1": { "message": "조직 만들기: 1단계" @@ -4210,7 +4210,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "removePassword": { - "message": "비밀번호 제거" + "message": "비밀번호 삭제" }, "removedPassword": { "message": "비밀번호 제거함" @@ -4255,7 +4255,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "downloadAttachments": { - "message": "Download attachments" + "message": "첨부 파일 다운로드" }, "sendAccessUnavailable": { "message": "접근하려고 하는 Send가 존재하지 않거나 더이상 제공되지 않습니다.", @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "이미 있는 조직 추가" }, + "addNewOrganization": { + "message": "Add new organization" + }, "myProvider": { "message": "내 공급자" }, @@ -5420,7 +5423,7 @@ "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Use the require single-sign-on authentication policy to require all members to log in with SSO.'" }, "memberDecryptionOption": { - "message": "멤버 복호화 옵션" + "message": "구성원 복호화 옵션" }, "memberDecryptionPassDesc": { "message": "Once authenticated, members will decrypt vault data using their master passwords." @@ -5625,7 +5628,7 @@ "message": "Exporting organization 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", @@ -5634,7 +5637,7 @@ } }, "exportingOrganizationVaultDesc": { - "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "message": "$ORGANIZATION$ 조직과 연관된 조직 보관함만 내보내기됩니다. 개인 보관함이나 다른 조직의 항목은 포함되지 않습니다.", "placeholders": { "organization": { "content": "$1", @@ -6043,34 +6046,34 @@ "description": "The label for the date and time when a item was last edited." }, "editSecret": { - "message": "Edit secret", + "message": "비밀 데이터 편집", "description": "Action to modify an existing secret." }, "addSecret": { - "message": "Add secret", + "message": "비밀 데이터 추가", "description": "Action to create a new secret." }, "copySecretName": { - "message": "Copy secret name", + "message": "비밀 데이터 이름 복사", "description": "Action to copy the name of a secret to the system's clipboard." }, "copySecretValue": { - "message": "Copy secret value", + "message": "비밀 데이터 값 복사", "description": "Action to copy the value of a secret to the system's clipboard." }, "deleteSecret": { - "message": "Delete secret", + "message": "비밀 데이터 삭제", "description": "Action to delete a single secret from the system." }, "deleteSecrets": { - "message": "Delete secrets", + "message": "비밀 데이터 삭제", "description": "The action to delete multiple secrets from the system." }, "hardDeleteSecret": { - "message": "Permanently delete secret" + "message": "비밀 데이터 영구 삭제" }, "hardDeleteSecrets": { - "message": "Permanently delete secrets" + "message": "비밀 데이터 영구 삭제" }, "secretProjectAssociationDescription": { "message": "Select projects that the secret will be associated with. Only organization users with access to these projects will be able to see the secret.", @@ -6105,7 +6108,7 @@ "description": "The action to delete multiple projects from the system." }, "secret": { - "message": "Secret", + "message": "비밀 데이터", "description": "Label for a secret (key/value pair)" }, "serviceAccount": { @@ -6677,13 +6680,13 @@ "message": "Resend code" }, "memberColumnHeader": { - "message": "Member" + "message": "구성원" }, "groupSlashMemberColumnHeader": { - "message": "Group/Member" + "message": "그룹/구성원" }, "selectGroupsAndMembers": { - "message": "Select groups and members" + "message": "그룹 및 구성원 선택" }, "selectGroups": { "message": "Select groups" @@ -6698,7 +6701,7 @@ "message": "Deleted" }, "memberStatusFilter": { - "message": "Member status filter" + "message": "구성원 상태 필터" }, "inviteMember": { "message": "구성원 초대" @@ -6884,7 +6887,7 @@ "message": "Proceeding will log you out of all active sessions. You will need to log back in and complete two-step login setup. We recommend exporting your vault before changing your encryption settings to prevent data loss." }, "secretsManager": { - "message": "Secrets Manager" + "message": "비밀 데이터 관리자" }, "secretsManagerAccessDescription": { "message": "Activate user access to Secrets Manager." @@ -7081,7 +7084,7 @@ "message": "Device approval required. Select an approval option below:" }, "rememberThisDevice": { - "message": "Remember this device" + "message": "이 기기 기억하기" }, "uncheckIfPublicDevice": { "message": "Uncheck if using a public device" @@ -7168,7 +7171,7 @@ "description": "This description is shown to an admin when they are attempting to add more users to Secrets Manager." }, "activateSecretsManager": { - "message": "Activate Secrets Manager" + "message": "비밀 데이터 관리자 활성화" }, "yourOrganizationsFingerprint": { "message": "Your organization's fingerprint phrase", @@ -7402,10 +7405,10 @@ "message": "Updated collection management setting" }, "passwordManagerPlanPrice": { - "message": "Password Manager plan price" + "message": "비밀번호 관리자 요금제 가격" }, "secretsManagerPlanPrice": { - "message": "Secrets Manager plan price" + "message": "비밀 데이터 관리자 요금제 가격" }, "passwordManager": { "message": "Password Manager" @@ -7453,7 +7456,7 @@ "message": "At least one member or group must have can manage permission." }, "typePasskey": { - "message": "Passkey" + "message": "패스키" }, "passkeyNotCopied": { "message": "Passkey will not be copied" @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "Collection access is restricted" }, + "readOnlyCollectionAccess": { + "message": "You do not have access to manage this collection." + }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." }, @@ -7542,7 +7548,7 @@ "message": "Confirmation details" }, "smFreeTrialThankYou": { - "message": "Thank you for signing up for Bitwarden Secrets Manager!" + "message": "Bitwarden 비밀 데이터 관리자에 가입해 주셔서 고맙습니다!" }, "smFreeTrialConfirmationEmail": { "message": "We've sent a confirmation email to your email at " @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "viewCollection": { + "message": "View collection" + }, "restrictedGroupAccess": { "message": "You cannot add yourself to groups." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Items" + }, + "assignedSeats": { + "message": "Assigned seats" + }, + "assigned": { + "message": "Assigned" + }, + "used": { + "message": "Used" + }, + "remaining": { + "message": "Remaining" + }, + "unlinkOrganization": { + "message": "Unlink organization" + }, + "manageSeats": { + "message": "MANAGE SEATS" + }, + "manageSeatsDescription": { + "message": "Adjustments to seats will be reflected in the next billing cycle." + }, + "unassignedSeatsDescription": { + "message": "Unassigned subscription seats" + }, + "purchaseSeatDescription": { + "message": "Additional seats purchased" + }, + "assignedSeatCannotUpdate": { + "message": "Assigned Seats can not be updated. Please contact your organization owner for assistance." + }, + "subscriptionUpdateFailed": { + "message": "Subscription update failed" + }, + "trial": { + "message": "Trial", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Past due", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Subscription expired", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Unpaid invoice", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "To reactivate your subscription, please resolve the past due invoices.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Cancellation date", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + }, + "machineAccount": { + "message": "Machine account", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Machine accounts", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "New machine account", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Create a new machine account to get started automating secret access.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Nothing to show yet", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Delete machine accounts", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Delete machine account", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "View machine account", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Deleting machine accounts is permanent and irreversible." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Delete $COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Machine account deleted" + }, + "deleteMachineAccountsToast": { + "message": "Machine accounts deleted" + }, + "searchMachineAccounts": { + "message": "Search machine accounts", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Edit machine account", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Machine account name", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Machine account created", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Machine account updated", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Grant machine accounts access to this project." + }, + "projectMachineAccountsSelectHint": { + "message": "Type or select machine accounts" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Add machine accounts to grant access" + }, + "machineAccountPeopleDescription": { + "message": "Grant groups or people access to this machine account." + }, + "machineAccountProjectsDescription": { + "message": "Assign projects to this machine account. " + }, + "createMachineAccount": { + "message": "Create a machine account" + }, + "maPeopleWarningMessage": { + "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Remove access to this machine account" + }, + "smAccessRemovalWarningMaMessage": { + "message": "This action will remove your access to the machine account." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ machine accounts included", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ per month for additional machine accounts", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Additional machine accounts" + }, + "includedMachineAccounts": { + "message": "Your plan comes with $COUNT$ machine accounts.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "You can add additional machine accounts for $COST$ per month.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Limit machine accounts (optional)" + }, + "limitMachineAccountsDesc": { + "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + }, + "machineAccountLimit": { + "message": "Machine account limit (optional)" + }, + "maxMachineAccountCost": { + "message": "Max potential machine account cost" + }, + "machineAccountAccessUpdated": { + "message": "Machine account access updated" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/web/src/locales/lv/messages.json b/apps/web/src/locales/lv/messages.json index 8c2df3012a..d4cb0de727 100644 --- a/apps/web/src/locales/lv/messages.json +++ b/apps/web/src/locales/lv/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Pievienot esošo apvienību" }, + "addNewOrganization": { + "message": "Pievienot jaunu apvienību" + }, "myProvider": { "message": "Mans nodrošinātājs" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "Piekļuve krājumam ir ierobežota" }, + "readOnlyCollectionAccess": { + "message": "Nav piekļuves pārvaldīt šo krājumu." + }, "grantCollectionAccess": { "message": "Piešķirt kopām vai dalībniekiem piekļuvi šim krājumam." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Nodrošinātāju portāls" }, + "viewCollection": { + "message": "Skatīt krājumu" + }, "restrictedGroupAccess": { "message": "Tu nevari sevi pievienot kopām." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Vienumi" + }, + "assignedSeats": { + "message": "Piešķirtās vietas" + }, + "assigned": { + "message": "Piešķirts" + }, + "used": { + "message": "Izmantots" + }, + "remaining": { + "message": "Atlicis" + }, + "unlinkOrganization": { + "message": "Atsaistīt apvienību" + }, + "manageSeats": { + "message": "PĀRVALDĪT VIETAS" + }, + "manageSeatsDescription": { + "message": "Vietu izmaiņas tiks atspoguļotas nākamajā norēķinu posmā." + }, + "unassignedSeatsDescription": { + "message": "Noņemtas piešķirtās abonementa vietas" + }, + "purchaseSeatDescription": { + "message": "Iegādātas papildu vietas" + }, + "assignedSeatCannotUpdate": { + "message": "Piešķirtās vietas nevar atjaunināt. Lūgums vērsties pēc palīdzības pie sava apvienības īpašnieka." + }, + "subscriptionUpdateFailed": { + "message": "Abonementa atjaunināšana neizdevās" + }, + "trial": { + "message": "Izmēģinājums", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Nokavēts", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Abonements ir beidzies", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "Tev ir $DAYS$ dienu papildu laiks no abonementa beigu datuma, lai paturētu savu abonementu. Lūgums apmaksāt nokavētos rēķinus līdz $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "Tev ir $DAYS$ dienu papildu laiks no pirmā neapmaksātā rēķina datuma, lai paturētu savu abonementu. Lūgums apmaksāt nokavētos rēķinus līdz $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Neapmaksāts rēķins", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "Lai atjaunotu savu abonementu, lūgums apmaksāt nokavētos rēķinus.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Atcelšanas datums", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + }, + "machineAccount": { + "message": "Machine account", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Machine accounts", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "New machine account", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Create a new machine account to get started automating secret access.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Nothing to show yet", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Delete machine accounts", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Delete machine account", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "View machine account", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Deleting machine accounts is permanent and irreversible." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Delete $COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Machine account deleted" + }, + "deleteMachineAccountsToast": { + "message": "Machine accounts deleted" + }, + "searchMachineAccounts": { + "message": "Search machine accounts", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Edit machine account", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Machine account name", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Machine account created", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Machine account updated", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Grant machine accounts access to this project." + }, + "projectMachineAccountsSelectHint": { + "message": "Type or select machine accounts" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Add machine accounts to grant access" + }, + "machineAccountPeopleDescription": { + "message": "Grant groups or people access to this machine account." + }, + "machineAccountProjectsDescription": { + "message": "Assign projects to this machine account. " + }, + "createMachineAccount": { + "message": "Create a machine account" + }, + "maPeopleWarningMessage": { + "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Remove access to this machine account" + }, + "smAccessRemovalWarningMaMessage": { + "message": "This action will remove your access to the machine account." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ machine accounts included", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ per month for additional machine accounts", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Additional machine accounts" + }, + "includedMachineAccounts": { + "message": "Your plan comes with $COUNT$ machine accounts.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "You can add additional machine accounts for $COST$ per month.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Limit machine accounts (optional)" + }, + "limitMachineAccountsDesc": { + "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + }, + "machineAccountLimit": { + "message": "Machine account limit (optional)" + }, + "maxMachineAccountCost": { + "message": "Max potential machine account cost" + }, + "machineAccountAccessUpdated": { + "message": "Machine account access updated" + }, + "unassignedItemsBanner": { + "message": "Jāņem vērā: nepiešķirti apvienības vienumi vairs nav redzami skatā \"Visas glabātavas\" dažādās ierīcēs un ir sasniedzami tikai no pārvaldības konsoles, kur šie vienumi jāpiešķir krājumam, lai padarītu tos redzamus." } } diff --git a/apps/web/src/locales/ml/messages.json b/apps/web/src/locales/ml/messages.json index 81b6529793..d3b3bfd7f9 100644 --- a/apps/web/src/locales/ml/messages.json +++ b/apps/web/src/locales/ml/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Add existing organization" }, + "addNewOrganization": { + "message": "Add new organization" + }, "myProvider": { "message": "My Provider" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "Collection access is restricted" }, + "readOnlyCollectionAccess": { + "message": "You do not have access to manage this collection." + }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "viewCollection": { + "message": "View collection" + }, "restrictedGroupAccess": { "message": "You cannot add yourself to groups." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Items" + }, + "assignedSeats": { + "message": "Assigned seats" + }, + "assigned": { + "message": "Assigned" + }, + "used": { + "message": "Used" + }, + "remaining": { + "message": "Remaining" + }, + "unlinkOrganization": { + "message": "Unlink organization" + }, + "manageSeats": { + "message": "MANAGE SEATS" + }, + "manageSeatsDescription": { + "message": "Adjustments to seats will be reflected in the next billing cycle." + }, + "unassignedSeatsDescription": { + "message": "Unassigned subscription seats" + }, + "purchaseSeatDescription": { + "message": "Additional seats purchased" + }, + "assignedSeatCannotUpdate": { + "message": "Assigned Seats can not be updated. Please contact your organization owner for assistance." + }, + "subscriptionUpdateFailed": { + "message": "Subscription update failed" + }, + "trial": { + "message": "Trial", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Past due", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Subscription expired", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Unpaid invoice", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "To reactivate your subscription, please resolve the past due invoices.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Cancellation date", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + }, + "machineAccount": { + "message": "Machine account", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Machine accounts", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "New machine account", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Create a new machine account to get started automating secret access.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Nothing to show yet", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Delete machine accounts", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Delete machine account", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "View machine account", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Deleting machine accounts is permanent and irreversible." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Delete $COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Machine account deleted" + }, + "deleteMachineAccountsToast": { + "message": "Machine accounts deleted" + }, + "searchMachineAccounts": { + "message": "Search machine accounts", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Edit machine account", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Machine account name", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Machine account created", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Machine account updated", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Grant machine accounts access to this project." + }, + "projectMachineAccountsSelectHint": { + "message": "Type or select machine accounts" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Add machine accounts to grant access" + }, + "machineAccountPeopleDescription": { + "message": "Grant groups or people access to this machine account." + }, + "machineAccountProjectsDescription": { + "message": "Assign projects to this machine account. " + }, + "createMachineAccount": { + "message": "Create a machine account" + }, + "maPeopleWarningMessage": { + "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Remove access to this machine account" + }, + "smAccessRemovalWarningMaMessage": { + "message": "This action will remove your access to the machine account." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ machine accounts included", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ per month for additional machine accounts", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Additional machine accounts" + }, + "includedMachineAccounts": { + "message": "Your plan comes with $COUNT$ machine accounts.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "You can add additional machine accounts for $COST$ per month.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Limit machine accounts (optional)" + }, + "limitMachineAccountsDesc": { + "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + }, + "machineAccountLimit": { + "message": "Machine account limit (optional)" + }, + "maxMachineAccountCost": { + "message": "Max potential machine account cost" + }, + "machineAccountAccessUpdated": { + "message": "Machine account access updated" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/web/src/locales/mr/messages.json b/apps/web/src/locales/mr/messages.json index ad51cf605f..abda98207b 100644 --- a/apps/web/src/locales/mr/messages.json +++ b/apps/web/src/locales/mr/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Add existing organization" }, + "addNewOrganization": { + "message": "Add new organization" + }, "myProvider": { "message": "My Provider" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "Collection access is restricted" }, + "readOnlyCollectionAccess": { + "message": "You do not have access to manage this collection." + }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "viewCollection": { + "message": "View collection" + }, "restrictedGroupAccess": { "message": "You cannot add yourself to groups." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Items" + }, + "assignedSeats": { + "message": "Assigned seats" + }, + "assigned": { + "message": "Assigned" + }, + "used": { + "message": "Used" + }, + "remaining": { + "message": "Remaining" + }, + "unlinkOrganization": { + "message": "Unlink organization" + }, + "manageSeats": { + "message": "MANAGE SEATS" + }, + "manageSeatsDescription": { + "message": "Adjustments to seats will be reflected in the next billing cycle." + }, + "unassignedSeatsDescription": { + "message": "Unassigned subscription seats" + }, + "purchaseSeatDescription": { + "message": "Additional seats purchased" + }, + "assignedSeatCannotUpdate": { + "message": "Assigned Seats can not be updated. Please contact your organization owner for assistance." + }, + "subscriptionUpdateFailed": { + "message": "Subscription update failed" + }, + "trial": { + "message": "Trial", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Past due", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Subscription expired", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Unpaid invoice", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "To reactivate your subscription, please resolve the past due invoices.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Cancellation date", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + }, + "machineAccount": { + "message": "Machine account", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Machine accounts", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "New machine account", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Create a new machine account to get started automating secret access.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Nothing to show yet", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Delete machine accounts", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Delete machine account", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "View machine account", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Deleting machine accounts is permanent and irreversible." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Delete $COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Machine account deleted" + }, + "deleteMachineAccountsToast": { + "message": "Machine accounts deleted" + }, + "searchMachineAccounts": { + "message": "Search machine accounts", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Edit machine account", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Machine account name", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Machine account created", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Machine account updated", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Grant machine accounts access to this project." + }, + "projectMachineAccountsSelectHint": { + "message": "Type or select machine accounts" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Add machine accounts to grant access" + }, + "machineAccountPeopleDescription": { + "message": "Grant groups or people access to this machine account." + }, + "machineAccountProjectsDescription": { + "message": "Assign projects to this machine account. " + }, + "createMachineAccount": { + "message": "Create a machine account" + }, + "maPeopleWarningMessage": { + "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Remove access to this machine account" + }, + "smAccessRemovalWarningMaMessage": { + "message": "This action will remove your access to the machine account." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ machine accounts included", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ per month for additional machine accounts", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Additional machine accounts" + }, + "includedMachineAccounts": { + "message": "Your plan comes with $COUNT$ machine accounts.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "You can add additional machine accounts for $COST$ per month.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Limit machine accounts (optional)" + }, + "limitMachineAccountsDesc": { + "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + }, + "machineAccountLimit": { + "message": "Machine account limit (optional)" + }, + "maxMachineAccountCost": { + "message": "Max potential machine account cost" + }, + "machineAccountAccessUpdated": { + "message": "Machine account access updated" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/web/src/locales/my/messages.json b/apps/web/src/locales/my/messages.json index ad51cf605f..abda98207b 100644 --- a/apps/web/src/locales/my/messages.json +++ b/apps/web/src/locales/my/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Add existing organization" }, + "addNewOrganization": { + "message": "Add new organization" + }, "myProvider": { "message": "My Provider" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "Collection access is restricted" }, + "readOnlyCollectionAccess": { + "message": "You do not have access to manage this collection." + }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "viewCollection": { + "message": "View collection" + }, "restrictedGroupAccess": { "message": "You cannot add yourself to groups." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Items" + }, + "assignedSeats": { + "message": "Assigned seats" + }, + "assigned": { + "message": "Assigned" + }, + "used": { + "message": "Used" + }, + "remaining": { + "message": "Remaining" + }, + "unlinkOrganization": { + "message": "Unlink organization" + }, + "manageSeats": { + "message": "MANAGE SEATS" + }, + "manageSeatsDescription": { + "message": "Adjustments to seats will be reflected in the next billing cycle." + }, + "unassignedSeatsDescription": { + "message": "Unassigned subscription seats" + }, + "purchaseSeatDescription": { + "message": "Additional seats purchased" + }, + "assignedSeatCannotUpdate": { + "message": "Assigned Seats can not be updated. Please contact your organization owner for assistance." + }, + "subscriptionUpdateFailed": { + "message": "Subscription update failed" + }, + "trial": { + "message": "Trial", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Past due", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Subscription expired", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Unpaid invoice", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "To reactivate your subscription, please resolve the past due invoices.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Cancellation date", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + }, + "machineAccount": { + "message": "Machine account", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Machine accounts", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "New machine account", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Create a new machine account to get started automating secret access.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Nothing to show yet", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Delete machine accounts", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Delete machine account", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "View machine account", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Deleting machine accounts is permanent and irreversible." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Delete $COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Machine account deleted" + }, + "deleteMachineAccountsToast": { + "message": "Machine accounts deleted" + }, + "searchMachineAccounts": { + "message": "Search machine accounts", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Edit machine account", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Machine account name", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Machine account created", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Machine account updated", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Grant machine accounts access to this project." + }, + "projectMachineAccountsSelectHint": { + "message": "Type or select machine accounts" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Add machine accounts to grant access" + }, + "machineAccountPeopleDescription": { + "message": "Grant groups or people access to this machine account." + }, + "machineAccountProjectsDescription": { + "message": "Assign projects to this machine account. " + }, + "createMachineAccount": { + "message": "Create a machine account" + }, + "maPeopleWarningMessage": { + "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Remove access to this machine account" + }, + "smAccessRemovalWarningMaMessage": { + "message": "This action will remove your access to the machine account." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ machine accounts included", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ per month for additional machine accounts", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Additional machine accounts" + }, + "includedMachineAccounts": { + "message": "Your plan comes with $COUNT$ machine accounts.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "You can add additional machine accounts for $COST$ per month.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Limit machine accounts (optional)" + }, + "limitMachineAccountsDesc": { + "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + }, + "machineAccountLimit": { + "message": "Machine account limit (optional)" + }, + "maxMachineAccountCost": { + "message": "Max potential machine account cost" + }, + "machineAccountAccessUpdated": { + "message": "Machine account access updated" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/web/src/locales/nb/messages.json b/apps/web/src/locales/nb/messages.json index 8840ef7c85..84c20f03c9 100644 --- a/apps/web/src/locales/nb/messages.json +++ b/apps/web/src/locales/nb/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Legg til eksisterende organisasjon" }, + "addNewOrganization": { + "message": "Add new organization" + }, "myProvider": { "message": "Min leverandør" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "Collection access is restricted" }, + "readOnlyCollectionAccess": { + "message": "You do not have access to manage this collection." + }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "viewCollection": { + "message": "View collection" + }, "restrictedGroupAccess": { "message": "You cannot add yourself to groups." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Items" + }, + "assignedSeats": { + "message": "Assigned seats" + }, + "assigned": { + "message": "Assigned" + }, + "used": { + "message": "Used" + }, + "remaining": { + "message": "Remaining" + }, + "unlinkOrganization": { + "message": "Unlink organization" + }, + "manageSeats": { + "message": "MANAGE SEATS" + }, + "manageSeatsDescription": { + "message": "Adjustments to seats will be reflected in the next billing cycle." + }, + "unassignedSeatsDescription": { + "message": "Unassigned subscription seats" + }, + "purchaseSeatDescription": { + "message": "Additional seats purchased" + }, + "assignedSeatCannotUpdate": { + "message": "Assigned Seats can not be updated. Please contact your organization owner for assistance." + }, + "subscriptionUpdateFailed": { + "message": "Subscription update failed" + }, + "trial": { + "message": "Trial", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Past due", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Subscription expired", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Unpaid invoice", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "To reactivate your subscription, please resolve the past due invoices.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Cancellation date", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + }, + "machineAccount": { + "message": "Machine account", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Machine accounts", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "New machine account", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Create a new machine account to get started automating secret access.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Nothing to show yet", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Delete machine accounts", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Delete machine account", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "View machine account", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Deleting machine accounts is permanent and irreversible." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Delete $COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Machine account deleted" + }, + "deleteMachineAccountsToast": { + "message": "Machine accounts deleted" + }, + "searchMachineAccounts": { + "message": "Search machine accounts", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Edit machine account", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Machine account name", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Machine account created", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Machine account updated", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Grant machine accounts access to this project." + }, + "projectMachineAccountsSelectHint": { + "message": "Type or select machine accounts" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Add machine accounts to grant access" + }, + "machineAccountPeopleDescription": { + "message": "Grant groups or people access to this machine account." + }, + "machineAccountProjectsDescription": { + "message": "Assign projects to this machine account. " + }, + "createMachineAccount": { + "message": "Create a machine account" + }, + "maPeopleWarningMessage": { + "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Remove access to this machine account" + }, + "smAccessRemovalWarningMaMessage": { + "message": "This action will remove your access to the machine account." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ machine accounts included", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ per month for additional machine accounts", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Additional machine accounts" + }, + "includedMachineAccounts": { + "message": "Your plan comes with $COUNT$ machine accounts.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "You can add additional machine accounts for $COST$ per month.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Limit machine accounts (optional)" + }, + "limitMachineAccountsDesc": { + "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + }, + "machineAccountLimit": { + "message": "Machine account limit (optional)" + }, + "maxMachineAccountCost": { + "message": "Max potential machine account cost" + }, + "machineAccountAccessUpdated": { + "message": "Machine account access updated" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/web/src/locales/ne/messages.json b/apps/web/src/locales/ne/messages.json index a42b9d71af..cbb84c245e 100644 --- a/apps/web/src/locales/ne/messages.json +++ b/apps/web/src/locales/ne/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Add existing organization" }, + "addNewOrganization": { + "message": "Add new organization" + }, "myProvider": { "message": "My Provider" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "Collection access is restricted" }, + "readOnlyCollectionAccess": { + "message": "You do not have access to manage this collection." + }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "viewCollection": { + "message": "View collection" + }, "restrictedGroupAccess": { "message": "You cannot add yourself to groups." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Items" + }, + "assignedSeats": { + "message": "Assigned seats" + }, + "assigned": { + "message": "Assigned" + }, + "used": { + "message": "Used" + }, + "remaining": { + "message": "Remaining" + }, + "unlinkOrganization": { + "message": "Unlink organization" + }, + "manageSeats": { + "message": "MANAGE SEATS" + }, + "manageSeatsDescription": { + "message": "Adjustments to seats will be reflected in the next billing cycle." + }, + "unassignedSeatsDescription": { + "message": "Unassigned subscription seats" + }, + "purchaseSeatDescription": { + "message": "Additional seats purchased" + }, + "assignedSeatCannotUpdate": { + "message": "Assigned Seats can not be updated. Please contact your organization owner for assistance." + }, + "subscriptionUpdateFailed": { + "message": "Subscription update failed" + }, + "trial": { + "message": "Trial", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Past due", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Subscription expired", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Unpaid invoice", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "To reactivate your subscription, please resolve the past due invoices.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Cancellation date", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + }, + "machineAccount": { + "message": "Machine account", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Machine accounts", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "New machine account", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Create a new machine account to get started automating secret access.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Nothing to show yet", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Delete machine accounts", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Delete machine account", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "View machine account", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Deleting machine accounts is permanent and irreversible." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Delete $COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Machine account deleted" + }, + "deleteMachineAccountsToast": { + "message": "Machine accounts deleted" + }, + "searchMachineAccounts": { + "message": "Search machine accounts", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Edit machine account", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Machine account name", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Machine account created", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Machine account updated", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Grant machine accounts access to this project." + }, + "projectMachineAccountsSelectHint": { + "message": "Type or select machine accounts" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Add machine accounts to grant access" + }, + "machineAccountPeopleDescription": { + "message": "Grant groups or people access to this machine account." + }, + "machineAccountProjectsDescription": { + "message": "Assign projects to this machine account. " + }, + "createMachineAccount": { + "message": "Create a machine account" + }, + "maPeopleWarningMessage": { + "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Remove access to this machine account" + }, + "smAccessRemovalWarningMaMessage": { + "message": "This action will remove your access to the machine account." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ machine accounts included", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ per month for additional machine accounts", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Additional machine accounts" + }, + "includedMachineAccounts": { + "message": "Your plan comes with $COUNT$ machine accounts.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "You can add additional machine accounts for $COST$ per month.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Limit machine accounts (optional)" + }, + "limitMachineAccountsDesc": { + "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + }, + "machineAccountLimit": { + "message": "Machine account limit (optional)" + }, + "maxMachineAccountCost": { + "message": "Max potential machine account cost" + }, + "machineAccountAccessUpdated": { + "message": "Machine account access updated" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/web/src/locales/nl/messages.json b/apps/web/src/locales/nl/messages.json index ecdde73f68..c0da91ee9a 100644 --- a/apps/web/src/locales/nl/messages.json +++ b/apps/web/src/locales/nl/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Bestaande organisatie toevoegen" }, + "addNewOrganization": { + "message": "Nieuwe organisatie toevoegen" + }, "myProvider": { "message": "Mijn provider" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "Collectietoegang is beperkt" }, + "readOnlyCollectionAccess": { + "message": "Je hebt geen toegang om deze collectie te beheren." + }, "grantCollectionAccess": { "message": "Groepen of mensen toegang tot deze collectie geven." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Providerportaal" }, + "viewCollection": { + "message": "Collectie weergeven" + }, "restrictedGroupAccess": { "message": "Het is niet mogelijk om jezelf toe te voegen aan groepen." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Items" + }, + "assignedSeats": { + "message": "Toegewezen plaatsen" + }, + "assigned": { + "message": "Toegewezen" + }, + "used": { + "message": "Gebruikt" + }, + "remaining": { + "message": "Resterend" + }, + "unlinkOrganization": { + "message": "Organisatie ontkoppelen" + }, + "manageSeats": { + "message": "PLAATSEN BEHEREN" + }, + "manageSeatsDescription": { + "message": "Aanpassingen op plaatsen zijn effectief in de volgende factureringscyclus." + }, + "unassignedSeatsDescription": { + "message": "Niet-toegewezen stoelen" + }, + "purchaseSeatDescription": { + "message": "Aanvullende gekochte plaatsen" + }, + "assignedSeatCannotUpdate": { + "message": "Je kunt toegewezen plaatsen niet bijwerken. Neem contact op met de eigenaar van je organisatie voor ondersteuning." + }, + "subscriptionUpdateFailed": { + "message": "Abonnement bijwerken mislukt" + }, + "trial": { + "message": "Proefperiode", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Verlopen", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Abonnement verlopen", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "Je hebt een gratieperiode van $DAYS$ dagen vanaf de vervaldatum van je abonnement om je abonnement te behouden. Graag de openstaande facturen voor $SUSPENSION_DATE$ voldoen.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "Je hebt een gratieperiode van $DAYS$ dagen vanaf de datum waarop je eerste niet-betaalde factuur verloopt om je abonnement te behouden. Graag de openstaande facturen voor $SUSPENSION_DATE$ voldoen.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Openstaande factuur", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "Voldoe de openstaande facturen om je abonnement te activeren.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Annuleringsdatum", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Je kunt geen machine-accounts aanmaken in opgeschorte organisaties. Neem contact op met de eigenaar van je organisatie voor hulp." + }, + "machineAccount": { + "message": "Machine-account", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Machine-accounts", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "Nieuw machine-account", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Nieuw machine-account aanmaken om te beginnen met het automatiseren van secrets.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Nog niets te tonen", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Machine-accounts verwijderen", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Machine-account verwijderen", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "Machine-account weergeven", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Het verwijderen van machine-account $MACHINE_ACCOUNT$ is definitief en onomkeerbaar.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Het verwijderen van machine-accounts is definitief en onomkeerbaar." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "$COUNT$ machine-accounts verwijderen", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Machine-account verwijderd" + }, + "deleteMachineAccountsToast": { + "message": "Machine-accounts verwijderd" + }, + "searchMachineAccounts": { + "message": "Machine-accounts zoeken", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Machine-account bewerken", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Machine-accountnaam", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Machine-account aangemaakt", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Machine-account verwijderd", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Machine-accounts toegang tot dit project geven." + }, + "projectMachineAccountsSelectHint": { + "message": "Typ of selecteer machine-accounts" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Machine-accounts toevoegen om toegang te verlenen" + }, + "machineAccountPeopleDescription": { + "message": "Groepen of mensen toegang tot dit machine-account geven." + }, + "machineAccountProjectsDescription": { + "message": "Projecten aan dit machine-account toewijzen. " + }, + "createMachineAccount": { + "message": "Machine-account aanmaken" + }, + "maPeopleWarningMessage": { + "message": "Het verwijderen van mensen uit een machine-account verwijdert de door hen aangemaakte toegangstokens niet. Voor de veiligheid kun je het beste de toegangstokens die zijn gemaakt door mensen die zijn verwijderd uit een machine-account intrekken." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Toegang tot dit machine-account verwijderen" + }, + "smAccessRemovalWarningMaMessage": { + "message": "Deze actie verwijdert je toegang tot het machine-account." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ machine-accounts inbegrepen", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ per maand voor extra machine-accounts", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Extra machine-accounts" + }, + "includedMachineAccounts": { + "message": "Bij je abonnement horen $COUNT$ machine-accounts.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "Je kunt extra machine-accounts toevoegen voor $COST$ per maand.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Machine-accounts beperken (optioneel)" + }, + "limitMachineAccountsDesc": { + "message": "Stel een limiet in voor je machine-accounts. Zodra deze limiet is bereikt, kun je geen nieuwe machine-accounts toevoegen." + }, + "machineAccountLimit": { + "message": "Machine-account limiet (optioneel)" + }, + "maxMachineAccountCost": { + "message": "Maximale potentiële machine-account kosten" + }, + "machineAccountAccessUpdated": { + "message": "Toegang tot machine-account bijgewerkt" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/web/src/locales/nn/messages.json b/apps/web/src/locales/nn/messages.json index 7ac2fff5ce..c75e8196f0 100644 --- a/apps/web/src/locales/nn/messages.json +++ b/apps/web/src/locales/nn/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Add existing organization" }, + "addNewOrganization": { + "message": "Add new organization" + }, "myProvider": { "message": "My Provider" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "Collection access is restricted" }, + "readOnlyCollectionAccess": { + "message": "You do not have access to manage this collection." + }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "viewCollection": { + "message": "View collection" + }, "restrictedGroupAccess": { "message": "You cannot add yourself to groups." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Items" + }, + "assignedSeats": { + "message": "Assigned seats" + }, + "assigned": { + "message": "Assigned" + }, + "used": { + "message": "Used" + }, + "remaining": { + "message": "Remaining" + }, + "unlinkOrganization": { + "message": "Unlink organization" + }, + "manageSeats": { + "message": "MANAGE SEATS" + }, + "manageSeatsDescription": { + "message": "Adjustments to seats will be reflected in the next billing cycle." + }, + "unassignedSeatsDescription": { + "message": "Unassigned subscription seats" + }, + "purchaseSeatDescription": { + "message": "Additional seats purchased" + }, + "assignedSeatCannotUpdate": { + "message": "Assigned Seats can not be updated. Please contact your organization owner for assistance." + }, + "subscriptionUpdateFailed": { + "message": "Subscription update failed" + }, + "trial": { + "message": "Trial", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Past due", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Subscription expired", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Unpaid invoice", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "To reactivate your subscription, please resolve the past due invoices.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Cancellation date", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + }, + "machineAccount": { + "message": "Machine account", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Machine accounts", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "New machine account", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Create a new machine account to get started automating secret access.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Nothing to show yet", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Delete machine accounts", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Delete machine account", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "View machine account", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Deleting machine accounts is permanent and irreversible." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Delete $COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Machine account deleted" + }, + "deleteMachineAccountsToast": { + "message": "Machine accounts deleted" + }, + "searchMachineAccounts": { + "message": "Search machine accounts", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Edit machine account", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Machine account name", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Machine account created", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Machine account updated", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Grant machine accounts access to this project." + }, + "projectMachineAccountsSelectHint": { + "message": "Type or select machine accounts" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Add machine accounts to grant access" + }, + "machineAccountPeopleDescription": { + "message": "Grant groups or people access to this machine account." + }, + "machineAccountProjectsDescription": { + "message": "Assign projects to this machine account. " + }, + "createMachineAccount": { + "message": "Create a machine account" + }, + "maPeopleWarningMessage": { + "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Remove access to this machine account" + }, + "smAccessRemovalWarningMaMessage": { + "message": "This action will remove your access to the machine account." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ machine accounts included", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ per month for additional machine accounts", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Additional machine accounts" + }, + "includedMachineAccounts": { + "message": "Your plan comes with $COUNT$ machine accounts.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "You can add additional machine accounts for $COST$ per month.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Limit machine accounts (optional)" + }, + "limitMachineAccountsDesc": { + "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + }, + "machineAccountLimit": { + "message": "Machine account limit (optional)" + }, + "maxMachineAccountCost": { + "message": "Max potential machine account cost" + }, + "machineAccountAccessUpdated": { + "message": "Machine account access updated" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/web/src/locales/or/messages.json b/apps/web/src/locales/or/messages.json index ad51cf605f..abda98207b 100644 --- a/apps/web/src/locales/or/messages.json +++ b/apps/web/src/locales/or/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Add existing organization" }, + "addNewOrganization": { + "message": "Add new organization" + }, "myProvider": { "message": "My Provider" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "Collection access is restricted" }, + "readOnlyCollectionAccess": { + "message": "You do not have access to manage this collection." + }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "viewCollection": { + "message": "View collection" + }, "restrictedGroupAccess": { "message": "You cannot add yourself to groups." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Items" + }, + "assignedSeats": { + "message": "Assigned seats" + }, + "assigned": { + "message": "Assigned" + }, + "used": { + "message": "Used" + }, + "remaining": { + "message": "Remaining" + }, + "unlinkOrganization": { + "message": "Unlink organization" + }, + "manageSeats": { + "message": "MANAGE SEATS" + }, + "manageSeatsDescription": { + "message": "Adjustments to seats will be reflected in the next billing cycle." + }, + "unassignedSeatsDescription": { + "message": "Unassigned subscription seats" + }, + "purchaseSeatDescription": { + "message": "Additional seats purchased" + }, + "assignedSeatCannotUpdate": { + "message": "Assigned Seats can not be updated. Please contact your organization owner for assistance." + }, + "subscriptionUpdateFailed": { + "message": "Subscription update failed" + }, + "trial": { + "message": "Trial", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Past due", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Subscription expired", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Unpaid invoice", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "To reactivate your subscription, please resolve the past due invoices.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Cancellation date", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + }, + "machineAccount": { + "message": "Machine account", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Machine accounts", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "New machine account", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Create a new machine account to get started automating secret access.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Nothing to show yet", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Delete machine accounts", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Delete machine account", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "View machine account", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Deleting machine accounts is permanent and irreversible." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Delete $COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Machine account deleted" + }, + "deleteMachineAccountsToast": { + "message": "Machine accounts deleted" + }, + "searchMachineAccounts": { + "message": "Search machine accounts", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Edit machine account", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Machine account name", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Machine account created", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Machine account updated", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Grant machine accounts access to this project." + }, + "projectMachineAccountsSelectHint": { + "message": "Type or select machine accounts" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Add machine accounts to grant access" + }, + "machineAccountPeopleDescription": { + "message": "Grant groups or people access to this machine account." + }, + "machineAccountProjectsDescription": { + "message": "Assign projects to this machine account. " + }, + "createMachineAccount": { + "message": "Create a machine account" + }, + "maPeopleWarningMessage": { + "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Remove access to this machine account" + }, + "smAccessRemovalWarningMaMessage": { + "message": "This action will remove your access to the machine account." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ machine accounts included", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ per month for additional machine accounts", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Additional machine accounts" + }, + "includedMachineAccounts": { + "message": "Your plan comes with $COUNT$ machine accounts.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "You can add additional machine accounts for $COST$ per month.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Limit machine accounts (optional)" + }, + "limitMachineAccountsDesc": { + "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + }, + "machineAccountLimit": { + "message": "Machine account limit (optional)" + }, + "maxMachineAccountCost": { + "message": "Max potential machine account cost" + }, + "machineAccountAccessUpdated": { + "message": "Machine account access updated" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/web/src/locales/pl/messages.json b/apps/web/src/locales/pl/messages.json index 08d6748023..21fc97fe71 100644 --- a/apps/web/src/locales/pl/messages.json +++ b/apps/web/src/locales/pl/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Dodaj obecną organizację" }, + "addNewOrganization": { + "message": "Dodaj nową organizację" + }, "myProvider": { "message": "Mój dostawca" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "Dostęp do kolekcji jest ograniczony" }, + "readOnlyCollectionAccess": { + "message": "Nie masz dostępu do zarządzania tą kolekcją." + }, "grantCollectionAccess": { "message": "Przyznaj grupom lub członkom dostęp do tej kolekcji." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Portal dostawcy" }, + "viewCollection": { + "message": "Zobacz kolekcję" + }, "restrictedGroupAccess": { "message": "You cannot add yourself to groups." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Elementy" + }, + "assignedSeats": { + "message": "Przypisane miejsca" + }, + "assigned": { + "message": "Przypisane" + }, + "used": { + "message": "Użyte" + }, + "remaining": { + "message": "Pozostałe" + }, + "unlinkOrganization": { + "message": "Odłącz organizację" + }, + "manageSeats": { + "message": "ZARZĄDZAJ MIEJSCAMI" + }, + "manageSeatsDescription": { + "message": "Dostosowania miejsc zostaną odzwierciedlone w kolejnym cyklu rozliczeniowym." + }, + "unassignedSeatsDescription": { + "message": "Nieprzydzielone miejsca w subskrypcji" + }, + "purchaseSeatDescription": { + "message": "Zakupiono dodatkowe miejsca" + }, + "assignedSeatCannotUpdate": { + "message": "Przypisane miejsca nie mogą zostać zaktualizowane. Skontaktuj się z właścicielem organizacji, aby uzyskać pomoc." + }, + "subscriptionUpdateFailed": { + "message": "Aktualizacja subskrypcji nie powiodła się" + }, + "trial": { + "message": "Wersja próbna", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Termin przekroczony", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Subskrypcja wygasła", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "Masz okres karencji $DAYS$ dni od daty wygaśnięcia subskrypcji, aby utrzymać subskrypcję. Opłać zaległe faktury przed $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "Masz okres karencji $DAYS$ dni od dnia, w którym Twoja pierwsza niezapłacona faktura dotycząca subskrypcji się przeterminowała. Opłać zaległe faktury przed $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Nieopłacona faktura", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "Aby reaktywować subskrypcję, opłać zaległe faktury.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Data anulowania", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + }, + "machineAccount": { + "message": "Machine account", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Machine accounts", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "New machine account", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Create a new machine account to get started automating secret access.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Nothing to show yet", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Delete machine accounts", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Delete machine account", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "View machine account", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Deleting machine accounts is permanent and irreversible." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Delete $COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Machine account deleted" + }, + "deleteMachineAccountsToast": { + "message": "Machine accounts deleted" + }, + "searchMachineAccounts": { + "message": "Search machine accounts", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Edit machine account", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Machine account name", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Machine account created", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Machine account updated", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Grant machine accounts access to this project." + }, + "projectMachineAccountsSelectHint": { + "message": "Type or select machine accounts" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Add machine accounts to grant access" + }, + "machineAccountPeopleDescription": { + "message": "Grant groups or people access to this machine account." + }, + "machineAccountProjectsDescription": { + "message": "Assign projects to this machine account. " + }, + "createMachineAccount": { + "message": "Create a machine account" + }, + "maPeopleWarningMessage": { + "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Remove access to this machine account" + }, + "smAccessRemovalWarningMaMessage": { + "message": "This action will remove your access to the machine account." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ machine accounts included", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ per month for additional machine accounts", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Additional machine accounts" + }, + "includedMachineAccounts": { + "message": "Your plan comes with $COUNT$ machine accounts.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "You can add additional machine accounts for $COST$ per month.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Limit machine accounts (optional)" + }, + "limitMachineAccountsDesc": { + "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + }, + "machineAccountLimit": { + "message": "Machine account limit (optional)" + }, + "maxMachineAccountCost": { + "message": "Max potential machine account cost" + }, + "machineAccountAccessUpdated": { + "message": "Machine account access updated" + }, + "unassignedItemsBanner": { + "message": "Uwaga: Nieprzypisane elementy w organizacji nie są już widoczne w widoku Wszystkie sejfy na urządzeniach i są teraz dostępne tylko przez Konsolę Administracyjną. Przypisz te elementy do kolekcji z konsoli administracyjnej, aby były one widoczne." } } diff --git a/apps/web/src/locales/pt_BR/messages.json b/apps/web/src/locales/pt_BR/messages.json index 5ff05ed37e..e0dff9d098 100644 --- a/apps/web/src/locales/pt_BR/messages.json +++ b/apps/web/src/locales/pt_BR/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Adicionar Organização Existente" }, + "addNewOrganization": { + "message": "Add new organization" + }, "myProvider": { "message": "Meu Provedor" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "O acesso à coleção está restrito" }, + "readOnlyCollectionAccess": { + "message": "You do not have access to manage this collection." + }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "viewCollection": { + "message": "View collection" + }, "restrictedGroupAccess": { "message": "Você não pode se adicionar aos grupos." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Items" + }, + "assignedSeats": { + "message": "Assigned seats" + }, + "assigned": { + "message": "Assigned" + }, + "used": { + "message": "Used" + }, + "remaining": { + "message": "Remaining" + }, + "unlinkOrganization": { + "message": "Unlink organization" + }, + "manageSeats": { + "message": "MANAGE SEATS" + }, + "manageSeatsDescription": { + "message": "Adjustments to seats will be reflected in the next billing cycle." + }, + "unassignedSeatsDescription": { + "message": "Unassigned subscription seats" + }, + "purchaseSeatDescription": { + "message": "Additional seats purchased" + }, + "assignedSeatCannotUpdate": { + "message": "Assigned Seats can not be updated. Please contact your organization owner for assistance." + }, + "subscriptionUpdateFailed": { + "message": "Subscription update failed" + }, + "trial": { + "message": "Trial", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Past due", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Subscription expired", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Unpaid invoice", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "To reactivate your subscription, please resolve the past due invoices.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Cancellation date", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + }, + "machineAccount": { + "message": "Machine account", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Machine accounts", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "New machine account", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Create a new machine account to get started automating secret access.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Nothing to show yet", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Delete machine accounts", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Delete machine account", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "View machine account", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Deleting machine accounts is permanent and irreversible." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Delete $COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Machine account deleted" + }, + "deleteMachineAccountsToast": { + "message": "Machine accounts deleted" + }, + "searchMachineAccounts": { + "message": "Search machine accounts", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Edit machine account", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Machine account name", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Machine account created", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Machine account updated", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Grant machine accounts access to this project." + }, + "projectMachineAccountsSelectHint": { + "message": "Type or select machine accounts" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Add machine accounts to grant access" + }, + "machineAccountPeopleDescription": { + "message": "Grant groups or people access to this machine account." + }, + "machineAccountProjectsDescription": { + "message": "Assign projects to this machine account. " + }, + "createMachineAccount": { + "message": "Create a machine account" + }, + "maPeopleWarningMessage": { + "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Remove access to this machine account" + }, + "smAccessRemovalWarningMaMessage": { + "message": "This action will remove your access to the machine account." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ machine accounts included", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ per month for additional machine accounts", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Additional machine accounts" + }, + "includedMachineAccounts": { + "message": "Your plan comes with $COUNT$ machine accounts.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "You can add additional machine accounts for $COST$ per month.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Limit machine accounts (optional)" + }, + "limitMachineAccountsDesc": { + "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + }, + "machineAccountLimit": { + "message": "Machine account limit (optional)" + }, + "maxMachineAccountCost": { + "message": "Max potential machine account cost" + }, + "machineAccountAccessUpdated": { + "message": "Machine account access updated" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/web/src/locales/pt_PT/messages.json b/apps/web/src/locales/pt_PT/messages.json index 0c8c809ffc..4bd18c27e5 100644 --- a/apps/web/src/locales/pt_PT/messages.json +++ b/apps/web/src/locales/pt_PT/messages.json @@ -2051,7 +2051,7 @@ "message": "Prioridade no apoio ao cliente." }, "premiumSignUpFuture": { - "message": "Todas as futuras funcionalidades Premium. Mais em breve!" + "message": "Todas as futuras funcionalidades Premium. E muitas mais em breve!" }, "premiumPrice": { "message": "Tudo por apenas $PRICE$ /ano!", @@ -2532,7 +2532,7 @@ } }, "trialThankYou": { - "message": "Obrigado por se inscrever no Bitwarden para $PLAN$!", + "message": "Obrigado por se registar no Bitwarden para $PLAN$!", "placeholders": { "plan": { "content": "$1", @@ -2541,7 +2541,7 @@ } }, "trialSecretsManagerThankYou": { - "message": "Obrigado por se inscrever no Gestor de Segredos Bitwarden para o plano $PLAN$!", + "message": "Obrigado por se registar no Gestor de Segredos Bitwarden para o plano $PLAN$!", "placeholders": { "plan": { "content": "$1", @@ -3534,7 +3534,7 @@ "message": "Opções adicionais" }, "additionalOptionsDesc": { - "message": "Para obter ajuda adicional na gestão da sua subscrição, contacte o serviço de apoio ao cliente." + "message": "Para obter ajuda adicional na gestão da sua subscrição, por favor, contacte o apoio ao cliente." }, "subscriptionUserSeatsUnlimitedAutoscale": { "message": "Os ajustes à sua subscrição resultarão em alterações proporcionais aos seus totais de faturação. Se os novos utilizadores convidados excederem o número de licenças da sua subscrição, receberá imediatamente uma cobrança proporcional pelos utilizadores adicionais." @@ -3717,16 +3717,16 @@ "message": "Organização suspensa" }, "secretsAccessSuspended": { - "message": "Não é possível aceder a organizações suspensas. Contacte o proprietário da organização para obter assistência." + "message": "Não é possível aceder a organizações suspensas. Por favor, contacte o proprietário da organização para obter assistência." }, "secretsCannotCreate": { - "message": "Os segredos não podem ser criados em organizações suspensas. Contacte o proprietário da organização para obter assistência." + "message": "Os segredos não podem ser criados em organizações suspensas. Por favor, contacte o proprietário da organização para obter assistência." }, "projectsCannotCreate": { - "message": "Os projetos não podem ser criados em organizações suspensas. Contacte o proprietário da organização para obter assistência." + "message": "Os projetos não podem ser criados em organizações suspensas. Por favor, contacte o proprietário da organização para obter assistência." }, "serviceAccountsCannotCreate": { - "message": "As contas de serviço não podem ser criadas em organizações suspensas. Contacte o proprietário da organização para obter assistência." + "message": "As contas de serviço não podem ser criadas em organizações suspensas. Por favor, contacte o proprietário da organização para obter assistência." }, "disabledOrganizationFilterError": { "message": "Não é possível aceder aos itens de organizações suspensas. Contacte o proprietário da organização para obter assistência." @@ -4102,7 +4102,7 @@ "message": "Impedir os membros de aderirem a outras organizações." }, "singleOrgBlockCreateMessage": { - "message": "A sua organização atual tem uma política que não lhe permite aderir a mais do que uma organização. Contacte os administradores da sua organização ou inscreva-se a partir de uma conta Bitwarden diferente." + "message": "A sua organização atual tem uma política que não lhe permite aderir a mais do que uma organização. Por favor, contacte os administradores da sua organização ou registe-se a partir de uma conta Bitwarden diferente." }, "singleOrgPolicyWarning": { "message": "Os membros da organização que não sejam proprietários ou administradores e que já sejam membros de outra organização serão removidos da sua organização." @@ -4638,7 +4638,7 @@ "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send **or** sign up to try it today.'" }, "sendAccessTaglineSignUp": { - "message": "inscreva-se", + "message": "registe-se", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or **sign up** to try it today.'" }, "sendAccessTaglineTryToday": { @@ -4896,7 +4896,7 @@ "message": "Foi convidado a configurar um novo fornecedor. Para continuar, é necessário iniciar sessão ou criar uma nova conta Bitwarden." }, "setupProviderDesc": { - "message": "Introduza os dados abaixo para concluir a configuração do fornecedor. Contacte o apoio ao cliente se tiver alguma dúvida." + "message": "Por favor, introduza os dados abaixo para concluir a configuração do fornecedor. Contacte o apoio ao cliente se tiver alguma dúvida." }, "providerName": { "message": "Nome do fornecedor" @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Adicionar organização existente" }, + "addNewOrganization": { + "message": "Adicionar nova organização" + }, "myProvider": { "message": "O meu fornecedor" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "O acesso à coleção é restrito" }, + "readOnlyCollectionAccess": { + "message": "Não tem acesso para gerir esta coleção." + }, "grantCollectionAccess": { "message": "Conceder a grupos ou membros acesso a esta coleção." }, @@ -7542,7 +7548,7 @@ "message": "Detalhes da confirmação" }, "smFreeTrialThankYou": { - "message": "Obrigado por se inscrever no Gestor de Segredos Bitwarden!" + "message": "Obrigado por se registar no Gestor de Segredos Bitwarden!" }, "smFreeTrialConfirmationEmail": { "message": "Enviámos um e-mail de confirmação para o seu e-mail:" @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Portal do fornecedor" }, + "viewCollection": { + "message": "Ver coleção" + }, "restrictedGroupAccess": { "message": "Não se pode adicionar a si próprio a grupos." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Itens" + }, + "assignedSeats": { + "message": "Lugares atribuídos" + }, + "assigned": { + "message": "Atribuídos" + }, + "used": { + "message": "Utilizados" + }, + "remaining": { + "message": "Restantes" + }, + "unlinkOrganization": { + "message": "Desvincular a organização" + }, + "manageSeats": { + "message": "GERIR LUGARES" + }, + "manageSeatsDescription": { + "message": "Os ajustes nos lugares serão refletidos no ciclo de faturação seguinte." + }, + "unassignedSeatsDescription": { + "message": "Lugares de subscrição não atribuídos" + }, + "purchaseSeatDescription": { + "message": "Lugares adicionais adquiridos" + }, + "assignedSeatCannotUpdate": { + "message": "Os lugares atribuídos não podem ser atualizados. Por favor, contacte o proprietário da organização para obter assistência." + }, + "subscriptionUpdateFailed": { + "message": "Falha na atualização da subscrição" + }, + "trial": { + "message": "Período experimental", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Vencida", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Subscrição expirada", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "Dispõe de um período de carência de $DAYS$ dias a partir da data de expiração da sua subscrição para manter a sua subscrição. Por favor, resolva as faturas vencidas até $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "Dispõe de um período de carência de $DAYS$ dias a partir da data de vencimento da sua primeira fatura não paga para manter a sua subscrição. Por favor, resolva as faturas vencidas até $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Fatura não paga", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "Para reativar a sua subscrição, por favor, resolva as faturas vencidas.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Data de cancelamento", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "As contas automáticas não podem ser criadas em organizações suspensas. Por favor, contacte o proprietário da organização para obter assistência." + }, + "machineAccount": { + "message": "Conta automática", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Contas automáticas", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "Nova conta automática", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Crie uma nova conta automática para começar a automatizar o acesso a segredos.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Ainda não há nada para mostrar", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Eliminar contas automáticas", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Eliminar conta automática", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "Ver conta automática", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "A eliminação da conta automática $MACHINE_ACCOUNT$ é permanente e irreversível.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "A eliminação de contas automáticas é permanente e irreversível." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Eliminar $COUNT$ contas automáticas", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Conta automática eliminada" + }, + "deleteMachineAccountsToast": { + "message": "Contas automáticas eliminadas" + }, + "searchMachineAccounts": { + "message": "Procurar contas automáticas", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Editar conta automática", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Nome da conta automática", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Conta automática criada", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Conta automática atualizada", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Conceder acesso a este projeto às contas automáticas." + }, + "projectMachineAccountsSelectHint": { + "message": "Escreva ou selecione contas automáticas" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Adicionar contas automáticas para conceder acesso" + }, + "machineAccountPeopleDescription": { + "message": "Conceder a grupos ou pessoas acesso a esta conta automática." + }, + "machineAccountProjectsDescription": { + "message": "Atribuir projetos a esta conta automática. " + }, + "createMachineAccount": { + "message": "Criar uma conta automática" + }, + "maPeopleWarningMessage": { + "message": "Remover pessoas de uma conta automática não remove os tokens de acesso que criaram. Como prática recomendada de segurança, recomenda-se a revogação dos tokens de acesso criados por pessoas removidas de uma conta automática." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Remover o acesso a esta conta automática" + }, + "smAccessRemovalWarningMaMessage": { + "message": "Esta ação retirará o seu acesso à conta automática." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ contas automáticas incluídas", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ por mês para contas automáticas adicionais", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Contas automáticas adicionais" + }, + "includedMachineAccounts": { + "message": "O seu plano inclui $COUNT$ contas automáticas.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "Pode adicionar contas automáticas adicionais por $COST$ por mês.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Limitar contas automáticas (opcional)" + }, + "limitMachineAccountsDesc": { + "message": "Defina um limite para as suas contas automáticas. Quando este limite for atingido, não será possível criar novas contas automáticas." + }, + "machineAccountLimit": { + "message": "Limite de contas automáticas (opcional)" + }, + "maxMachineAccountCost": { + "message": "Custo potencial máximo da conta automática" + }, + "machineAccountAccessUpdated": { + "message": "Acesso à conta automática atualizado" + }, + "unassignedItemsBanner": { + "message": "Aviso: Os itens da organização não atribuídos já não são visíveis na sua vista Todos os cofres em todos os dispositivos e agora só estã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/web/src/locales/ro/messages.json b/apps/web/src/locales/ro/messages.json index 244395daaa..f0624013b0 100644 --- a/apps/web/src/locales/ro/messages.json +++ b/apps/web/src/locales/ro/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Adăugare organizație existentă" }, + "addNewOrganization": { + "message": "Add new organization" + }, "myProvider": { "message": "Furnizorul meu" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "Collection access is restricted" }, + "readOnlyCollectionAccess": { + "message": "You do not have access to manage this collection." + }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "viewCollection": { + "message": "View collection" + }, "restrictedGroupAccess": { "message": "You cannot add yourself to groups." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Items" + }, + "assignedSeats": { + "message": "Assigned seats" + }, + "assigned": { + "message": "Assigned" + }, + "used": { + "message": "Used" + }, + "remaining": { + "message": "Remaining" + }, + "unlinkOrganization": { + "message": "Unlink organization" + }, + "manageSeats": { + "message": "MANAGE SEATS" + }, + "manageSeatsDescription": { + "message": "Adjustments to seats will be reflected in the next billing cycle." + }, + "unassignedSeatsDescription": { + "message": "Unassigned subscription seats" + }, + "purchaseSeatDescription": { + "message": "Additional seats purchased" + }, + "assignedSeatCannotUpdate": { + "message": "Assigned Seats can not be updated. Please contact your organization owner for assistance." + }, + "subscriptionUpdateFailed": { + "message": "Subscription update failed" + }, + "trial": { + "message": "Trial", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Past due", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Subscription expired", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Unpaid invoice", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "To reactivate your subscription, please resolve the past due invoices.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Cancellation date", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + }, + "machineAccount": { + "message": "Machine account", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Machine accounts", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "New machine account", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Create a new machine account to get started automating secret access.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Nothing to show yet", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Delete machine accounts", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Delete machine account", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "View machine account", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Deleting machine accounts is permanent and irreversible." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Delete $COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Machine account deleted" + }, + "deleteMachineAccountsToast": { + "message": "Machine accounts deleted" + }, + "searchMachineAccounts": { + "message": "Search machine accounts", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Edit machine account", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Machine account name", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Machine account created", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Machine account updated", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Grant machine accounts access to this project." + }, + "projectMachineAccountsSelectHint": { + "message": "Type or select machine accounts" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Add machine accounts to grant access" + }, + "machineAccountPeopleDescription": { + "message": "Grant groups or people access to this machine account." + }, + "machineAccountProjectsDescription": { + "message": "Assign projects to this machine account. " + }, + "createMachineAccount": { + "message": "Create a machine account" + }, + "maPeopleWarningMessage": { + "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Remove access to this machine account" + }, + "smAccessRemovalWarningMaMessage": { + "message": "This action will remove your access to the machine account." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ machine accounts included", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ per month for additional machine accounts", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Additional machine accounts" + }, + "includedMachineAccounts": { + "message": "Your plan comes with $COUNT$ machine accounts.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "You can add additional machine accounts for $COST$ per month.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Limit machine accounts (optional)" + }, + "limitMachineAccountsDesc": { + "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + }, + "machineAccountLimit": { + "message": "Machine account limit (optional)" + }, + "maxMachineAccountCost": { + "message": "Max potential machine account cost" + }, + "machineAccountAccessUpdated": { + "message": "Machine account access updated" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/web/src/locales/ru/messages.json b/apps/web/src/locales/ru/messages.json index b68b045bbb..c09830eb0c 100644 --- a/apps/web/src/locales/ru/messages.json +++ b/apps/web/src/locales/ru/messages.json @@ -615,7 +615,7 @@ "message": "Войти с мастер-паролем" }, "readingPasskeyLoading": { - "message": "Чтение ключа доступа..." + "message": "Чтение passkey..." }, "readingPasskeyLoadingInfo": { "message": "Не закрывайте это окно и следуйте запросам браузера." @@ -624,40 +624,40 @@ "message": "Использовать другой способ авторизации" }, "loginWithPasskey": { - "message": "Войти с ключом доступа" + "message": "Войти с passkey" }, "invalidPasskeyPleaseTryAgain": { - "message": "Неверный ключ доступа. Попробуйте снова." + "message": "Неверный passkey. Попробуйте снова." }, "twoFactorForPasskeysNotSupportedOnClientUpdateToLogIn": { - "message": "2FA для ключей доступа не поддерживается. Для авторизации обновите приложение." + "message": "2FA для passkey не поддерживается. Для авторизации обновите приложение." }, "loginWithPasskeyInfo": { - "message": "Используйте сгенерированный ключ доступа для автоматической авторизации без пароля. Ваша личность будет подтверждена биометрическими данными (распознаванием лица, отпечатком пальца или другим методом безопасности FIDO2)." + "message": "Используйте сгенерированный passkey для автоматической авторизации без пароля. Ваша личность будет подтверждена биометрическими данными (распознаванием лица, отпечатком пальца или другим методом безопасности FIDO2)." }, "newPasskey": { - "message": "Новый ключ доступа" + "message": "Новый passkey" }, "learnMoreAboutPasswordless": { "message": "Подробнее о беспарольном режиме" }, "creatingPasskeyLoading": { - "message": "Создание ключа доступа..." + "message": "Создание passkey..." }, "creatingPasskeyLoadingInfo": { "message": "Не закрывайте это окно и следуйте запросам браузера." }, "errorCreatingPasskey": { - "message": "Ошибка создания ключа доступа" + "message": "Ошибка создания passkey" }, "errorCreatingPasskeyInfo": { - "message": "При создании ключа доступа возникла проблема." + "message": "При создании passkey возникла проблема." }, "passkeySuccessfullyCreated": { - "message": "Ключ доступа успешно создан!" + "message": "Passkey успешно создан!" }, "customPasskeyNameInfo": { - "message": "Назовите ключ доступа так, чтобы вы могли его идентифицировать." + "message": "Назовите passkey так, чтобы вы могли его идентифицировать." }, "useForVaultEncryption": { "message": "Использовать для шифрования хранилища" @@ -666,7 +666,7 @@ "message": "Авторизация и разблокировка на поддерживаемых устройствах без мастер-пароля. Для завершения настройки следуйте подсказкам браузера." }, "useForVaultEncryptionErrorReadingPasskey": { - "message": "Ошибка чтения ключа доступа. Попробуйте еще раз или снимите флажок с этой опции." + "message": "Ошибка чтения passkey. Попробуйте еще раз или снимите флажок с этой опции." }, "encryptionNotSupported": { "message": "Шифрование не поддерживается" @@ -678,7 +678,7 @@ "message": "Используется для шифрования" }, "loginWithPasskeyEnabled": { - "message": "Авторизоваться с помощью ключа доступа" + "message": "Авторизоваться с помощью passkey" }, "passkeySaved": { "message": "$NAME$ сохранен", @@ -690,16 +690,16 @@ } }, "passkeyRemoved": { - "message": "Ключ доступа удален" + "message": "Passkey удален" }, "removePasskey": { - "message": "Удалить ключ доступа" + "message": "Удалить passkey" }, "removePasskeyInfo": { - "message": "Если будут удалены все ключи доступа, вы не сможете авторизоваться на новых устройствах без мастер-пароля." + "message": "Если будут удалены все passkey, вы не сможете авторизоваться на новых устройствах без мастер-пароля." }, "passkeyLimitReachedInfo": { - "message": "Достигнут предел ключей доступа. Удалите какой-нибудь ключ, чтобы добавить другой." + "message": "Достигнут лимит passkey. Удалите какой-нибудь passkey, чтобы добавить другой." }, "tryAgain": { "message": "Попробуйте снова" @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Добавить существующую организацию" }, + "addNewOrganization": { + "message": "Добавить новую организацию" + }, "myProvider": { "message": "Мой поставщик" }, @@ -7453,13 +7456,13 @@ "message": "Как минимум один участник или группа должны иметь разрешение 'Может управлять'." }, "typePasskey": { - "message": "Ключ доступа" + "message": "Passkey" }, "passkeyNotCopied": { - "message": "Ключ доступа не будет скопирован" + "message": "Passkey не будет скопирован" }, "passkeyNotCopiedAlert": { - "message": "Ключ доступа не будет скопирован в клонированный элемент. Продолжить клонирование этого элемента?" + "message": "Passkey не будет скопирован в клонированный элемент. Продолжить клонирование этого элемента?" }, "modifiedCollectionManagement": { "message": "Изменена настройка управления коллекцией $ID$.", @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "Доступ к коллекции ограничен" }, + "readOnlyCollectionAccess": { + "message": "У вас нет доступа к управлению этой коллекцией." + }, "grantCollectionAccess": { "message": "Предоставить группам или участникам доступ к этой коллекции." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Портал провайдера" }, + "viewCollection": { + "message": "Посмотреть коллекцию" + }, "restrictedGroupAccess": { "message": "Нельзя добавить самого себя в группы." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Элементы" + }, + "assignedSeats": { + "message": "Назначенные места" + }, + "assigned": { + "message": "Назначено" + }, + "used": { + "message": "Использовано" + }, + "remaining": { + "message": "Осталось" + }, + "unlinkOrganization": { + "message": "Отвязать организацию" + }, + "manageSeats": { + "message": "УПРАВЛЕНИЕ МЕСТАМИ" + }, + "manageSeatsDescription": { + "message": "Корректировка мест будет отражена в следующем расчетном цикле." + }, + "unassignedSeatsDescription": { + "message": "Нераспределенные места в соответствии с подпиской" + }, + "purchaseSeatDescription": { + "message": "Приобретено дополнительных мест" + }, + "assignedSeatCannotUpdate": { + "message": "Назначенные места не могут быть обновлены. Обратитесь за помощью к владельцу организации." + }, + "subscriptionUpdateFailed": { + "message": "Не удалось обновить подписку" + }, + "trial": { + "message": "Пробная версия", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Просрочено", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Срок действия подписки истек", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "У вас есть льготный период в $DAYS$ дней с даты истечения срока действия подписки, чтобы сохранить подписку. Пожалуйста, решите вопрос с просроченными платежами до $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "У вас есть льготный период в $DAYS$ дней с момента оплаты первого неоплаченного счета, чтобы сохранить подписку. Пожалуйста, решите вопрос с просроченными платежами до $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Неоплаченный счет", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "Чтобы возобновить подписку, пожалуйста, решите вопрос с просроченными платежами.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Дата аннуляции", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Аккаунты компьютеров не могут быть созданы в отключенных организациях. Обратитесь за помощью к владельцу организации." + }, + "machineAccount": { + "message": "Аккаунт компьютера", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Аккаунты компьютеров", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "Новый аккаунт компьютера", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Чтобы приступить к автоматизации секретного доступа, создайте новый аккаунт компьютера.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Пока нечего показать", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Удалить аккаунты компьютеров", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Удалить аккаунт компьютера", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "Просмотр аккаунта компьютера", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Удаление аккаунта компьютера $MACHINE_ACCOUNT$ окончательно и необратимо.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Удаление аккаунтов компьютеров окончательно и необратимо." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Аккаунтов компьютеров к удалению: $COUNT$", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Аккаунт компьютера удален" + }, + "deleteMachineAccountsToast": { + "message": "Аккаунты компьютеров удалены" + }, + "searchMachineAccounts": { + "message": "Поиск аккаунтов компьютеров", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Изменить аккаунт компьютера", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Название аккаунта компьютера", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Аккаунт компьютера создан", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Аккаунт компьютера обновлен", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Предоставить аккаунтам компьютеров доступ к этому проекту." + }, + "projectMachineAccountsSelectHint": { + "message": "Введите или выберите аккаунты компьютеров" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Добавить аккаунты компьютеров для предоставления доступа" + }, + "machineAccountPeopleDescription": { + "message": "Предоставить группам или людям доступ к этому аккаунту компьютера." + }, + "machineAccountProjectsDescription": { + "message": "Назначить проекты этому аккаунту компьютера. " + }, + "createMachineAccount": { + "message": "Создать аккаунт компьютера" + }, + "maPeopleWarningMessage": { + "message": "Удаление пользователей из аккаунта компьютера не приводит к удалению созданных ими токенов доступа. В целях обеспечения безопасности рекомендуется отзывать токены доступа, созданные пользователями, удаленными из аккаунта компьютера." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Удалить доступ к этому аккаунту компьютера" + }, + "smAccessRemovalWarningMaMessage": { + "message": "Это действие лишит вас доступа к аккаунту компьютера." + }, + "machineAccountsIncluded": { + "message": "Включено аккаунтов компьютеров: $COUNT$", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "В месяц за дополнительные аккаунты компьютеров: $COST$", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Дополнительные аккаунты компьютеров" + }, + "includedMachineAccounts": { + "message": "Ваш план включает в себя аккаунтов компьютеров: $COUNT$", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "Стоимость дополнительных аккаунтов компьютеров за месяц: $COST$", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Лимит аккаунтов компьютеров (опционально)" + }, + "limitMachineAccountsDesc": { + "message": "Установите лимит для аккаунтов компьютеров. По достижении этого лимита создание новых аккаунтов компьютеров будет невозможным." + }, + "machineAccountLimit": { + "message": "Лимит аккаунтов компьютеров (опционально)" + }, + "maxMachineAccountCost": { + "message": "Максимальная потенциальная стоимость аккаунта компьютера" + }, + "machineAccountAccessUpdated": { + "message": "Доступ аккаунта компьютера обновлен" + }, + "unassignedItemsBanner": { + "message": "Обратите внимание: неприсвоенные элементы организации больше не видны в представлении \"Все хранилища\" на всех устройствах и теперь доступны только через консоль администратора. Назначьте эти элементы коллекции в консоли администратора, чтобы сделать их видимыми." } } diff --git a/apps/web/src/locales/si/messages.json b/apps/web/src/locales/si/messages.json index ce4df3eeda..536c05b2e4 100644 --- a/apps/web/src/locales/si/messages.json +++ b/apps/web/src/locales/si/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Add existing organization" }, + "addNewOrganization": { + "message": "Add new organization" + }, "myProvider": { "message": "My Provider" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "Collection access is restricted" }, + "readOnlyCollectionAccess": { + "message": "You do not have access to manage this collection." + }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "viewCollection": { + "message": "View collection" + }, "restrictedGroupAccess": { "message": "You cannot add yourself to groups." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Items" + }, + "assignedSeats": { + "message": "Assigned seats" + }, + "assigned": { + "message": "Assigned" + }, + "used": { + "message": "Used" + }, + "remaining": { + "message": "Remaining" + }, + "unlinkOrganization": { + "message": "Unlink organization" + }, + "manageSeats": { + "message": "MANAGE SEATS" + }, + "manageSeatsDescription": { + "message": "Adjustments to seats will be reflected in the next billing cycle." + }, + "unassignedSeatsDescription": { + "message": "Unassigned subscription seats" + }, + "purchaseSeatDescription": { + "message": "Additional seats purchased" + }, + "assignedSeatCannotUpdate": { + "message": "Assigned Seats can not be updated. Please contact your organization owner for assistance." + }, + "subscriptionUpdateFailed": { + "message": "Subscription update failed" + }, + "trial": { + "message": "Trial", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Past due", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Subscription expired", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Unpaid invoice", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "To reactivate your subscription, please resolve the past due invoices.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Cancellation date", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + }, + "machineAccount": { + "message": "Machine account", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Machine accounts", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "New machine account", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Create a new machine account to get started automating secret access.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Nothing to show yet", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Delete machine accounts", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Delete machine account", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "View machine account", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Deleting machine accounts is permanent and irreversible." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Delete $COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Machine account deleted" + }, + "deleteMachineAccountsToast": { + "message": "Machine accounts deleted" + }, + "searchMachineAccounts": { + "message": "Search machine accounts", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Edit machine account", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Machine account name", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Machine account created", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Machine account updated", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Grant machine accounts access to this project." + }, + "projectMachineAccountsSelectHint": { + "message": "Type or select machine accounts" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Add machine accounts to grant access" + }, + "machineAccountPeopleDescription": { + "message": "Grant groups or people access to this machine account." + }, + "machineAccountProjectsDescription": { + "message": "Assign projects to this machine account. " + }, + "createMachineAccount": { + "message": "Create a machine account" + }, + "maPeopleWarningMessage": { + "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Remove access to this machine account" + }, + "smAccessRemovalWarningMaMessage": { + "message": "This action will remove your access to the machine account." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ machine accounts included", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ per month for additional machine accounts", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Additional machine accounts" + }, + "includedMachineAccounts": { + "message": "Your plan comes with $COUNT$ machine accounts.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "You can add additional machine accounts for $COST$ per month.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Limit machine accounts (optional)" + }, + "limitMachineAccountsDesc": { + "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + }, + "machineAccountLimit": { + "message": "Machine account limit (optional)" + }, + "maxMachineAccountCost": { + "message": "Max potential machine account cost" + }, + "machineAccountAccessUpdated": { + "message": "Machine account access updated" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/web/src/locales/sk/messages.json b/apps/web/src/locales/sk/messages.json index 74f68fc0cc..87ecffe11f 100644 --- a/apps/web/src/locales/sk/messages.json +++ b/apps/web/src/locales/sk/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Pridať existujúcu organizáciu" }, + "addNewOrganization": { + "message": "Pridať novú organizáciu" + }, "myProvider": { "message": "Môj poskytovateľ" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "Collection access is restricted" }, + "readOnlyCollectionAccess": { + "message": "Nemáte prístup k spravovaniu tejto zbierky." + }, "grantCollectionAccess": { "message": "Povoľte skupinám, alebo jednotlivcom prístup k tejto zbierke." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Portál poskytovateľa" }, + "viewCollection": { + "message": "Pozrieť zbierku" + }, "restrictedGroupAccess": { "message": "Seba nemôžete pridať do skupín." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Položky" + }, + "assignedSeats": { + "message": "Pridelené sedenia" + }, + "assigned": { + "message": "Pridelené" + }, + "used": { + "message": "Použité" + }, + "remaining": { + "message": "Zostáva" + }, + "unlinkOrganization": { + "message": "Odpojiť organizáciu" + }, + "manageSeats": { + "message": "SPRAVOVAŤ SEDENIA" + }, + "manageSeatsDescription": { + "message": "Zmeny v sedeniach sa prejavia v najbližšom fakturačnom období." + }, + "unassignedSeatsDescription": { + "message": "Nepridelené sedenia predplatného" + }, + "purchaseSeatDescription": { + "message": "Boli zakúpené dodatočné sedenia" + }, + "assignedSeatCannotUpdate": { + "message": "Pridelené sedenia sa nedajú aktualizovať. Pre pomoc prosím kontaktujte vlastníka vašej organizácie." + }, + "subscriptionUpdateFailed": { + "message": "Aktualizácia predplatného zlyhala" + }, + "trial": { + "message": "Trial", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Past due", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Subscription expired", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Unpaid invoice", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "To reactivate your subscription, please resolve the past due invoices.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Cancellation date", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + }, + "machineAccount": { + "message": "Machine account", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Machine accounts", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "New machine account", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Create a new machine account to get started automating secret access.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Nothing to show yet", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Delete machine accounts", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Delete machine account", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "View machine account", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Deleting machine accounts is permanent and irreversible." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Delete $COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Machine account deleted" + }, + "deleteMachineAccountsToast": { + "message": "Machine accounts deleted" + }, + "searchMachineAccounts": { + "message": "Search machine accounts", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Edit machine account", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Machine account name", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Machine account created", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Machine account updated", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Grant machine accounts access to this project." + }, + "projectMachineAccountsSelectHint": { + "message": "Type or select machine accounts" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Add machine accounts to grant access" + }, + "machineAccountPeopleDescription": { + "message": "Grant groups or people access to this machine account." + }, + "machineAccountProjectsDescription": { + "message": "Assign projects to this machine account. " + }, + "createMachineAccount": { + "message": "Create a machine account" + }, + "maPeopleWarningMessage": { + "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Remove access to this machine account" + }, + "smAccessRemovalWarningMaMessage": { + "message": "This action will remove your access to the machine account." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ machine accounts included", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ per month for additional machine accounts", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Additional machine accounts" + }, + "includedMachineAccounts": { + "message": "Your plan comes with $COUNT$ machine accounts.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "You can add additional machine accounts for $COST$ per month.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Limit machine accounts (optional)" + }, + "limitMachineAccountsDesc": { + "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + }, + "machineAccountLimit": { + "message": "Machine account limit (optional)" + }, + "maxMachineAccountCost": { + "message": "Max potential machine account cost" + }, + "machineAccountAccessUpdated": { + "message": "Machine account access updated" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/web/src/locales/sl/messages.json b/apps/web/src/locales/sl/messages.json index 4982120003..e8d8306b40 100644 --- a/apps/web/src/locales/sl/messages.json +++ b/apps/web/src/locales/sl/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Add existing organization" }, + "addNewOrganization": { + "message": "Add new organization" + }, "myProvider": { "message": "My Provider" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "Collection access is restricted" }, + "readOnlyCollectionAccess": { + "message": "You do not have access to manage this collection." + }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "viewCollection": { + "message": "View collection" + }, "restrictedGroupAccess": { "message": "You cannot add yourself to groups." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Items" + }, + "assignedSeats": { + "message": "Assigned seats" + }, + "assigned": { + "message": "Assigned" + }, + "used": { + "message": "Used" + }, + "remaining": { + "message": "Remaining" + }, + "unlinkOrganization": { + "message": "Unlink organization" + }, + "manageSeats": { + "message": "MANAGE SEATS" + }, + "manageSeatsDescription": { + "message": "Adjustments to seats will be reflected in the next billing cycle." + }, + "unassignedSeatsDescription": { + "message": "Unassigned subscription seats" + }, + "purchaseSeatDescription": { + "message": "Additional seats purchased" + }, + "assignedSeatCannotUpdate": { + "message": "Assigned Seats can not be updated. Please contact your organization owner for assistance." + }, + "subscriptionUpdateFailed": { + "message": "Subscription update failed" + }, + "trial": { + "message": "Trial", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Past due", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Subscription expired", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Unpaid invoice", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "To reactivate your subscription, please resolve the past due invoices.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Cancellation date", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + }, + "machineAccount": { + "message": "Machine account", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Machine accounts", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "New machine account", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Create a new machine account to get started automating secret access.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Nothing to show yet", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Delete machine accounts", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Delete machine account", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "View machine account", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Deleting machine accounts is permanent and irreversible." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Delete $COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Machine account deleted" + }, + "deleteMachineAccountsToast": { + "message": "Machine accounts deleted" + }, + "searchMachineAccounts": { + "message": "Search machine accounts", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Edit machine account", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Machine account name", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Machine account created", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Machine account updated", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Grant machine accounts access to this project." + }, + "projectMachineAccountsSelectHint": { + "message": "Type or select machine accounts" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Add machine accounts to grant access" + }, + "machineAccountPeopleDescription": { + "message": "Grant groups or people access to this machine account." + }, + "machineAccountProjectsDescription": { + "message": "Assign projects to this machine account. " + }, + "createMachineAccount": { + "message": "Create a machine account" + }, + "maPeopleWarningMessage": { + "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Remove access to this machine account" + }, + "smAccessRemovalWarningMaMessage": { + "message": "This action will remove your access to the machine account." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ machine accounts included", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ per month for additional machine accounts", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Additional machine accounts" + }, + "includedMachineAccounts": { + "message": "Your plan comes with $COUNT$ machine accounts.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "You can add additional machine accounts for $COST$ per month.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Limit machine accounts (optional)" + }, + "limitMachineAccountsDesc": { + "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + }, + "machineAccountLimit": { + "message": "Machine account limit (optional)" + }, + "maxMachineAccountCost": { + "message": "Max potential machine account cost" + }, + "machineAccountAccessUpdated": { + "message": "Machine account access updated" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/web/src/locales/sr/messages.json b/apps/web/src/locales/sr/messages.json index f5fe041433..b70a61b789 100644 --- a/apps/web/src/locales/sr/messages.json +++ b/apps/web/src/locales/sr/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Додај постојећу организацију" }, + "addNewOrganization": { + "message": "Додај нову организацију" + }, "myProvider": { "message": "Мој провајдер" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "Приступ колекцији је ограничен" }, + "readOnlyCollectionAccess": { + "message": "You do not have access to manage this collection." + }, "grantCollectionAccess": { "message": "Одобрите групама или члановима приступ овој колекцији." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Портал провајдера" }, + "viewCollection": { + "message": "View collection" + }, "restrictedGroupAccess": { "message": "Не можете да се додате у групе." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Ставке" + }, + "assignedSeats": { + "message": "Додељена места" + }, + "assigned": { + "message": "Додељено" + }, + "used": { + "message": "У употреби" + }, + "remaining": { + "message": "Преостало" + }, + "unlinkOrganization": { + "message": "Прекини везу са организацијом" + }, + "manageSeats": { + "message": "УПРАВЉАЈТЕ МЕСТИМА" + }, + "manageSeatsDescription": { + "message": "Прилагођавања местима ће се одразити на следећи обрачунски циклус." + }, + "unassignedSeatsDescription": { + "message": "Недодијељена претплатничка места" + }, + "purchaseSeatDescription": { + "message": "Додатна места су купљена" + }, + "assignedSeatCannotUpdate": { + "message": "Додељена места се не могу ажурирати. За помоћ контактирајте власника организације." + }, + "subscriptionUpdateFailed": { + "message": "Ажурирање претплате није успело" + }, + "trial": { + "message": "Проба", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Протекли задаци", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Претплата је истекла", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "Имате грејс период од $DAYS$ дана од датума истека ваше претплате да бисте одржали претплату. Решите фактуре са кашњењем до $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "Имате грејс период од $DAYS$ дана од датума када ваша прва неплаћена фактура треба да одржи претплату. Решите фактуре са кашњењем до $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Неплаћен рачун", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "Да бисте поново активирали своју претплату, решите фактуре са кашњењем.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Датум отказивања", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Налози машине се не могу креирати у суспендованим организацијама. За помоћ контактирајте власника своје организације." + }, + "machineAccount": { + "message": "Налог машине", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Налози машине", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "Нов налог машине", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Креирајте нови налог машине да бисте започели аутоматизацију тајног приступа.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Нема ништа за приказати", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Избришите налоге машине", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Избришите налог машине", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "Приказ налог машине", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Брисање налога машине $MACHINE_ACCOUNT$ је трајно и неповратно.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Брисање налога машине је трајно и неповратно." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Брисање $COUNT$ налога машине", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Налог машине избрисан" + }, + "deleteMachineAccountsToast": { + "message": "Налози машине избрисани" + }, + "searchMachineAccounts": { + "message": "Тражити налоге машине", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Уредити налог машине", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Име налога машине", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Налог машине креиран", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Налог машине ажуриран", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Одобрите налозима машине приступ овом пројекту." + }, + "projectMachineAccountsSelectHint": { + "message": "Унесите или изаберите налоге машине" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Додајте налоге машине да бисте одобрили приступ" + }, + "machineAccountPeopleDescription": { + "message": "Одобрите групама или људима приступ овом налогу машине." + }, + "machineAccountProjectsDescription": { + "message": "Доделите пројекте овом налогу машине. " + }, + "createMachineAccount": { + "message": "Креирајте налог машине" + }, + "maPeopleWarningMessage": { + "message": "Уклањање људи са налога машине не уклања приступне токене које су креирали. Ради најбоље безбедносне праксе, препоручује се да опозовете приступне токене које су креирали људи уклоњени са налога машине." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Уклоните приступ овом налогу машине" + }, + "smAccessRemovalWarningMaMessage": { + "message": "Ова радња ће вам уклонити приступ налогу машине." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ рачуни машина укључени", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ месечно за додатне рачуне машина", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Додатни налози машине" + }, + "includedMachineAccounts": { + "message": "Ваш план долази са $COUNT$ налога машине.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "Можете додати још налога машине за $COST$ месечно.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Ограничити рачуне машина (опционо)" + }, + "limitMachineAccountsDesc": { + "message": "Поставите ограничење за рачуне машине. Када се ово ограничење достигне, нећете моћи да креирате нове налоге на машини." + }, + "machineAccountLimit": { + "message": "Ограничење рачуна машине (опционо)" + }, + "maxMachineAccountCost": { + "message": "Максимални потенцијални трошак рачуна машине" + }, + "machineAccountAccessUpdated": { + "message": "Приступ налога машине ажуриран" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/web/src/locales/sr_CS/messages.json b/apps/web/src/locales/sr_CS/messages.json index 4553f85c33..5424fc8fb4 100644 --- a/apps/web/src/locales/sr_CS/messages.json +++ b/apps/web/src/locales/sr_CS/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Add existing organization" }, + "addNewOrganization": { + "message": "Add new organization" + }, "myProvider": { "message": "My Provider" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "Collection access is restricted" }, + "readOnlyCollectionAccess": { + "message": "You do not have access to manage this collection." + }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "viewCollection": { + "message": "View collection" + }, "restrictedGroupAccess": { "message": "You cannot add yourself to groups." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Items" + }, + "assignedSeats": { + "message": "Assigned seats" + }, + "assigned": { + "message": "Assigned" + }, + "used": { + "message": "Used" + }, + "remaining": { + "message": "Remaining" + }, + "unlinkOrganization": { + "message": "Unlink organization" + }, + "manageSeats": { + "message": "MANAGE SEATS" + }, + "manageSeatsDescription": { + "message": "Adjustments to seats will be reflected in the next billing cycle." + }, + "unassignedSeatsDescription": { + "message": "Unassigned subscription seats" + }, + "purchaseSeatDescription": { + "message": "Additional seats purchased" + }, + "assignedSeatCannotUpdate": { + "message": "Assigned Seats can not be updated. Please contact your organization owner for assistance." + }, + "subscriptionUpdateFailed": { + "message": "Subscription update failed" + }, + "trial": { + "message": "Trial", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Past due", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Subscription expired", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Unpaid invoice", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "To reactivate your subscription, please resolve the past due invoices.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Cancellation date", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + }, + "machineAccount": { + "message": "Machine account", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Machine accounts", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "New machine account", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Create a new machine account to get started automating secret access.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Nothing to show yet", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Delete machine accounts", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Delete machine account", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "View machine account", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Deleting machine accounts is permanent and irreversible." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Delete $COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Machine account deleted" + }, + "deleteMachineAccountsToast": { + "message": "Machine accounts deleted" + }, + "searchMachineAccounts": { + "message": "Search machine accounts", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Edit machine account", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Machine account name", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Machine account created", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Machine account updated", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Grant machine accounts access to this project." + }, + "projectMachineAccountsSelectHint": { + "message": "Type or select machine accounts" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Add machine accounts to grant access" + }, + "machineAccountPeopleDescription": { + "message": "Grant groups or people access to this machine account." + }, + "machineAccountProjectsDescription": { + "message": "Assign projects to this machine account. " + }, + "createMachineAccount": { + "message": "Create a machine account" + }, + "maPeopleWarningMessage": { + "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Remove access to this machine account" + }, + "smAccessRemovalWarningMaMessage": { + "message": "This action will remove your access to the machine account." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ machine accounts included", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ per month for additional machine accounts", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Additional machine accounts" + }, + "includedMachineAccounts": { + "message": "Your plan comes with $COUNT$ machine accounts.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "You can add additional machine accounts for $COST$ per month.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Limit machine accounts (optional)" + }, + "limitMachineAccountsDesc": { + "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + }, + "machineAccountLimit": { + "message": "Machine account limit (optional)" + }, + "maxMachineAccountCost": { + "message": "Max potential machine account cost" + }, + "machineAccountAccessUpdated": { + "message": "Machine account access updated" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/web/src/locales/sv/messages.json b/apps/web/src/locales/sv/messages.json index 8765baaa3b..ba6f66aaaa 100644 --- a/apps/web/src/locales/sv/messages.json +++ b/apps/web/src/locales/sv/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Lägg till befintlig organisation" }, + "addNewOrganization": { + "message": "Lägg till ny organisation" + }, "myProvider": { "message": "My Provider" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "Collection access is restricted" }, + "readOnlyCollectionAccess": { + "message": "You do not have access to manage this collection." + }, "grantCollectionAccess": { "message": "Ge grupper eller medlemmar tillgång till denna samling." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "viewCollection": { + "message": "Visa samling" + }, "restrictedGroupAccess": { "message": "Du kan inte lägga till dig själv i grupper." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Objekt" + }, + "assignedSeats": { + "message": "Assigned seats" + }, + "assigned": { + "message": "Assigned" + }, + "used": { + "message": "Used" + }, + "remaining": { + "message": "Remaining" + }, + "unlinkOrganization": { + "message": "Unlink organization" + }, + "manageSeats": { + "message": "MANAGE SEATS" + }, + "manageSeatsDescription": { + "message": "Adjustments to seats will be reflected in the next billing cycle." + }, + "unassignedSeatsDescription": { + "message": "Unassigned subscription seats" + }, + "purchaseSeatDescription": { + "message": "Additional seats purchased" + }, + "assignedSeatCannotUpdate": { + "message": "Assigned Seats can not be updated. Please contact your organization owner for assistance." + }, + "subscriptionUpdateFailed": { + "message": "Subscription update failed" + }, + "trial": { + "message": "Provperiod", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Förfallen", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Subscription expired", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Obetald faktura", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "To reactivate your subscription, please resolve the past due invoices.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Cancellation date", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + }, + "machineAccount": { + "message": "Maskinkonto", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Maskinkonton", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "New machine account", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Create a new machine account to get started automating secret access.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Nothing to show yet", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Delete machine accounts", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Delete machine account", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "View machine account", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Deleting machine accounts is permanent and irreversible." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Delete $COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Machine account deleted" + }, + "deleteMachineAccountsToast": { + "message": "Machine accounts deleted" + }, + "searchMachineAccounts": { + "message": "Search machine accounts", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Edit machine account", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Machine account name", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Machine account created", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Machine account updated", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Grant machine accounts access to this project." + }, + "projectMachineAccountsSelectHint": { + "message": "Type or select machine accounts" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Add machine accounts to grant access" + }, + "machineAccountPeopleDescription": { + "message": "Grant groups or people access to this machine account." + }, + "machineAccountProjectsDescription": { + "message": "Assign projects to this machine account. " + }, + "createMachineAccount": { + "message": "Create a machine account" + }, + "maPeopleWarningMessage": { + "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Remove access to this machine account" + }, + "smAccessRemovalWarningMaMessage": { + "message": "This action will remove your access to the machine account." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ machine accounts included", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ per month for additional machine accounts", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Additional machine accounts" + }, + "includedMachineAccounts": { + "message": "Your plan comes with $COUNT$ machine accounts.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "You can add additional machine accounts for $COST$ per month.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Limit machine accounts (optional)" + }, + "limitMachineAccountsDesc": { + "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + }, + "machineAccountLimit": { + "message": "Machine account limit (optional)" + }, + "maxMachineAccountCost": { + "message": "Max potential machine account cost" + }, + "machineAccountAccessUpdated": { + "message": "Machine account access updated" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/web/src/locales/te/messages.json b/apps/web/src/locales/te/messages.json index ad51cf605f..abda98207b 100644 --- a/apps/web/src/locales/te/messages.json +++ b/apps/web/src/locales/te/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Add existing organization" }, + "addNewOrganization": { + "message": "Add new organization" + }, "myProvider": { "message": "My Provider" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "Collection access is restricted" }, + "readOnlyCollectionAccess": { + "message": "You do not have access to manage this collection." + }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "viewCollection": { + "message": "View collection" + }, "restrictedGroupAccess": { "message": "You cannot add yourself to groups." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Items" + }, + "assignedSeats": { + "message": "Assigned seats" + }, + "assigned": { + "message": "Assigned" + }, + "used": { + "message": "Used" + }, + "remaining": { + "message": "Remaining" + }, + "unlinkOrganization": { + "message": "Unlink organization" + }, + "manageSeats": { + "message": "MANAGE SEATS" + }, + "manageSeatsDescription": { + "message": "Adjustments to seats will be reflected in the next billing cycle." + }, + "unassignedSeatsDescription": { + "message": "Unassigned subscription seats" + }, + "purchaseSeatDescription": { + "message": "Additional seats purchased" + }, + "assignedSeatCannotUpdate": { + "message": "Assigned Seats can not be updated. Please contact your organization owner for assistance." + }, + "subscriptionUpdateFailed": { + "message": "Subscription update failed" + }, + "trial": { + "message": "Trial", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Past due", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Subscription expired", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Unpaid invoice", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "To reactivate your subscription, please resolve the past due invoices.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Cancellation date", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + }, + "machineAccount": { + "message": "Machine account", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Machine accounts", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "New machine account", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Create a new machine account to get started automating secret access.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Nothing to show yet", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Delete machine accounts", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Delete machine account", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "View machine account", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Deleting machine accounts is permanent and irreversible." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Delete $COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Machine account deleted" + }, + "deleteMachineAccountsToast": { + "message": "Machine accounts deleted" + }, + "searchMachineAccounts": { + "message": "Search machine accounts", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Edit machine account", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Machine account name", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Machine account created", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Machine account updated", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Grant machine accounts access to this project." + }, + "projectMachineAccountsSelectHint": { + "message": "Type or select machine accounts" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Add machine accounts to grant access" + }, + "machineAccountPeopleDescription": { + "message": "Grant groups or people access to this machine account." + }, + "machineAccountProjectsDescription": { + "message": "Assign projects to this machine account. " + }, + "createMachineAccount": { + "message": "Create a machine account" + }, + "maPeopleWarningMessage": { + "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Remove access to this machine account" + }, + "smAccessRemovalWarningMaMessage": { + "message": "This action will remove your access to the machine account." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ machine accounts included", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ per month for additional machine accounts", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Additional machine accounts" + }, + "includedMachineAccounts": { + "message": "Your plan comes with $COUNT$ machine accounts.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "You can add additional machine accounts for $COST$ per month.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Limit machine accounts (optional)" + }, + "limitMachineAccountsDesc": { + "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + }, + "machineAccountLimit": { + "message": "Machine account limit (optional)" + }, + "maxMachineAccountCost": { + "message": "Max potential machine account cost" + }, + "machineAccountAccessUpdated": { + "message": "Machine account access updated" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/web/src/locales/th/messages.json b/apps/web/src/locales/th/messages.json index 69ee59eb26..bb8288380a 100644 --- a/apps/web/src/locales/th/messages.json +++ b/apps/web/src/locales/th/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Add existing organization" }, + "addNewOrganization": { + "message": "Add new organization" + }, "myProvider": { "message": "My Provider" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "Collection access is restricted" }, + "readOnlyCollectionAccess": { + "message": "You do not have access to manage this collection." + }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "viewCollection": { + "message": "View collection" + }, "restrictedGroupAccess": { "message": "You cannot add yourself to groups." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Items" + }, + "assignedSeats": { + "message": "Assigned seats" + }, + "assigned": { + "message": "Assigned" + }, + "used": { + "message": "Used" + }, + "remaining": { + "message": "Remaining" + }, + "unlinkOrganization": { + "message": "Unlink organization" + }, + "manageSeats": { + "message": "MANAGE SEATS" + }, + "manageSeatsDescription": { + "message": "Adjustments to seats will be reflected in the next billing cycle." + }, + "unassignedSeatsDescription": { + "message": "Unassigned subscription seats" + }, + "purchaseSeatDescription": { + "message": "Additional seats purchased" + }, + "assignedSeatCannotUpdate": { + "message": "Assigned Seats can not be updated. Please contact your organization owner for assistance." + }, + "subscriptionUpdateFailed": { + "message": "Subscription update failed" + }, + "trial": { + "message": "Trial", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Past due", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Subscription expired", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Unpaid invoice", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "To reactivate your subscription, please resolve the past due invoices.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Cancellation date", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + }, + "machineAccount": { + "message": "Machine account", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Machine accounts", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "New machine account", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Create a new machine account to get started automating secret access.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Nothing to show yet", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Delete machine accounts", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Delete machine account", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "View machine account", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Deleting machine accounts is permanent and irreversible." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Delete $COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Machine account deleted" + }, + "deleteMachineAccountsToast": { + "message": "Machine accounts deleted" + }, + "searchMachineAccounts": { + "message": "Search machine accounts", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Edit machine account", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Machine account name", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Machine account created", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Machine account updated", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Grant machine accounts access to this project." + }, + "projectMachineAccountsSelectHint": { + "message": "Type or select machine accounts" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Add machine accounts to grant access" + }, + "machineAccountPeopleDescription": { + "message": "Grant groups or people access to this machine account." + }, + "machineAccountProjectsDescription": { + "message": "Assign projects to this machine account. " + }, + "createMachineAccount": { + "message": "Create a machine account" + }, + "maPeopleWarningMessage": { + "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Remove access to this machine account" + }, + "smAccessRemovalWarningMaMessage": { + "message": "This action will remove your access to the machine account." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ machine accounts included", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ per month for additional machine accounts", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Additional machine accounts" + }, + "includedMachineAccounts": { + "message": "Your plan comes with $COUNT$ machine accounts.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "You can add additional machine accounts for $COST$ per month.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Limit machine accounts (optional)" + }, + "limitMachineAccountsDesc": { + "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + }, + "machineAccountLimit": { + "message": "Machine account limit (optional)" + }, + "maxMachineAccountCost": { + "message": "Max potential machine account cost" + }, + "machineAccountAccessUpdated": { + "message": "Machine account access updated" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/web/src/locales/tr/messages.json b/apps/web/src/locales/tr/messages.json index fb626b82f5..0856068063 100644 --- a/apps/web/src/locales/tr/messages.json +++ b/apps/web/src/locales/tr/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Mevcut kuruluşu ekle" }, + "addNewOrganization": { + "message": "Add new organization" + }, "myProvider": { "message": "Sağlayıcım" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "Koleksiyona erişim kısıtlı" }, + "readOnlyCollectionAccess": { + "message": "You do not have access to manage this collection." + }, "grantCollectionAccess": { "message": "Gruplara veya üyelere bu koleksiyona erişim izni verin." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Sağlayıcı Portalı" }, + "viewCollection": { + "message": "View collection" + }, "restrictedGroupAccess": { "message": "Kendinizi gruplara ekleyemezsiniz." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Ögeler" + }, + "assignedSeats": { + "message": "Assigned seats" + }, + "assigned": { + "message": "Assigned" + }, + "used": { + "message": "Used" + }, + "remaining": { + "message": "Remaining" + }, + "unlinkOrganization": { + "message": "Unlink organization" + }, + "manageSeats": { + "message": "MANAGE SEATS" + }, + "manageSeatsDescription": { + "message": "Adjustments to seats will be reflected in the next billing cycle." + }, + "unassignedSeatsDescription": { + "message": "Unassigned subscription seats" + }, + "purchaseSeatDescription": { + "message": "Additional seats purchased" + }, + "assignedSeatCannotUpdate": { + "message": "Assigned Seats can not be updated. Please contact your organization owner for assistance." + }, + "subscriptionUpdateFailed": { + "message": "Subscription update failed" + }, + "trial": { + "message": "Trial", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Past due", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Subscription expired", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Unpaid invoice", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "To reactivate your subscription, please resolve the past due invoices.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Cancellation date", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + }, + "machineAccount": { + "message": "Machine account", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Machine accounts", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "New machine account", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Create a new machine account to get started automating secret access.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Nothing to show yet", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Delete machine accounts", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Delete machine account", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "View machine account", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Deleting machine accounts is permanent and irreversible." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Delete $COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Machine account deleted" + }, + "deleteMachineAccountsToast": { + "message": "Machine accounts deleted" + }, + "searchMachineAccounts": { + "message": "Search machine accounts", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Edit machine account", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Machine account name", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Machine account created", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Machine account updated", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Grant machine accounts access to this project." + }, + "projectMachineAccountsSelectHint": { + "message": "Type or select machine accounts" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Add machine accounts to grant access" + }, + "machineAccountPeopleDescription": { + "message": "Grant groups or people access to this machine account." + }, + "machineAccountProjectsDescription": { + "message": "Assign projects to this machine account. " + }, + "createMachineAccount": { + "message": "Create a machine account" + }, + "maPeopleWarningMessage": { + "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Remove access to this machine account" + }, + "smAccessRemovalWarningMaMessage": { + "message": "This action will remove your access to the machine account." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ machine accounts included", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ per month for additional machine accounts", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Additional machine accounts" + }, + "includedMachineAccounts": { + "message": "Your plan comes with $COUNT$ machine accounts.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "You can add additional machine accounts for $COST$ per month.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Limit machine accounts (optional)" + }, + "limitMachineAccountsDesc": { + "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + }, + "machineAccountLimit": { + "message": "Machine account limit (optional)" + }, + "maxMachineAccountCost": { + "message": "Max potential machine account cost" + }, + "machineAccountAccessUpdated": { + "message": "Machine account access updated" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/web/src/locales/uk/messages.json b/apps/web/src/locales/uk/messages.json index e2d2e6163c..5d8b803550 100644 --- a/apps/web/src/locales/uk/messages.json +++ b/apps/web/src/locales/uk/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Додати наявну організацію" }, + "addNewOrganization": { + "message": "Додати нову організацію" + }, "myProvider": { "message": "Мій провайдер" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "Доступ до збірки обмежено" }, + "readOnlyCollectionAccess": { + "message": "У вас немає доступу до керування цією збіркою." + }, "grantCollectionAccess": { "message": "Надайте групам або учасникам доступ до цієї збірки." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Портал провайдера" }, + "viewCollection": { + "message": "Переглянути збірку" + }, "restrictedGroupAccess": { "message": "Ви не можете додати себе до груп." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Записи" + }, + "assignedSeats": { + "message": "Призначені місця" + }, + "assigned": { + "message": "Призначено" + }, + "used": { + "message": "Використано" + }, + "remaining": { + "message": "Залишилось" + }, + "unlinkOrganization": { + "message": "Від'єднати організацію" + }, + "manageSeats": { + "message": "КЕРУВАТИ МІСЦЯМИ" + }, + "manageSeatsDescription": { + "message": "Скориговані місця будуть відображені в наступному платіжному циклі." + }, + "unassignedSeatsDescription": { + "message": "Непризначені місця передплати" + }, + "purchaseSeatDescription": { + "message": "Додаткові придбані місця" + }, + "assignedSeatCannotUpdate": { + "message": "Призначені місця не можна оновити. Будь ласка, зверніться до власника вашої організації по допомогу." + }, + "subscriptionUpdateFailed": { + "message": "Не вдалося оновити передплату" + }, + "trial": { + "message": "Пробний період", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Протерміновано", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Термін дії передплати завершився", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "У вас є $DAYS$ днів пільгового періоду після завершення передплати для можливості її поновлення. Будь ласка, сплатіть протерміновані рахунки до $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "У вас є $DAYS$ днів пільгового періоду від дати протермінування першого несплаченого рахунку. Протягом цього часу вам необхідно поновити передплату. Будь ласка, сплатіть протерміновані рахунки до $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Несплачений рахунок", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "Будь ласка, сплатіть протерміновані рахунки, щоб знову активувати передплату.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Дата скасування", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "У призупинених організаціях не можна створювати машинні облікові записи. Зверніться до власника вашої організації для отримання допомоги." + }, + "machineAccount": { + "message": "Машинний обліковий запис", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Машинні облікові записи", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "Новий машинний обліковий запис", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Створіть новий машинний обліковий запис, щоб почати автоматизацію доступу до секретів.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Наразі немає даних для показу", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Видалити машинні облікові записи", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Видалити машинний обліковий запис", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "Переглянути машинний обліковий запис", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Видалення машинного облікового запису $MACHINE_ACCOUNT$ – це остаточна й незворотна дія.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Видалення машинних облікових записів – це остаточна й незворотна дія." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Видалити машинні облікові записи: $COUNT$", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Машинний обліковий запис видалено" + }, + "deleteMachineAccountsToast": { + "message": "Машинні облікові записи видалено" + }, + "searchMachineAccounts": { + "message": "Пошук машинних облікових записів", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Редагувати машинний обліковий запис", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Назва машинного облікового запису", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Машинний обліковий запис створено", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Машинний обліковий запис оновлено", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Надайте машинним обліковим записам доступ до цього проєкту." + }, + "projectMachineAccountsSelectHint": { + "message": "Введіть або виберіть машинні облікові записи" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Додайте машинні облікові записи для надання доступу" + }, + "machineAccountPeopleDescription": { + "message": "Надайте групам або людям доступ до цього машинного облікового запису." + }, + "machineAccountProjectsDescription": { + "message": "Призначте проєкти цьому машинному обліковому запису. " + }, + "createMachineAccount": { + "message": "Створити машинний обліковий запис" + }, + "maPeopleWarningMessage": { + "message": "Вилучення людей з машинного облікового запису не вилучає створені ними токени доступу. З міркувань безпеки рекомендовано відкликати токени доступу, створені людьми, вилученими з машинного облікового запису." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Вилучити доступ до цього машинного облікового запису" + }, + "smAccessRemovalWarningMaMessage": { + "message": "Ця дія призведе до вилучення вашого доступу до цього машинного облікового запису." + }, + "machineAccountsIncluded": { + "message": "Включає машинні облікові записи: $COUNT$", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ на місяць за додаткові машинні облікові записи", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Додаткові машинні облікові записи" + }, + "includedMachineAccounts": { + "message": "Ваш тарифний план включає машинні облікові записи: $COUNT$.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "Ви можете додати більше машинних облікових записів за $COST$/місяць.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Обмеження машинних облікових записів (необов'язково)" + }, + "limitMachineAccountsDesc": { + "message": "Встановіть обмеження кількості машинних облікових записів. Після досягнення ліміту ви не зможете створювати нові машинні облікові записи." + }, + "machineAccountLimit": { + "message": "Обмеження машинних облікових записів (необов'язково)" + }, + "maxMachineAccountCost": { + "message": "Потенційна максимальна вартість машинного облікового запису" + }, + "machineAccountAccessUpdated": { + "message": "Доступ до машинного облікового запису оновлено" + }, + "unassignedItemsBanner": { + "message": "Увага: непризначені елементи організації більше не видимі у поданні \"Усі сховища\" на різних пристроях і тепер доступні лише в консолі адміністратора. Щоб зробити їх видимими, призначте ці елементи збірці в консолі адміністратора." } } diff --git a/apps/web/src/locales/vi/messages.json b/apps/web/src/locales/vi/messages.json index 316704ffdf..777af538dc 100644 --- a/apps/web/src/locales/vi/messages.json +++ b/apps/web/src/locales/vi/messages.json @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "Add existing organization" }, + "addNewOrganization": { + "message": "Add new organization" + }, "myProvider": { "message": "My Provider" }, @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "Collection access is restricted" }, + "readOnlyCollectionAccess": { + "message": "You do not have access to manage this collection." + }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." }, @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "viewCollection": { + "message": "View collection" + }, "restrictedGroupAccess": { "message": "You cannot add yourself to groups." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Items" + }, + "assignedSeats": { + "message": "Assigned seats" + }, + "assigned": { + "message": "Assigned" + }, + "used": { + "message": "Used" + }, + "remaining": { + "message": "Remaining" + }, + "unlinkOrganization": { + "message": "Unlink organization" + }, + "manageSeats": { + "message": "MANAGE SEATS" + }, + "manageSeatsDescription": { + "message": "Adjustments to seats will be reflected in the next billing cycle." + }, + "unassignedSeatsDescription": { + "message": "Unassigned subscription seats" + }, + "purchaseSeatDescription": { + "message": "Additional seats purchased" + }, + "assignedSeatCannotUpdate": { + "message": "Assigned Seats can not be updated. Please contact your organization owner for assistance." + }, + "subscriptionUpdateFailed": { + "message": "Subscription update failed" + }, + "trial": { + "message": "Trial", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Past due", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Subscription expired", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Unpaid invoice", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "To reactivate your subscription, please resolve the past due invoices.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Cancellation date", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + }, + "machineAccount": { + "message": "Machine account", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Machine accounts", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "New machine account", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Create a new machine account to get started automating secret access.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Nothing to show yet", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Delete machine accounts", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Delete machine account", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "View machine account", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Deleting machine accounts is permanent and irreversible." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Delete $COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Machine account deleted" + }, + "deleteMachineAccountsToast": { + "message": "Machine accounts deleted" + }, + "searchMachineAccounts": { + "message": "Search machine accounts", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Edit machine account", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Machine account name", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Machine account created", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Machine account updated", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Grant machine accounts access to this project." + }, + "projectMachineAccountsSelectHint": { + "message": "Type or select machine accounts" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Add machine accounts to grant access" + }, + "machineAccountPeopleDescription": { + "message": "Grant groups or people access to this machine account." + }, + "machineAccountProjectsDescription": { + "message": "Assign projects to this machine account. " + }, + "createMachineAccount": { + "message": "Create a machine account" + }, + "maPeopleWarningMessage": { + "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Remove access to this machine account" + }, + "smAccessRemovalWarningMaMessage": { + "message": "This action will remove your access to the machine account." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ machine accounts included", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ per month for additional machine accounts", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Additional machine accounts" + }, + "includedMachineAccounts": { + "message": "Your plan comes with $COUNT$ machine accounts.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "You can add additional machine accounts for $COST$ per month.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Limit machine accounts (optional)" + }, + "limitMachineAccountsDesc": { + "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + }, + "machineAccountLimit": { + "message": "Machine account limit (optional)" + }, + "maxMachineAccountCost": { + "message": "Max potential machine account cost" + }, + "machineAccountAccessUpdated": { + "message": "Machine account access updated" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/web/src/locales/zh_CN/messages.json b/apps/web/src/locales/zh_CN/messages.json index e7668f85d5..c197b40f3f 100644 --- a/apps/web/src/locales/zh_CN/messages.json +++ b/apps/web/src/locales/zh_CN/messages.json @@ -579,7 +579,7 @@ "message": "访问权限" }, "accessLevel": { - "message": "访问级别" + "message": "访问权限等级" }, "loggedOut": { "message": "已注销" @@ -630,7 +630,7 @@ "message": "通行密钥无效。请重试。" }, "twoFactorForPasskeysNotSupportedOnClientUpdateToLogIn": { - "message": "不支持通行密钥 2FA。请更新 App 以登录。" + "message": "不支持通行密钥 2FA。请更新 App 再登录。" }, "loginWithPasskeyInfo": { "message": "使用已生成的通行密钥,无需密码即可自动登录。生物识别(例如面部识别或指纹)或其他 FIDO2 安全方法将用于验证您的身份。" @@ -675,7 +675,7 @@ "message": "设置加密" }, "usedForEncryption": { - "message": "用于加密" + "message": "已用于加密" }, "loginWithPasskeyEnabled": { "message": "已启用通行密钥登录" @@ -989,7 +989,7 @@ "message": "选择一个您想将这些项目移至的组织。移动到组织会将这些项目的所有权转让给该组织。移动后,您将不再是这些项目的直接所有者。" }, "collectionsDesc": { - "message": "编辑与此项目共享的集合。只有具有这些集合访问权限的组织用户才能看到此项目。" + "message": "编辑与其共享此项目的集合。只有具有这些集合访问权限的组织用户才能看到此项目。" }, "deleteSelectedItemsDesc": { "message": "$COUNT$ 个项目将被发送到回收站。", @@ -1498,7 +1498,7 @@ "message": "排除" }, "include": { - "message": "包括" + "message": "包含" }, "customize": { "message": "自定义" @@ -2661,7 +2661,7 @@ "message": "外部 ID 是一个 Bitwarden 目录连接器和 API 使用的未经加密的参考。" }, "nestCollectionUnder": { - "message": "嵌套集合在" + "message": "嵌套于集合下" }, "accessControl": { "message": "访问控制" @@ -4646,7 +4646,7 @@ "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, "sendAccessCreatorIdentifier": { - "message": "Bitwarden 成员 $USER_IDENTIFIER$ 与您分享了以下内容", + "message": "Bitwarden 成员 $USER_IDENTIFIER$ 与您共享了以下内容", "placeholders": { "user_identifier": { "content": "$1", @@ -4956,6 +4956,9 @@ "addExistingOrganization": { "message": "添加现有组织" }, + "addNewOrganization": { + "message": "添加新组织" + }, "myProvider": { "message": "我的提供商" }, @@ -5582,7 +5585,7 @@ "message": "必填" }, "charactersCurrentAndMaximum": { - "message": "$CURRENT$ / 最多字符数 $MAX$", + "message": "$CURRENT$ / 最多 $MAX$ 个字符", "placeholders": { "current": { "content": "$1", @@ -7393,7 +7396,7 @@ "message": "管理组织的集合行为" }, "limitCollectionCreationDeletionDesc": { - "message": "对所有者和管理员限制集合的创建和删除" + "message": "限制为仅所有者和管理员可以创建和删除集合" }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "所有者和管理员可以管理所有集合和项目" @@ -7478,7 +7481,7 @@ "message": "安装浏览器扩展" }, "installBrowserExtensionDetails": { - "message": "使用扩展快速保存登录信息和自动填充表单,而无需打开网页 App。" + "message": "使用扩展快速保存登录信息以及自动填充表单,而无需打开网页 App。" }, "projectAccessUpdated": { "message": "工程访问权限已更新" @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "集合访问权限受限" }, + "readOnlyCollectionAccess": { + "message": "您没有管理此集合的权限。" + }, "grantCollectionAccess": { "message": "授予群组或成员对此集合的访问权限。" }, @@ -7592,7 +7598,7 @@ "message": "欢迎使用全新改进的网页 App。了解更多有关变更的信息。" }, "releaseBlog": { - "message": "阅读发布博客" + "message": "阅读发行博客" }, "adminConsole": { "message": "管理控制台" @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "提供商门户" }, + "viewCollection": { + "message": "查看集合" + }, "restrictedGroupAccess": { "message": "您不能将自己添加到群组。" }, @@ -7616,7 +7625,7 @@ "message": "分配到这些集合" }, "bulkCollectionAssignmentDialogDescription": { - "message": "选择要共享项目的集合。当一个项目在某个集合中更新后,它将反映在所有集合中。只有能够访问这些集合的组织成员才能看到此项目。" + "message": "选择与其共享项目的集合。当一个项目在某个集合中更新后,它将反映到所有集合中。只有具有这些集合访问权限的组织成员才能看到这些项目。" }, "selectCollectionsToAssign": { "message": "选择要分配的集合" @@ -7642,5 +7651,256 @@ }, "items": { "message": "项目" + }, + "assignedSeats": { + "message": "分配的座席" + }, + "assigned": { + "message": "已分配" + }, + "used": { + "message": "已用" + }, + "remaining": { + "message": "余额:" + }, + "unlinkOrganization": { + "message": "脱离组织" + }, + "manageSeats": { + "message": "管理坐席" + }, + "manageSeatsDescription": { + "message": "席位的调整将反映在下一个计费周期中。" + }, + "unassignedSeatsDescription": { + "message": "未分配的订阅座席" + }, + "purchaseSeatDescription": { + "message": "已购买额外座席" + }, + "assignedSeatCannotUpdate": { + "message": "无法更新已分配的坐席。请联系您的组织所有者群求协助。" + }, + "subscriptionUpdateFailed": { + "message": "订阅更新失败" + }, + "trial": { + "message": "Trial", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Past due", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "订阅已过期", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "未支付的账单", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "To reactivate your subscription, please resolve the past due invoices.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "取消日期", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + }, + "machineAccount": { + "message": "Machine account", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Machine accounts", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "New machine account", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Create a new machine account to get started automating secret access.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Nothing to show yet", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Delete machine accounts", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Delete machine account", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "View machine account", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Deleting machine accounts is permanent and irreversible." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Delete $COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Machine account deleted" + }, + "deleteMachineAccountsToast": { + "message": "Machine accounts deleted" + }, + "searchMachineAccounts": { + "message": "Search machine accounts", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Edit machine account", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Machine account name", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Machine account created", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Machine account updated", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Grant machine accounts access to this project." + }, + "projectMachineAccountsSelectHint": { + "message": "Type or select machine accounts" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Add machine accounts to grant access" + }, + "machineAccountPeopleDescription": { + "message": "Grant groups or people access to this machine account." + }, + "machineAccountProjectsDescription": { + "message": "Assign projects to this machine account. " + }, + "createMachineAccount": { + "message": "Create a machine account" + }, + "maPeopleWarningMessage": { + "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Remove access to this machine account" + }, + "smAccessRemovalWarningMaMessage": { + "message": "This action will remove your access to the machine account." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ machine accounts included", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ per month for additional machine accounts", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Additional machine accounts" + }, + "includedMachineAccounts": { + "message": "Your plan comes with $COUNT$ machine accounts.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "You can add additional machine accounts for $COST$ per month.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Limit machine accounts (optional)" + }, + "limitMachineAccountsDesc": { + "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + }, + "machineAccountLimit": { + "message": "Machine account limit (optional)" + }, + "maxMachineAccountCost": { + "message": "Max potential machine account cost" + }, + "machineAccountAccessUpdated": { + "message": "Machine account access updated" + }, + "unassignedItemsBanner": { + "message": "注意:未分配的组织项目在您所有设备的「所有密码库」视图中不再可见,现在只能通过管理控制台访问。通过管理控制台将这些项目分配给集合以使其可见。" } } diff --git a/apps/web/src/locales/zh_TW/messages.json b/apps/web/src/locales/zh_TW/messages.json index 0fa0838f2d..6f512741f2 100644 --- a/apps/web/src/locales/zh_TW/messages.json +++ b/apps/web/src/locales/zh_TW/messages.json @@ -615,7 +615,7 @@ "message": "使用主密碼登入" }, "readingPasskeyLoading": { - "message": "正在讀取密碼金輪..." + "message": "正在讀取金輪..." }, "readingPasskeyLoadingInfo": { "message": "請保持視窗開啟,並按照瀏覽器指示操作。" @@ -627,7 +627,7 @@ "message": "使用密碼金鑰登入" }, "invalidPasskeyPleaseTryAgain": { - "message": "無效的密碼金鑰,請再試一次。" + "message": "無效的金鑰。請再試一次。" }, "twoFactorForPasskeysNotSupportedOnClientUpdateToLogIn": { "message": "不支援的密碼金鑰的 2FA 兩階段認證。更新 app 以登入" @@ -1355,7 +1355,7 @@ "description": "This will be part of a larger sentence, that will read like this: If you don't have any data to import, you can create a new item instead. (Optional second half: You may need to wait until your administrator confirms your organization membership.)" }, "onboardingImportDataDetailsLink": { - "message": "new item", + "message": "新項目", "description": "This will be part of a larger sentence, that will read like this: If you don't have any data to import, you can create a new item instead. (Optional second half: You may need to wait until your administrator confirms your organization membership.)" }, "onboardingImportDataDetailsPartTwoNoOrgs": { @@ -2835,7 +2835,7 @@ "message": "不正確的代碼" }, "incorrectPin": { - "message": "Incorrect PIN" + "message": "PIN 碼不正確" }, "exportedVault": { "message": "已匯出密碼庫" @@ -4044,7 +4044,7 @@ "message": "您現在可以關閉此分頁,並且回到擴充套件繼續。" }, "youSuccessfullyLoggedIn": { - "message": "You successfully logged in" + "message": "你已成功登入" }, "thisWindowWillCloseIn5Seconds": { "message": "This window will automatically close in 5 seconds" @@ -4951,11 +4951,14 @@ "message": "建立一個新的客戶組織,該組織將作為提供者與您建立關聯。您將能夠存取和管理此組織。" }, "newClient": { - "message": "New client" + "message": "新用戶端" }, "addExistingOrganization": { "message": "新增現有組織" }, + "addNewOrganization": { + "message": "新增組織" + }, "myProvider": { "message": "我的提供者" }, @@ -5943,7 +5946,7 @@ "message": "Duo two-step login is required for your account." }, "launchDuo": { - "message": "Launch Duo" + "message": "啟動 Duo" }, "turnOn": { "message": "開啟" @@ -6647,40 +6650,40 @@ "message": "Verification required for this action. Set a PIN to continue." }, "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": "重新傳送驗證碼" }, "memberColumnHeader": { - "message": "Member" + "message": "成員" }, "groupSlashMemberColumnHeader": { - "message": "Group/Member" + "message": "群組/成員" }, "selectGroupsAndMembers": { "message": "選擇群組和成員" @@ -6934,7 +6937,7 @@ "description": "Software Development Kit" }, "createAnAccount": { - "message": "Create an account" + "message": "建立帳號" }, "createSecret": { "message": "建立機密" @@ -7147,7 +7150,7 @@ } }, "verificationRequired": { - "message": "Verification required", + "message": "需要驗證", "description": "Default title for the user verification dialog." }, "recoverAccount": { @@ -7475,7 +7478,7 @@ "description": "This is followed a by a hyperlink to the help website." }, "installBrowserExtension": { - "message": "Install browser extension" + "message": "安裝瀏覽器擴充套件" }, "installBrowserExtensionDetails": { "message": "Use the extension to quickly save logins and auto-fill forms without opening the web app." @@ -7498,6 +7501,9 @@ "collectionAccessRestricted": { "message": "Collection access is restricted" }, + "readOnlyCollectionAccess": { + "message": "You do not have access to manage this collection." + }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." }, @@ -7511,7 +7517,7 @@ "message": "Service account access updated" }, "commonImportFormats": { - "message": "Common formats", + "message": "常見格式", "description": "Label indicating the most common import formats" }, "maintainYourSubscription": { @@ -7525,7 +7531,7 @@ } }, "addAPaymentMethod": { - "message": "add a payment method", + "message": "新增付款方式", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To maintain your subscription for $ORG$, add a payment method.'" }, "collectionEnhancementsDesc": { @@ -7536,7 +7542,7 @@ "message": "Learn more about collection management" }, "organizationInformation": { - "message": "Organization information" + "message": "組織資訊" }, "confirmationDetails": { "message": "Confirmation details" @@ -7548,7 +7554,7 @@ "message": "We've sent a confirmation email to your email at " }, "confirmCollectionEnhancementsDialogTitle": { - "message": "This action is irreversible" + "message": "此操作是不可逆轉的" }, "confirmCollectionEnhancementsDialogContent": { "message": "Turning on this feature will deprecate the manager role and replace it with a Can manage permission. This will take a few moments. Do not make any organization changes until it is complete. Are you sure you want to proceed?" @@ -7600,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "viewCollection": { + "message": "View collection" + }, "restrictedGroupAccess": { "message": "You cannot add yourself to groups." }, @@ -7642,5 +7651,256 @@ }, "items": { "message": "Items" + }, + "assignedSeats": { + "message": "Assigned seats" + }, + "assigned": { + "message": "已指派" + }, + "used": { + "message": "已使用" + }, + "remaining": { + "message": "剩餘" + }, + "unlinkOrganization": { + "message": "取消連結組織" + }, + "manageSeats": { + "message": "MANAGE SEATS" + }, + "manageSeatsDescription": { + "message": "Adjustments to seats will be reflected in the next billing cycle." + }, + "unassignedSeatsDescription": { + "message": "Unassigned subscription seats" + }, + "purchaseSeatDescription": { + "message": "Additional seats purchased" + }, + "assignedSeatCannotUpdate": { + "message": "Assigned Seats can not be updated. Please contact your organization owner for assistance." + }, + "subscriptionUpdateFailed": { + "message": "Subscription update failed" + }, + "trial": { + "message": "Trial", + "description": "A subscription status label." + }, + "pastDue": { + "message": "Past due", + "description": "A subscription status label" + }, + "subscriptionExpired": { + "message": "Subscription expired", + "description": "The date header used when a subscription is past due." + }, + "pastDueWarningForChargeAutomatically": { + "message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they are charged automatically." + }, + "pastDueWarningForSendInvoice": { + "message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "placeholders": { + "days": { + "content": "$1", + "example": "11" + }, + "suspension_date": { + "content": "$2", + "example": "01/10/2024" + } + }, + "description": "A warning shown to the user when their subscription is past due and they pay via invoice." + }, + "unpaidInvoice": { + "message": "Unpaid invoice", + "description": "The header of a warning box shown to a user whose subscription is unpaid." + }, + "toReactivateYourSubscription": { + "message": "To reactivate your subscription, please resolve the past due invoices.", + "description": "The body of a warning box shown to a user whose subscription is unpaid." + }, + "cancellationDate": { + "message": "Cancellation date", + "description": "The date header used when a subscription is cancelled." + }, + "machineAccountsCannotCreate": { + "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + }, + "machineAccount": { + "message": "Machine account", + "description": "A machine user which can be used to automate processes and access secrets in the system." + }, + "machineAccounts": { + "message": "Machine accounts", + "description": "The title for the section that deals with machine accounts." + }, + "newMachineAccount": { + "message": "New machine account", + "description": "Title for creating a new machine account." + }, + "machineAccountsNoItemsMessage": { + "message": "Create a new machine account to get started automating secret access.", + "description": "Message to encourage the user to start creating machine accounts." + }, + "machineAccountsNoItemsTitle": { + "message": "Nothing to show yet", + "description": "Title to indicate that there are no machine accounts to display." + }, + "deleteMachineAccounts": { + "message": "Delete machine accounts", + "description": "Title for the action to delete one or multiple machine accounts." + }, + "deleteMachineAccount": { + "message": "Delete machine account", + "description": "Title for the action to delete a single machine account." + }, + "viewMachineAccount": { + "message": "View machine account", + "description": "Action to view the details of a machine account." + }, + "deleteMachineAccountDialogMessage": { + "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "placeholders": { + "machine_account": { + "content": "$1", + "example": "Machine account name" + } + } + }, + "deleteMachineAccountsDialogMessage": { + "message": "Deleting machine accounts is permanent and irreversible." + }, + "deleteMachineAccountsConfirmMessage": { + "message": "Delete $COUNT$ machine accounts", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, + "deleteMachineAccountToast": { + "message": "Machine account deleted" + }, + "deleteMachineAccountsToast": { + "message": "Machine accounts deleted" + }, + "searchMachineAccounts": { + "message": "Search machine accounts", + "description": "Placeholder text for searching machine accounts." + }, + "editMachineAccount": { + "message": "Edit machine account", + "description": "Title for editing a machine account." + }, + "machineAccountName": { + "message": "Machine account name", + "description": "Label for the name of a machine account" + }, + "machineAccountCreated": { + "message": "Machine account created", + "description": "Notifies that a new machine account has been created" + }, + "machineAccountUpdated": { + "message": "Machine account updated", + "description": "Notifies that a machine account has been updated" + }, + "projectMachineAccountsDescription": { + "message": "Grant machine accounts access to this project." + }, + "projectMachineAccountsSelectHint": { + "message": "Type or select machine accounts" + }, + "projectEmptyMachineAccountAccessPolicies": { + "message": "Add machine accounts to grant access" + }, + "machineAccountPeopleDescription": { + "message": "Grant groups or people access to this machine account." + }, + "machineAccountProjectsDescription": { + "message": "Assign projects to this machine account. " + }, + "createMachineAccount": { + "message": "Create a machine account" + }, + "maPeopleWarningMessage": { + "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + }, + "smAccessRemovalWarningMaTitle": { + "message": "Remove access to this machine account" + }, + "smAccessRemovalWarningMaMessage": { + "message": "This action will remove your access to the machine account." + }, + "machineAccountsIncluded": { + "message": "$COUNT$ machine accounts included", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, + "additionalMachineAccountCost": { + "message": "$COST$ per month for additional machine accounts", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "additionalMachineAccounts": { + "message": "Additional machine accounts" + }, + "includedMachineAccounts": { + "message": "Your plan comes with $COUNT$ machine accounts.", + "placeholders": { + "count": { + "content": "$1", + "example": "50" + } + } + }, + "addAdditionalMachineAccounts": { + "message": "You can add additional machine accounts for $COST$ per month.", + "placeholders": { + "cost": { + "content": "$1", + "example": "$0.50" + } + } + }, + "limitMachineAccounts": { + "message": "Limit machine accounts (optional)" + }, + "limitMachineAccountsDesc": { + "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + }, + "machineAccountLimit": { + "message": "Machine account limit (optional)" + }, + "maxMachineAccountCost": { + "message": "Max potential machine account cost" + }, + "machineAccountAccessUpdated": { + "message": "Machine account access updated" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } From d0bcc757216ea35c212077b040a036be8611d7f9 Mon Sep 17 00:00:00 2001 From: Bitwarden DevOps <106330231+bitwarden-devops-bot@users.noreply.github.com> Date: Mon, 15 Apr 2024 12:14:45 -0400 Subject: [PATCH 182/351] Bumped desktop version to (#8751) --- apps/desktop/package.json | 2 +- apps/desktop/src/package-lock.json | 4 ++-- apps/desktop/src/package.json | 2 +- package-lock.json | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 0dc23b04b1..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.4.1", + "version": "2024.4.2", "keywords": [ "bitwarden", "password", diff --git a/apps/desktop/src/package-lock.json b/apps/desktop/src/package-lock.json index 0531345131..11b38bd273 100644 --- a/apps/desktop/src/package-lock.json +++ b/apps/desktop/src/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bitwarden/desktop", - "version": "2024.4.1", + "version": "2024.4.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bitwarden/desktop", - "version": "2024.4.1", + "version": "2024.4.2", "license": "GPL-3.0", "dependencies": { "@bitwarden/desktop-native": "file:../desktop_native", diff --git a/apps/desktop/src/package.json b/apps/desktop/src/package.json index 6527c21521..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.4.1", + "version": "2024.4.2", "author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)", "homepage": "https://bitwarden.com", "license": "GPL-3.0", diff --git a/package-lock.json b/package-lock.json index c399536cca..123dead7a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -233,7 +233,7 @@ }, "apps/desktop": { "name": "@bitwarden/desktop", - "version": "2024.4.1", + "version": "2024.4.2", "hasInstallScript": true, "license": "GPL-3.0" }, From 576431d29e18a37293ff33a92c18b463ea307dc0 Mon Sep 17 00:00:00 2001 From: Jake Fink <jfink@bitwarden.com> Date: Mon, 15 Apr 2024 12:34:30 -0400 Subject: [PATCH 183/351] [PM-5499] auth request service migrations (#8597) * move auth request storage to service * create migrations for auth requests * fix tests * fix browser * fix login strategy * update migration * use correct test descriptions in migration --- .../auth-request-service.factory.ts | 8 +- .../browser/src/background/main.background.ts | 2 + apps/cli/src/bw.ts | 1 + .../src/app/accounts/settings.component.ts | 13 +- .../src/vault/app/vault/vault.component.ts | 11 +- .../login-via-auth-request.component.ts | 30 ++-- .../src/services/jslib-services.module.ts | 2 + .../auth-request.service.abstraction.ts | 40 +++++ .../auth-request-login.strategy.ts | 6 +- .../common/login-strategies/login.strategy.ts | 14 +- .../password-login.strategy.ts | 7 +- .../sso-login.strategy.spec.ts | 4 +- .../login-strategies/sso-login.strategy.ts | 16 +- .../user-api-login.strategy.ts | 11 +- .../webauthn-login.strategy.ts | 3 +- .../auth-request/auth-request.service.spec.ts | 28 ++++ .../auth-request/auth-request.service.ts | 88 ++++++++++- .../models/domain/admin-auth-req-storable.ts | 10 +- .../platform/abstractions/state.service.ts | 8 - .../src/platform/models/domain/account.ts | 5 - .../src/platform/services/state.service.ts | 51 ------- .../src/platform/state/state-definitions.ts | 3 + .../src/services/notifications.service.ts | 14 +- libs/common/src/state-migrations/migrate.ts | 6 +- .../migrations/56-move-auth-requests.spec.ts | 138 ++++++++++++++++++ .../migrations/56-move-auth-requests.ts | 104 +++++++++++++ 26 files changed, 503 insertions(+), 120 deletions(-) create mode 100644 libs/common/src/state-migrations/migrations/56-move-auth-requests.spec.ts create mode 100644 libs/common/src/state-migrations/migrations/56-move-auth-requests.ts 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 295fedbadd..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 @@ -17,6 +17,10 @@ import { FactoryOptions, factory, } from "../../../platform/background/service-factories/factory-options"; +import { + stateProviderFactory, + StateProviderInitOptions, +} from "../../../platform/background/service-factories/state-provider.factory"; import { accountServiceFactory, AccountServiceInitOptions } from "./account-service.factory"; import { @@ -31,7 +35,8 @@ export type AuthRequestServiceInitOptions = AuthRequestServiceFactoryOptions & AccountServiceInitOptions & MasterPasswordServiceInitOptions & CryptoServiceInitOptions & - ApiServiceInitOptions; + ApiServiceInitOptions & + StateProviderInitOptions; export function authRequestServiceFactory( cache: { authRequestService?: AuthRequestServiceAbstraction } & CachedServices, @@ -48,6 +53,7 @@ export function authRequestServiceFactory( await internalMasterPasswordServiceFactory(cache, opts), await cryptoServiceFactory(cache, opts), await apiServiceFactory(cache, opts), + await stateProviderFactory(cache, opts), ), ); } diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 4aecf8f585..105e7e2a38 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -596,6 +596,7 @@ export default class MainBackground { this.masterPasswordService, this.cryptoService, this.apiService, + this.stateProvider, ); this.authService = new AuthService( @@ -844,6 +845,7 @@ export default class MainBackground { logoutCallback, this.stateService, this.authService, + this.authRequestService, this.messagingService, ); diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index ebae308a81..4228eba965 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -483,6 +483,7 @@ export class Main { this.masterPasswordService, this.cryptoService, this.apiService, + this.stateProvider, ); this.billingAccountProfileStateService = new DefaultBillingAccountProfileStateService( diff --git a/apps/desktop/src/app/accounts/settings.component.ts b/apps/desktop/src/app/accounts/settings.component.ts index 07fcc5d3b8..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,6 +21,7 @@ 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"; @@ -60,6 +62,7 @@ export class SettingsComponent implements OnInit { showAppPreferences = true; currentUserEmail: string; + currentUserId: UserId; availableVaultTimeoutActions$: Observable<VaultTimeoutAction[]>; vaultTimeoutPolicyCallout: Observable<{ @@ -122,6 +125,7 @@ export class SettingsComponent implements OnInit { private desktopSettingsService: DesktopSettingsService, private biometricStateService: BiometricStateService, private desktopAutofillSettingsService: DesktopAutofillSettingsService, + private authRequestService: AuthRequestServiceAbstraction, ) { const isMac = this.platformUtilsService.getDevice() === DeviceType.MacOsDesktop; @@ -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$), @@ -665,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() { 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/libs/angular/src/auth/components/login-via-auth-request.component.ts b/libs/angular/src/auth/components/login-via-auth-request.component.ts index 6ba94d3001..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 @@ -33,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"; @@ -131,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")); @@ -142,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(); @@ -173,7 +175,10 @@ export class LoginViaAuthRequestComponent this.destroy$.complete(); } - 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. @@ -183,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 @@ -203,7 +208,7 @@ export class LoginViaAuthRequestComponent // Request denied if (adminAuthReqResponse.isAnswered && !adminAuthReqResponse.requestApproved) { - return await this.handleExistingAdminAuthReqDeletedOrDenied(); + return await this.handleExistingAdminAuthReqDeletedOrDenied(userId); } // Request approved @@ -211,6 +216,7 @@ export class LoginViaAuthRequestComponent return await this.handleApprovedAdminAuthRequest( adminAuthReqResponse, adminAuthReqStorable.privateKey, + userId, ); } @@ -219,9 +225,9 @@ export class LoginViaAuthRequestComponent 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. @@ -269,7 +275,8 @@ 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); @@ -333,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, ); } @@ -363,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 @@ -384,7 +394,7 @@ 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")); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 3de36020da..b311000fb8 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -740,6 +740,7 @@ const safeProviders: SafeProvider[] = [ LOGOUT_CALLBACK, StateServiceAbstraction, AuthServiceAbstraction, + AuthRequestServiceAbstraction, MessagingServiceAbstraction, ], }), @@ -963,6 +964,7 @@ const safeProviders: SafeProvider[] = [ InternalMasterPasswordServiceAbstraction, CryptoServiceAbstraction, ApiServiceAbstraction, + StateProvider, ], }), safeProvider({ 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 7af92fc8f8..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,12 +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<string>; + + /** + * 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<boolean>; + /** + * 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<void>; + /** + * 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<AdminAuthRequestStorable | null>; + /** + * 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<void>; + /** + * 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<void>; /** * Approve or deny an auth request. * @param approve True to approve, false to deny. 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 e47f0f88ee..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 @@ -132,7 +132,10 @@ export class AuthRequestLoginStrategy extends LoginStrategy { } } - protected override async setUserKey(response: IdentityTokenResponse): Promise<void> { + protected override async setUserKey( + response: IdentityTokenResponse, + userId: UserId, + ): Promise<void> { 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 @@ -143,7 +146,6 @@ export class AuthRequestLoginStrategy extends LoginStrategy { } else { await this.trySetUserKeyWithMasterKey(); - const userId = (await this.stateService.getUserId()) as UserId; // Establish trust if required after setting user key await this.deviceTrustCryptoService.trustDeviceIfRequired(userId); } diff --git a/libs/auth/src/common/login-strategies/login.strategy.ts b/libs/auth/src/common/login-strategies/login.strategy.ts index df6aa171db..94f96d40d0 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.ts @@ -32,6 +32,7 @@ import { 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 { @@ -160,14 +161,11 @@ export abstract class LoginStrategy { * @param {IdentityTokenResponse} tokenResponse - The response from the server containing the identity token. * @returns {Promise<void>} - A promise that resolves when the account information has been successfully saved. */ - protected async saveAccountInformation(tokenResponse: IdentityTokenResponse): Promise<void> { + protected async saveAccountInformation(tokenResponse: IdentityTokenResponse): Promise<UserId> { const accountInformation = await this.tokenService.decodeAccessToken(tokenResponse.accessToken); const userId = accountInformation.sub; - // 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(); @@ -197,7 +195,6 @@ export abstract class LoginStrategy { tokens: { ...new AccountTokens(), }, - adminAuthRequest: adminAuthRequest?.toJSON(), }), ); @@ -206,6 +203,7 @@ export abstract class LoginStrategy { ); await this.billingAccountProfileStateService.setHasPremium(accountInformation.premium, false); + return userId as UserId; } protected async processTokenResponse(response: IdentityTokenResponse): Promise<AuthResult> { @@ -228,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 @@ -238,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"); @@ -248,7 +246,7 @@ export abstract class LoginStrategy { // The keys comes from different sources depending on the login strategy protected abstract setMasterKey(response: IdentityTokenResponse): Promise<void>; - protected abstract setUserKey(response: IdentityTokenResponse): Promise<void>; + protected abstract setUserKey(response: IdentityTokenResponse, userId: UserId): Promise<void>; protected abstract setPrivateKey(response: IdentityTokenResponse): Promise<void>; // 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.ts b/libs/auth/src/common/login-strategies/password-login.strategy.ts index 52c97d5d85..2490c35a00 100644 --- a/libs/auth/src/common/login-strategies/password-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/password-login.strategy.ts @@ -25,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"; @@ -207,14 +208,16 @@ export class PasswordLoginStrategy extends LoginStrategy { await this.masterPasswordService.setMasterKeyHash(localMasterKeyHash, userId); } - protected override async setUserKey(response: IdentityTokenResponse): Promise<void> { + protected override async setUserKey( + response: IdentityTokenResponse, + userId: UserId, + ): Promise<void> { // 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 userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); if (masterKey) { const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey); 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 bce62681d0..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 @@ -301,7 +301,7 @@ describe("SsoLoginStrategy", () => { id: "1", privateKey: "PRIVATE" as any, } as AdminAuthRequestStorable; - stateService.getAdminAuthRequest.mockResolvedValue( + authRequestService.getAdminAuthRequest.mockResolvedValue( new AdminAuthRequestStorable(adminAuthRequest), ); }); @@ -364,7 +364,7 @@ describe("SsoLoginStrategy", () => { await ssoLoginStrategy.logIn(credentials); - expect(stateService.setAdminAuthRequest).toHaveBeenCalledWith(null); + expect(authRequestService.clearAdminAuthRequest).toHaveBeenCalled(); expect( authRequestService.setKeysAfterDecryptingSharedMasterKeyAndHash, ).not.toHaveBeenCalled(); 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 db0228a338..d8efd78984 100644 --- a/libs/auth/src/common/login-strategies/sso-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/sso-login.strategy.ts @@ -216,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<void> { + protected override async setUserKey( + tokenResponse: IdentityTokenResponse, + userId: UserId, + ): Promise<void> { const masterKeyEncryptedUserKey = tokenResponse.key; // Note: masterKeyEncryptedUserKey is undefined for SSO JIT provisioned users @@ -232,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(); @@ -252,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<void> { + private async trySetUserKeyWithApprovedAdminRequestIfExists(userId: UserId): Promise<void> { // 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; @@ -268,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 @@ -295,12 +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 - const userId = (await this.stateService.getUserId()) as UserId; 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")); } 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 421746b49c..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 @@ -18,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"; @@ -97,7 +98,10 @@ export class UserApiLoginStrategy extends LoginStrategy { } } - protected override async setUserKey(response: IdentityTokenResponse): Promise<void> { + protected override async setUserKey( + response: IdentityTokenResponse, + userId: UserId, + ): Promise<void> { await this.cryptoService.setMasterKeyEncryptedUserKey(response.key); if (response.apiUseKeyConnector) { @@ -116,8 +120,8 @@ export class UserApiLoginStrategy extends LoginStrategy { ); } - protected async saveAccountInformation(tokenResponse: IdentityTokenResponse) { - await super.saveAccountInformation(tokenResponse); + protected async saveAccountInformation(tokenResponse: IdentityTokenResponse): Promise<UserId> { + const userId = await super.saveAccountInformation(tokenResponse); const vaultTimeout = await this.stateService.getVaultTimeout(); const vaultTimeoutAction = await this.stateService.getVaultTimeoutAction(); @@ -134,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.ts b/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts index 843978e2a2..8a62a8fb3c 100644 --- a/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts @@ -17,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"; @@ -98,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/services/auth-request/auth-request.service.spec.ts b/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts index f04628ffd9..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 @@ -9,6 +9,7 @@ import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.se 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"; @@ -18,6 +19,7 @@ import { AuthRequestService } from "./auth-request.service"; describe("AuthRequestService", () => { let sut: AuthRequestService; + const stateProvider = mock<StateProvider>(); let accountService: FakeAccountService; let masterPasswordService: FakeMasterPasswordService; const appIdService = mock<AppIdService>(); @@ -38,6 +40,7 @@ describe("AuthRequestService", () => { masterPasswordService, cryptoService, apiService, + stateProvider, ); mockPrivateKey = new Uint8Array(64); @@ -59,6 +62,31 @@ describe("AuthRequestService", () => { }); }); + 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({ 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 5f8dcfd729..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,8 +1,10 @@ -import { firstValueFrom, Observable, Subject } from "rxjs"; +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"; @@ -10,10 +12,43 @@ import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.ser import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.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<boolean>( + 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<Jsonify<AdminAuthRequestStorable>>( + AUTH_REQUEST_DISK_LOCAL, + "adminAuthRequest", + { + deserializer: (value) => value, + clearOn: [], + }, +); + export class AuthRequestService implements AuthRequestServiceAbstraction { private authRequestPushNotificationSubject = new Subject<string>(); authRequestPushNotification$: Observable<string>; @@ -24,10 +59,61 @@ export class AuthRequestService implements AuthRequestServiceAbstraction { private masterPasswordService: InternalMasterPasswordServiceAbstraction, private cryptoService: CryptoService, private apiService: ApiService, + private stateProvider: StateProvider, ) { this.authRequestPushNotification$ = this.authRequestPushNotificationSubject.asObservable(); } + async getAcceptAuthRequests(userId: UserId): Promise<boolean> { + 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<void> { + 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<AdminAuthRequestStorable | null> { + 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<void> { + 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<void> { + if (userId == null) { + throw new Error("User ID is required"); + } + + await this.stateProvider.setUserState(ADMIN_AUTH_REQUEST_KEY, null, userId); + } + async approveOrDenyAuthRequest( approve: boolean, authRequest: AuthRequestResponse, 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>): AdminAuthRequestStorable { if (obj == null) { return null; } diff --git a/libs/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index 3017ae7195..fd76cad6d1 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -1,6 +1,5 @@ import { Observable } from "rxjs"; -import { AdminAuthRequestStorable } from "../../auth/models/domain/admin-auth-req-storable"; import { KdfConfig } from "../../auth/models/domain/kdf-config"; import { BiometricKey } from "../../auth/types/biometric-key"; import { GeneratorOptions } from "../../tools/generator/generator-options"; @@ -124,11 +123,6 @@ export abstract class StateService<T extends Account = Account> { setDecryptedPinProtected: (value: EncString, options?: StorageOptions) => Promise<void>; getDuckDuckGoSharedKey: (options?: StorageOptions) => Promise<string>; setDuckDuckGoSharedKey: (value: string, options?: StorageOptions) => Promise<void>; - getAdminAuthRequest: (options?: StorageOptions) => Promise<AdminAuthRequestStorable | null>; - setAdminAuthRequest: ( - adminAuthRequest: AdminAuthRequestStorable, - options?: StorageOptions, - ) => Promise<void>; getEmail: (options?: StorageOptions) => Promise<string>; setEmail: (value: string, options?: StorageOptions) => Promise<void>; getEmailVerified: (options?: StorageOptions) => Promise<boolean>; @@ -207,7 +201,5 @@ export abstract class StateService<T extends Account = Account> { setVaultTimeout: (value: number, options?: StorageOptions) => Promise<void>; getVaultTimeoutAction: (options?: StorageOptions) => Promise<string>; setVaultTimeoutAction: (value: string, options?: StorageOptions) => Promise<void>; - getApproveLoginRequests: (options?: StorageOptions) => Promise<boolean>; - setApproveLoginRequests: (value: boolean, options?: StorageOptions) => Promise<void>; nextUpActiveUser: () => Promise<UserId>; } diff --git a/libs/common/src/platform/models/domain/account.ts b/libs/common/src/platform/models/domain/account.ts index 753b15c09b..759a903514 100644 --- a/libs/common/src/platform/models/domain/account.ts +++ b/libs/common/src/platform/models/domain/account.ts @@ -1,6 +1,5 @@ import { Jsonify } from "type-fest"; -import { AdminAuthRequestStorable } from "../../../auth/models/domain/admin-auth-req-storable"; import { UriMatchStrategySetting } from "../../../models/domain/domain-service"; import { GeneratorOptions } from "../../../tools/generator/generator-options"; import { @@ -169,7 +168,6 @@ export class AccountSettings { protectedPin?: string; vaultTimeout?: number; vaultTimeoutAction?: string = "lock"; - approveLoginRequests?: boolean; /** @deprecated July 2023, left for migration purposes*/ pinProtected?: EncryptionPair<string, EncString> = new EncryptionPair<string, EncString>(); @@ -206,7 +204,6 @@ export class Account { profile?: AccountProfile = new AccountProfile(); settings?: AccountSettings = new AccountSettings(); tokens?: AccountTokens = new AccountTokens(); - adminAuthRequest?: Jsonify<AdminAuthRequestStorable> = null; constructor(init: Partial<Account>) { Object.assign(this, { @@ -230,7 +227,6 @@ export class Account { ...new AccountTokens(), ...init?.tokens, }, - adminAuthRequest: init?.adminAuthRequest, }); } @@ -245,7 +241,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/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index 7dbac2b02a..3d512175a8 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -3,7 +3,6 @@ import { Jsonify, JsonValue } from "type-fest"; import { AccountService } from "../../auth/abstractions/account.service"; import { TokenService } from "../../auth/abstractions/token.service"; -import { AdminAuthRequestStorable } from "../../auth/models/domain/admin-auth-req-storable"; import { KdfConfig } from "../../auth/models/domain/kdf-config"; import { BiometricKey } from "../../auth/types/biometric-key"; import { GeneratorOptions } from "../../tools/generator/generator-options"; @@ -548,37 +547,6 @@ export class StateService< : await this.secureStorageService.save(DDG_SHARED_KEY, value, options); } - async getAdminAuthRequest(options?: StorageOptions): Promise<AdminAuthRequestStorable | null> { - 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<void> { - 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 getEmail(options?: StorageOptions): Promise<string> { return ( await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) @@ -1032,24 +1000,6 @@ export class StateService< ); } - async getApproveLoginRequests(options?: StorageOptions): Promise<boolean> { - const approveLoginRequests = ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())) - )?.settings?.approveLoginRequests; - return approveLoginRequests; - } - - async setApproveLoginRequests(value: boolean, options?: StorageOptions): Promise<void> { - 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()), - ); - } - protected async getGlobals(options: StorageOptions): Promise<TGlobalState> { let globals: TGlobalState; if (this.useMemory(options.storageLocation)) { @@ -1392,7 +1342,6 @@ export class StateService< protected resetAccount(account: TAccount) { const persistentAccountInformation = { settings: account.settings, - adminAuthRequest: account.adminAuthRequest, }; return Object.assign(this.createAccount(), persistentAccountInformation); } diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index ed6ef1590d..518e5c51d6 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -45,6 +45,9 @@ 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", { 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<void>, 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/state-migrations/migrate.ts b/libs/common/src/state-migrations/migrate.ts index 76f0d7fd46..77b949126f 100644 --- a/libs/common/src/state-migrations/migrate.ts +++ b/libs/common/src/state-migrations/migrate.ts @@ -52,6 +52,7 @@ 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 { 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"; @@ -59,7 +60,7 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting import { MinVersionMigrator } from "./migrations/min-version"; export const MIN_VERSION = 3; -export const CURRENT_VERSION = 55; +export const CURRENT_VERSION = 56; export type MinVersion = typeof MIN_VERSION; @@ -117,7 +118,8 @@ export function createMigrationBuilder() { .with(DeleteInstalledVersion, 51, 52) .with(DeviceTrustCryptoServiceStateProviderMigrator, 52, 53) .with(SendMigrator, 53, 54) - .with(MoveMasterKeyStateToProviderMigrator, 54, CURRENT_VERSION); + .with(MoveMasterKeyStateToProviderMigrator, 54, 55) + .with(AuthRequestMigrator, 55, CURRENT_VERSION); } export async function currentVersion( 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<MigrationHelper>; + 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<void> { + const accounts = await helper.getAccounts<ExpectedAccountType>(); + + async function migrateAccount(userId: string, account: ExpectedAccountType): Promise<void> { + 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<void> { + const accounts = await helper.getAccounts<ExpectedAccountType>(); + + async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise<void> { + 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))]); + } +} From 5a371c11fcfef5200396b0ffcd5c2fc1d6affbb9 Mon Sep 17 00:00:00 2001 From: Robyn MacCallum <robyntmaccallum@gmail.com> Date: Mon, 15 Apr 2024 12:42:16 -0400 Subject: [PATCH 184/351] Revert "Bumped desktop version to (#8751)" (#8752) This reverts commit d0bcc757216ea35c212077b040a036be8611d7f9. --- apps/desktop/package.json | 2 +- apps/desktop/src/package-lock.json | 4 ++-- apps/desktop/src/package.json | 2 +- package-lock.json | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 4bb0ab2d93..0dc23b04b1 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.4.2", + "version": "2024.4.1", "keywords": [ "bitwarden", "password", diff --git a/apps/desktop/src/package-lock.json b/apps/desktop/src/package-lock.json index 11b38bd273..0531345131 100644 --- a/apps/desktop/src/package-lock.json +++ b/apps/desktop/src/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bitwarden/desktop", - "version": "2024.4.2", + "version": "2024.4.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bitwarden/desktop", - "version": "2024.4.2", + "version": "2024.4.1", "license": "GPL-3.0", "dependencies": { "@bitwarden/desktop-native": "file:../desktop_native", diff --git a/apps/desktop/src/package.json b/apps/desktop/src/package.json index a65dab016c..6527c21521 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.4.2", + "version": "2024.4.1", "author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)", "homepage": "https://bitwarden.com", "license": "GPL-3.0", diff --git a/package-lock.json b/package-lock.json index 123dead7a0..c399536cca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -233,7 +233,7 @@ }, "apps/desktop": { "name": "@bitwarden/desktop", - "version": "2024.4.2", + "version": "2024.4.1", "hasInstallScript": true, "license": "GPL-3.0" }, From ffcf660ff57114c9403a3b0dace2b4d7d3369fed Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 15 Apr 2024 17:55:41 +0000 Subject: [PATCH 185/351] Autosync the updated translations (#8741) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/web/src/locales/af/messages.json | 3 + apps/web/src/locales/ar/messages.json | 3 + apps/web/src/locales/az/messages.json | 3 + apps/web/src/locales/be/messages.json | 3 + apps/web/src/locales/bg/messages.json | 5 +- apps/web/src/locales/bn/messages.json | 3 + apps/web/src/locales/bs/messages.json | 3 + apps/web/src/locales/ca/messages.json | 3 + apps/web/src/locales/cs/messages.json | 3 + apps/web/src/locales/cy/messages.json | 3 + apps/web/src/locales/da/messages.json | 5 +- apps/web/src/locales/de/messages.json | 9 +- apps/web/src/locales/el/messages.json | 3 + apps/web/src/locales/en_GB/messages.json | 3 + apps/web/src/locales/en_IN/messages.json | 3 + apps/web/src/locales/eo/messages.json | 3 + apps/web/src/locales/es/messages.json | 3 + apps/web/src/locales/et/messages.json | 3 + apps/web/src/locales/eu/messages.json | 3 + apps/web/src/locales/fa/messages.json | 3 + apps/web/src/locales/fi/messages.json | 29 ++++--- apps/web/src/locales/fil/messages.json | 3 + apps/web/src/locales/fr/messages.json | 5 +- apps/web/src/locales/gl/messages.json | 3 + apps/web/src/locales/he/messages.json | 3 + apps/web/src/locales/hi/messages.json | 3 + apps/web/src/locales/hr/messages.json | 3 + apps/web/src/locales/hu/messages.json | 3 + apps/web/src/locales/id/messages.json | 3 + apps/web/src/locales/it/messages.json | 3 + apps/web/src/locales/ja/messages.json | 3 + apps/web/src/locales/ka/messages.json | 3 + apps/web/src/locales/km/messages.json | 3 + apps/web/src/locales/kn/messages.json | 3 + apps/web/src/locales/ko/messages.json | 3 + apps/web/src/locales/lv/messages.json | 3 + apps/web/src/locales/ml/messages.json | 3 + apps/web/src/locales/mr/messages.json | 3 + apps/web/src/locales/my/messages.json | 3 + apps/web/src/locales/nb/messages.json | 3 + apps/web/src/locales/ne/messages.json | 3 + apps/web/src/locales/nl/messages.json | 3 + apps/web/src/locales/nn/messages.json | 3 + apps/web/src/locales/or/messages.json | 3 + apps/web/src/locales/pl/messages.json | 3 + apps/web/src/locales/pt_BR/messages.json | 3 + apps/web/src/locales/pt_PT/messages.json | 3 + apps/web/src/locales/ro/messages.json | 3 + apps/web/src/locales/ru/messages.json | 3 + apps/web/src/locales/si/messages.json | 3 + apps/web/src/locales/sk/messages.json | 3 + apps/web/src/locales/sl/messages.json | 3 + apps/web/src/locales/sr/messages.json | 9 +- apps/web/src/locales/sr_CS/messages.json | 3 + apps/web/src/locales/sv/messages.json | 11 ++- apps/web/src/locales/te/messages.json | 3 + apps/web/src/locales/th/messages.json | 3 + apps/web/src/locales/tr/messages.json | 3 + apps/web/src/locales/uk/messages.json | 3 + apps/web/src/locales/vi/messages.json | 3 + apps/web/src/locales/zh_CN/messages.json | 103 ++++++++++++----------- apps/web/src/locales/zh_TW/messages.json | 3 + 62 files changed, 262 insertions(+), 76 deletions(-) diff --git a/apps/web/src/locales/af/messages.json b/apps/web/src/locales/af/messages.json index efd009b3ad..6dfb5ec30d 100644 --- a/apps/web/src/locales/af/messages.json +++ b/apps/web/src/locales/af/messages.json @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now 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 your All Vaults view across devices 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/web/src/locales/ar/messages.json b/apps/web/src/locales/ar/messages.json index e2ec92c79f..1923502b1a 100644 --- a/apps/web/src/locales/ar/messages.json +++ b/apps/web/src/locales/ar/messages.json @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now 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 your All Vaults view across devices 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/web/src/locales/az/messages.json b/apps/web/src/locales/az/messages.json index 6b84306675..911031f0f1 100644 --- a/apps/web/src/locales/az/messages.json +++ b/apps/web/src/locales/az/messages.json @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerSelfHost": { + "message": "Bildiriş: 2 May 2024-cü ildən etibarən təyin edilməmiş təşkilat elementləri artıq cihazlar arasında 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/web/src/locales/be/messages.json b/apps/web/src/locales/be/messages.json index 756b87237f..c93f773382 100644 --- a/apps/web/src/locales/be/messages.json +++ b/apps/web/src/locales/be/messages.json @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now 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 your All Vaults view across devices 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/web/src/locales/bg/messages.json b/apps/web/src/locales/bg/messages.json index c4a6597bf1..b3c20f50e2 100644 --- a/apps/web/src/locales/bg/messages.json +++ b/apps/web/src/locales/bg/messages.json @@ -7901,6 +7901,9 @@ "message": "Достъпът на машинния акаунт е променен" }, "unassignedItemsBanner": { - "message": "Забележка: неразпределените елементи на организацията вече не се виждат в изгледа с „Всички трезори“ на различните устройства, а са достъпни само през Административната конзола. Добавете тези елементи към някоя колекция в Административната конзола, за да станат видими." + "message": "Известие: неразпределените елементи на организацията вече не се виждат в изгледа с „Всички трезори“ на различните устройства, а са достъпни само през Административната конзола. Добавете тези елементи към някоя колекция в Административната конзола, за да станат видими." + }, + "unassignedItemsBannerSelfHost": { + "message": "Известие: от 2 май 2024г. неразпределените елементи на организациите вече няма се виждат в изгледа с „Всички трезори“ на различните устройства, а ще бъдат достъпни само през Административната конзола. Добавете тези елементи към някоя колекция в Административната конзола, за да станат видими." } } diff --git a/apps/web/src/locales/bn/messages.json b/apps/web/src/locales/bn/messages.json index 14457f7ddc..afde1e40d4 100644 --- a/apps/web/src/locales/bn/messages.json +++ b/apps/web/src/locales/bn/messages.json @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now 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 your All Vaults view across devices 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/web/src/locales/bs/messages.json b/apps/web/src/locales/bs/messages.json index a0033e67fe..f5cddc79db 100644 --- a/apps/web/src/locales/bs/messages.json +++ b/apps/web/src/locales/bs/messages.json @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now 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 your All Vaults view across devices 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/web/src/locales/ca/messages.json b/apps/web/src/locales/ca/messages.json index 97d870febc..2bec515116 100644 --- a/apps/web/src/locales/ca/messages.json +++ b/apps/web/src/locales/ca/messages.json @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now 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 your All Vaults view across devices 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/web/src/locales/cs/messages.json b/apps/web/src/locales/cs/messages.json index e8816e9dd6..82dfe2ecae 100644 --- a/apps/web/src/locales/cs/messages.json +++ b/apps/web/src/locales/cs/messages.json @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "Upozornění: Nepřiřazené položky organizace již nejsou viditelné ve Vašem zobrazení všech trezorů napříč zařízeními 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 ve všech zařízeních 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/web/src/locales/cy/messages.json b/apps/web/src/locales/cy/messages.json index 8883fd78cb..8ee7ab3569 100644 --- a/apps/web/src/locales/cy/messages.json +++ b/apps/web/src/locales/cy/messages.json @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now 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 your All Vaults view across devices 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/web/src/locales/da/messages.json b/apps/web/src/locales/da/messages.json index 683c1e7862..e52510ed1c 100644 --- a/apps/web/src/locales/da/messages.json +++ b/apps/web/src/locales/da/messages.json @@ -7734,7 +7734,7 @@ "description": "The body of a warning box shown to a user whose subscription is unpaid." }, "cancellationDate": { - "message": "Cancellation date", + "message": "Opsigelsesdato", "description": "The date header used when a subscription is cancelled." }, "machineAccountsCannotCreate": { @@ -7902,5 +7902,8 @@ }, "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 på tværs af enheder 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/web/src/locales/de/messages.json b/apps/web/src/locales/de/messages.json index 2679bc189e..0e9b0a983f 100644 --- a/apps/web/src/locales/de/messages.json +++ b/apps/web/src/locales/de/messages.json @@ -4957,7 +4957,7 @@ "message": "Bestehende Organisation hinzufügen" }, "addNewOrganization": { - "message": "Neue Organisation erstellen" + "message": "Neue Organisation hinzufügen" }, "myProvider": { "message": "Mein Anbieter" @@ -7502,7 +7502,7 @@ "message": "Zugriff auf Sammlungen ist eingeschränkt" }, "readOnlyCollectionAccess": { - "message": "Du hast keinen Zugriff, um diese Sammlung zu verwalten." + "message": "Du hast keinen Zugriff zur Verwaltung dieser Sammlung." }, "grantCollectionAccess": { "message": "Gewähre Gruppen oder Mitgliedern Zugriff auf diese Sammlung." @@ -7665,7 +7665,7 @@ "message": "Verbleibend" }, "unlinkOrganization": { - "message": "Organisations-Verknüpfung aufheben" + "message": "Organisationsverknüpfung aufheben" }, "manageSeats": { "message": "Benutzerpätze verwalten" @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "Hinweis: Nicht zugewiesene Organisationseinträge sind nicht mehr geräteübergreifend in der Ansicht aller Tresore sichtbar und sind nun 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 geräteübergreifend 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/web/src/locales/el/messages.json b/apps/web/src/locales/el/messages.json index 4502d940b2..256a5644d6 100644 --- a/apps/web/src/locales/el/messages.json +++ b/apps/web/src/locales/el/messages.json @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now 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 your All Vaults view across devices 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/web/src/locales/en_GB/messages.json b/apps/web/src/locales/en_GB/messages.json index 88d67c51b4..152c6a89e1 100644 --- a/apps/web/src/locales/en_GB/messages.json +++ b/apps/web/src/locales/en_GB/messages.json @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now 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 your All Vaults view across devices 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/web/src/locales/en_IN/messages.json b/apps/web/src/locales/en_IN/messages.json index ea3ff3f395..d4d22f6aba 100644 --- a/apps/web/src/locales/en_IN/messages.json +++ b/apps/web/src/locales/en_IN/messages.json @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now 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 your All Vaults view across devices 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/web/src/locales/eo/messages.json b/apps/web/src/locales/eo/messages.json index 929a83d4fa..899558160f 100644 --- a/apps/web/src/locales/eo/messages.json +++ b/apps/web/src/locales/eo/messages.json @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now 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 your All Vaults view across devices 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/web/src/locales/es/messages.json b/apps/web/src/locales/es/messages.json index 8f9d2993eb..78b051d588 100644 --- a/apps/web/src/locales/es/messages.json +++ b/apps/web/src/locales/es/messages.json @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now 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 your All Vaults view across devices 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/web/src/locales/et/messages.json b/apps/web/src/locales/et/messages.json index b41732103f..fdb656f31e 100644 --- a/apps/web/src/locales/et/messages.json +++ b/apps/web/src/locales/et/messages.json @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now 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 your All Vaults view across devices 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/web/src/locales/eu/messages.json b/apps/web/src/locales/eu/messages.json index a57e36221a..0125ceec25 100644 --- a/apps/web/src/locales/eu/messages.json +++ b/apps/web/src/locales/eu/messages.json @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now 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 your All Vaults view across devices 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/web/src/locales/fa/messages.json b/apps/web/src/locales/fa/messages.json index 0298bbc64b..e91ea40d05 100644 --- a/apps/web/src/locales/fa/messages.json +++ b/apps/web/src/locales/fa/messages.json @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now 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 your All Vaults view across devices 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/web/src/locales/fi/messages.json b/apps/web/src/locales/fi/messages.json index 8875133d01..18fa9c15fb 100644 --- a/apps/web/src/locales/fi/messages.json +++ b/apps/web/src/locales/fi/messages.json @@ -7622,10 +7622,10 @@ "message": "Määritä kokoelmiin" }, "assignToTheseCollections": { - "message": "Määritä näihin kokoelmiin" + "message": "Määritä seuraaviin kokoelmiin" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Valitse kokoelmat, joihin kohteet jaetaan. Kun kohdetta muokataan yhdessä kokoelmassa, päivittyy muutos kaikkiin kokoelmiin. Kohteet näkyvät vain niille organisaation jäsenille, joilla on näiden kokoelmien käyttöoikeus." + "message": "Valitse kokoelmat, joihin kohteet sisällytetään. Kun kohdetta muokataan yhdessä kokoelmassa, päivittyy muutos kaikkiin kokoelmiin. Kohteet näkyvät vain niille organisaation jäsenille, joilla on näiden kokoelmien käyttöoikeus." }, "selectCollectionsToAssign": { "message": "Valitse määritettävät kokoelmat" @@ -7634,10 +7634,10 @@ "message": "Kokoelmia ei ole määritetty" }, "successfullyAssignedCollections": { - "message": "Kokoelmat on määritetty" + "message": "Kokoelmat määritettiin" }, "bulkCollectionAssignmentWarning": { - "message": "Olet valinnut $TOTAL_COUNT$ kohdetta. Et voi muuttaa näistä $READONLY_COUNT$ kohdetta, koska käyttöoikeutesi eivät riitä niiden muokkaukseen.", + "message": "Olet valinnut $TOTAL_COUNT$ kohdetta. Et voi päivittää näistä $READONLY_COUNT$ kohdetta, koska käyttöoikeutesi eivät salli muokkausta.", "placeholders": { "total_count": { "content": "$1", @@ -7677,7 +7677,7 @@ "message": "Tilauksen määrittämättömät käyttäjäpaikat" }, "purchaseSeatDescription": { - "message": "Käyttäjäpaikkoja ostettu lisää" + "message": "Käyttäjäpaikkoja ostettiin lisää" }, "assignedSeatCannotUpdate": { "message": "Määritettyjä käyttäjäpaikkoja ei ole mahdollista päivittää. Ole yhteydessä organisaatiosi omistajaan saadaksesi apua." @@ -7698,7 +7698,7 @@ "description": "The date header used when a subscription is past due." }, "pastDueWarningForChargeAutomatically": { - "message": "Sinulla on $DAYS$ päivän varoaika tilauksesi säilyttämiseksi. Maksa erääntyneet laskut $SUSPENSION_DATE$ mennessä.", + "message": "Tilauksesi päätyttyä sinulla on $DAYS$ päivän varoaika sen säilyttämiseksi. Maksa erääntyneet laskut $SUSPENSION_DATE$ mennessä.", "placeholders": { "days": { "content": "$1", @@ -7712,7 +7712,7 @@ "description": "A warning shown to the user when their subscription is past due and they are charged automatically." }, "pastDueWarningForSendInvoice": { - "message": "Sinulla on $DAYS$ päivän varoaika ensimmäisen erääntyneen laskusi eräpäivästä tilauksesi säilyttämiseksi. Maksa erääntyneet laskut $SUSPENSION_DATE$ mennessä.", + "message": "Ensimmäisen maksamattoman laskusi eräpäivästä sinulla on $DAYS$ päivän varoaika tilauksesi säilyttämiseksi. Maksa erääntyneet laskut $SUSPENSION_DATE$ mennessä.", "placeholders": { "days": { "content": "$1", @@ -7738,7 +7738,7 @@ "description": "The date header used when a subscription is cancelled." }, "machineAccountsCannotCreate": { - "message": "Konetilien luonti ei ole mahdollista jäädytetyissä organisaatioissa. Ole yhteydessä organisaatiosi omistajaan saadaksesi apua." + "message": "Konetilejä ei ole mahdollista luoda jäädytetyissä organisaatioissa. Ole yhteydessä organisaatiosi omistajaan saadaksesi apua." }, "machineAccount": { "message": "Konetili", @@ -7753,11 +7753,11 @@ "description": "Title for creating a new machine account." }, "machineAccountsNoItemsMessage": { - "message": "Aloita salaisen käytön automatisointi luomalla uusi konetili.", + "message": "Aloita salaisuuksien käytön automatisointi luomalla uusi konetili.", "description": "Message to encourage the user to start creating machine accounts." }, "machineAccountsNoItemsTitle": { - "message": "Ei vielä mitään näytettävää", + "message": "Mitään näytettävää ei vielä ole", "description": "Title to indicate that there are no machine accounts to display." }, "deleteMachineAccounts": { @@ -7820,7 +7820,7 @@ "description": "Notifies that a machine account has been updated" }, "projectMachineAccountsDescription": { - "message": "Myönnä konetileille käyttöoikeus projektiin." + "message": "Myönnä konetileille projektin käyttöoikeus." }, "projectMachineAccountsSelectHint": { "message": "Syötä tai valitse konetilit" @@ -7838,7 +7838,7 @@ "message": "Luo konetili" }, "maPeopleWarningMessage": { - "message": "Henkilöiden poistaminen konetililtä ei poista heidän luomiaan käyttötunnisteita. Parasta suojauskäytäntöä varten on suositeltavaa mitätöidä konetililtä poistettujen henkilöiden luomat tunnukset." + "message": "Henkilöiden poistaminen konetililtä ei poista heidän luomiaan käyttötunnisteita. Parasta suojauskäytäntöä varten on suositeltavaa mitätöidä konetililtä poistettujen henkilöiden luomat tunnisteet." }, "smAccessRemovalWarningMaTitle": { "message": "Poista konetilin käyttöoikeus" @@ -7901,6 +7901,9 @@ "message": "Konetilin oikeuksia muutettiin" }, "unassignedItemsBanner": { - "message": "Huomautus: Organisaation kohteita, joita ei ole lisätty kokoelmaan, ei enää näytetä laitteiden \"Kaikki holvit\" -näkymissä, ja jatkossa ne näkyvät vain hallintakonsolin kautta. Lisää kohteet kokoelmiin, jotta ne ovat laitteilla käytettävissä." + "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/web/src/locales/fil/messages.json b/apps/web/src/locales/fil/messages.json index 7664321333..2cf9eb3b7e 100644 --- a/apps/web/src/locales/fil/messages.json +++ b/apps/web/src/locales/fil/messages.json @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now 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 your All Vaults view across devices 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/web/src/locales/fr/messages.json b/apps/web/src/locales/fr/messages.json index 57132fbe9a..27b529442c 100644 --- a/apps/web/src/locales/fr/messages.json +++ b/apps/web/src/locales/fr/messages.json @@ -7901,6 +7901,9 @@ "message": "Accès au compte machine mis à jour" }, "unassignedItemsBanner": { - "message": "Remarque : Les éléments d'organisation non assignés ne sont plus visibles dans la vue tous les coffres sur les appareils et ne sont maintenant accessibles que via la Console Admin. Assigner ces éléments à une collection de la Console Admin pour les rendre visibles." + "message": "Remarque : 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." + }, + "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/web/src/locales/gl/messages.json b/apps/web/src/locales/gl/messages.json index abda98207b..781c9000cb 100644 --- a/apps/web/src/locales/gl/messages.json +++ b/apps/web/src/locales/gl/messages.json @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now 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 your All Vaults view across devices 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/web/src/locales/he/messages.json b/apps/web/src/locales/he/messages.json index bcbea04c28..fd42338a28 100644 --- a/apps/web/src/locales/he/messages.json +++ b/apps/web/src/locales/he/messages.json @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now 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 your All Vaults view across devices 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/web/src/locales/hi/messages.json b/apps/web/src/locales/hi/messages.json index 892db00010..f0487066d6 100644 --- a/apps/web/src/locales/hi/messages.json +++ b/apps/web/src/locales/hi/messages.json @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now 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 your All Vaults view across devices 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/web/src/locales/hr/messages.json b/apps/web/src/locales/hr/messages.json index 0915a1a818..20a62053f0 100644 --- a/apps/web/src/locales/hr/messages.json +++ b/apps/web/src/locales/hr/messages.json @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now 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 your All Vaults view across devices 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/web/src/locales/hu/messages.json b/apps/web/src/locales/hu/messages.json index 16ed9d34d9..35781b814a 100644 --- a/apps/web/src/locales/hu/messages.json +++ b/apps/web/src/locales/hu/messages.json @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "Megjegyzés: A nem hozzá rendelt szervezeti elemek már nem láthatók az Összes széf nézetben a különböző eszközökön és mostantól csak a Felügyeleti konzolon keresztül érhetők el. Rendeljük ezeket az elemeket egy gyűjteményhez az Adminisztrátori konzolból, hogy láthatóvá tegyeük azokat." + }, + "unassignedItemsBannerSelfHost": { + "message": "Megjegyzés: A nem hozzá rendelt szervezeti elemek már nem láthatók az Összes széf nézetben a különböző eszközökön és mostantól csak a Felügyeleti konzolon keresztül érhetők el. Rendeljük ezeket az elemeket egy gyűjteményhez az Adminisztrátori konzolból, hogy láthatóvá tegyeük azokat." } } diff --git a/apps/web/src/locales/id/messages.json b/apps/web/src/locales/id/messages.json index ee0830e6ff..4cf235eb16 100644 --- a/apps/web/src/locales/id/messages.json +++ b/apps/web/src/locales/id/messages.json @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now 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 your All Vaults view across devices 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/web/src/locales/it/messages.json b/apps/web/src/locales/it/messages.json index 0ae1f9ae41..1cdee4420d 100644 --- a/apps/web/src/locales/it/messages.json +++ b/apps/web/src/locales/it/messages.json @@ -7902,5 +7902,8 @@ }, "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 tua 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/web/src/locales/ja/messages.json b/apps/web/src/locales/ja/messages.json index 2691faff33..3561c81bd1 100644 --- a/apps/web/src/locales/ja/messages.json +++ b/apps/web/src/locales/ja/messages.json @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "注意: 割り当てられていない組織項目は、デバイス間のすべての保管庫のビューでは表示されなくなり、管理コンソールからのみアクセスできます。 管理コンソールからコレクションにこれらのアイテムを割り当てると、表示するようにできます。" + }, + "unassignedItemsBannerSelfHost": { + "message": "お知らせ:2024年5月2日に、 割り当てられていない組織アイテムはデバイス間のすべての保管庫ビューに表示されなくなり、管理コンソールからのみアクセス可能になります。 管理コンソールからコレクションにこれらのアイテムを割り当てると、表示できるようになります。" } } diff --git a/apps/web/src/locales/ka/messages.json b/apps/web/src/locales/ka/messages.json index 25a97a05a3..d94a701410 100644 --- a/apps/web/src/locales/ka/messages.json +++ b/apps/web/src/locales/ka/messages.json @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now 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 your All Vaults view across devices 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/web/src/locales/km/messages.json b/apps/web/src/locales/km/messages.json index abda98207b..781c9000cb 100644 --- a/apps/web/src/locales/km/messages.json +++ b/apps/web/src/locales/km/messages.json @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now 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 your All Vaults view across devices 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/web/src/locales/kn/messages.json b/apps/web/src/locales/kn/messages.json index 1dbb90ad26..f2a5bdd829 100644 --- a/apps/web/src/locales/kn/messages.json +++ b/apps/web/src/locales/kn/messages.json @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now 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 your All Vaults view across devices 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/web/src/locales/ko/messages.json b/apps/web/src/locales/ko/messages.json index c01a591b63..b54268adbd 100644 --- a/apps/web/src/locales/ko/messages.json +++ b/apps/web/src/locales/ko/messages.json @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now 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 your All Vaults view across devices 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/web/src/locales/lv/messages.json b/apps/web/src/locales/lv/messages.json index d4cb0de727..d68a62bdcf 100644 --- a/apps/web/src/locales/lv/messages.json +++ b/apps/web/src/locales/lv/messages.json @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "Jāņem vērā: nepiešķirti apvienības vienumi vairs nav redzami skatā \"Visas glabātavas\" dažādās ierīcēs 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\" dažādās ierīcēs 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/web/src/locales/ml/messages.json b/apps/web/src/locales/ml/messages.json index d3b3bfd7f9..4f81532796 100644 --- a/apps/web/src/locales/ml/messages.json +++ b/apps/web/src/locales/ml/messages.json @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now 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 your All Vaults view across devices 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/web/src/locales/mr/messages.json b/apps/web/src/locales/mr/messages.json index abda98207b..781c9000cb 100644 --- a/apps/web/src/locales/mr/messages.json +++ b/apps/web/src/locales/mr/messages.json @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now 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 your All Vaults view across devices 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/web/src/locales/my/messages.json b/apps/web/src/locales/my/messages.json index abda98207b..781c9000cb 100644 --- a/apps/web/src/locales/my/messages.json +++ b/apps/web/src/locales/my/messages.json @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now 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 your All Vaults view across devices 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/web/src/locales/nb/messages.json b/apps/web/src/locales/nb/messages.json index 84c20f03c9..0719b129b3 100644 --- a/apps/web/src/locales/nb/messages.json +++ b/apps/web/src/locales/nb/messages.json @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now 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 your All Vaults view across devices 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/web/src/locales/ne/messages.json b/apps/web/src/locales/ne/messages.json index cbb84c245e..e0ca2b4f0a 100644 --- a/apps/web/src/locales/ne/messages.json +++ b/apps/web/src/locales/ne/messages.json @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now 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 your All Vaults view across devices 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/web/src/locales/nl/messages.json b/apps/web/src/locales/nl/messages.json index c0da91ee9a..3ab66e763b 100644 --- a/apps/web/src/locales/nl/messages.json +++ b/apps/web/src/locales/nl/messages.json @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "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/web/src/locales/nn/messages.json b/apps/web/src/locales/nn/messages.json index c75e8196f0..a80816b511 100644 --- a/apps/web/src/locales/nn/messages.json +++ b/apps/web/src/locales/nn/messages.json @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now 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 your All Vaults view across devices 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/web/src/locales/or/messages.json b/apps/web/src/locales/or/messages.json index abda98207b..781c9000cb 100644 --- a/apps/web/src/locales/or/messages.json +++ b/apps/web/src/locales/or/messages.json @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now 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 your All Vaults view across devices 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/web/src/locales/pl/messages.json b/apps/web/src/locales/pl/messages.json index 21fc97fe71..9532e8d25c 100644 --- a/apps/web/src/locales/pl/messages.json +++ b/apps/web/src/locales/pl/messages.json @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "Uwaga: Nieprzypisane elementy w organizacji nie są już widoczne w widoku Wszystkie sejfy na urządzeniach i są teraz 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 na urządzeniach 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/web/src/locales/pt_BR/messages.json b/apps/web/src/locales/pt_BR/messages.json index e0dff9d098..084d4c95d9 100644 --- a/apps/web/src/locales/pt_BR/messages.json +++ b/apps/web/src/locales/pt_BR/messages.json @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now 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 your All Vaults view across devices 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/web/src/locales/pt_PT/messages.json b/apps/web/src/locales/pt_PT/messages.json index 4bd18c27e5..cf1ad10d9b 100644 --- a/apps/web/src/locales/pt_PT/messages.json +++ b/apps/web/src/locales/pt_PT/messages.json @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "Aviso: Os itens da organização não atribuídos já não são visíveis na sua vista Todos os cofres em todos os dispositivos e agora só estã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 estar visíveis na vista Todos os cofres em todos os dispositivos 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/web/src/locales/ro/messages.json b/apps/web/src/locales/ro/messages.json index f0624013b0..ba5badd908 100644 --- a/apps/web/src/locales/ro/messages.json +++ b/apps/web/src/locales/ro/messages.json @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now 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 your All Vaults view across devices 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/web/src/locales/ru/messages.json b/apps/web/src/locales/ru/messages.json index c09830eb0c..eeb89acf55 100644 --- a/apps/web/src/locales/ru/messages.json +++ b/apps/web/src/locales/ru/messages.json @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "Обратите внимание: неприсвоенные элементы организации больше не видны в представлении \"Все хранилища\" на всех устройствах и теперь доступны только через консоль администратора. Назначьте эти элементы коллекции в консоли администратора, чтобы сделать их видимыми." + }, + "unassignedItemsBannerSelfHost": { + "message": "Уведомление: 2 мая 2024 года неприсвоенные элементы организации больше не будут отображаться в представлении \"Все хранилища\" на всех устройствах и будут доступны только через консоль администратора. Назначьте эти элементы коллекции в консоли администратора, чтобы сделать их видимыми." } } diff --git a/apps/web/src/locales/si/messages.json b/apps/web/src/locales/si/messages.json index 536c05b2e4..0faa92c08d 100644 --- a/apps/web/src/locales/si/messages.json +++ b/apps/web/src/locales/si/messages.json @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now 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 your All Vaults view across devices 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/web/src/locales/sk/messages.json b/apps/web/src/locales/sk/messages.json index 87ecffe11f..67f5abdc7d 100644 --- a/apps/web/src/locales/sk/messages.json +++ b/apps/web/src/locales/sk/messages.json @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now 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 your All Vaults view across devices 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/web/src/locales/sl/messages.json b/apps/web/src/locales/sl/messages.json index e8d8306b40..6a55868df2 100644 --- a/apps/web/src/locales/sl/messages.json +++ b/apps/web/src/locales/sl/messages.json @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now 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 your All Vaults view across devices 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/web/src/locales/sr/messages.json b/apps/web/src/locales/sr/messages.json index b70a61b789..9683c587b7 100644 --- a/apps/web/src/locales/sr/messages.json +++ b/apps/web/src/locales/sr/messages.json @@ -7502,7 +7502,7 @@ "message": "Приступ колекцији је ограничен" }, "readOnlyCollectionAccess": { - "message": "You do not have access to manage this collection." + "message": "Немате приступ за управљање овом колекцијом." }, "grantCollectionAccess": { "message": "Одобрите групама или члановима приступ овој колекцији." @@ -7607,7 +7607,7 @@ "message": "Портал провајдера" }, "viewCollection": { - "message": "View collection" + "message": "Преглед колекције" }, "restrictedGroupAccess": { "message": "Не можете да се додате у групе." @@ -7901,6 +7901,9 @@ "message": "Приступ налога машине ажуриран" }, "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "message": "Напомена: Недодељене ставке организације више нису видљиве у вашем приказу Сви сефови на свим уређајима и сада су доступне само преко Админ конзоле. Доделите ове ставке колекцији са Админ конзолом да бисте их учинили видљивим." + }, + "unassignedItemsBannerSelfHost": { + "message": "Обавештење: 2. маја 2024. недодељене ставке организације више неће бити видљиве у вашем приказу Сви сефови на свим уређајима и биће им доступне само преко Админ конзоле. Доделите ове ставке колекцији са Админ конзолом да бисте их учинили видљивим." } } diff --git a/apps/web/src/locales/sr_CS/messages.json b/apps/web/src/locales/sr_CS/messages.json index 5424fc8fb4..15770437ce 100644 --- a/apps/web/src/locales/sr_CS/messages.json +++ b/apps/web/src/locales/sr_CS/messages.json @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now 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 your All Vaults view across devices 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/web/src/locales/sv/messages.json b/apps/web/src/locales/sv/messages.json index ba6f66aaaa..57321d3376 100644 --- a/apps/web/src/locales/sv/messages.json +++ b/apps/web/src/locales/sv/messages.json @@ -579,7 +579,7 @@ "message": "Åtkomst" }, "accessLevel": { - "message": "Access level" + "message": "Åtkomstnivå" }, "loggedOut": { "message": "Utloggad" @@ -1060,7 +1060,7 @@ "message": "Denna export innehåller ditt valv i ett okrypterat format. Du bör inte lagra eller skicka den exporterade filen över osäkra kanaler (t.ex. e-post). Radera den omedelbart när du är färdig med den." }, "exportSecretsWarningDesc": { - "message": "This export contains your secrets data in an unencrypted format. You should not store or send the exported file over unsecure channels (such as email). Delete it immediately after you are done using it." + "message": "Denna export innehåller ditt valv i ett okrypterat format. Du bör inte lagra eller skicka den exporterade filen över osäkra kanaler (t.ex. e-post). Radera den omedelbart när du är färdig med den. " }, "encExportKeyWarningDesc": { "message": "Denna export krypterar dina data med kontots krypteringsnyckel. Om du någonsin roterar kontots krypteringsnyckel bör du exportera igen eftersom du inte kommer att kunna dekryptera denna exportfil." @@ -1151,7 +1151,7 @@ "message": "Längd" }, "passwordMinLength": { - "message": "Minimum password length" + "message": "Minsta tillåtna lösenordslängd" }, "uppercase": { "message": "Versaler (A-Ö)", @@ -1359,7 +1359,7 @@ "description": "This will be part of a larger sentence, that will read like this: If you don't have any data to import, you can create a new item instead. (Optional second half: You may need to wait until your administrator confirms your organization membership.)" }, "onboardingImportDataDetailsPartTwoNoOrgs": { - "message": " instead.", + "message": " istället.", "description": "This will be part of a larger sentence, that will read like this: If you don't have any data to import, you can create a new item instead." }, "onboardingImportDataDetailsPartTwoWithOrgs": { @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now 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 your All Vaults view across devices 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/web/src/locales/te/messages.json b/apps/web/src/locales/te/messages.json index abda98207b..781c9000cb 100644 --- a/apps/web/src/locales/te/messages.json +++ b/apps/web/src/locales/te/messages.json @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now 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 your All Vaults view across devices 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/web/src/locales/th/messages.json b/apps/web/src/locales/th/messages.json index bb8288380a..1ecd45a9e4 100644 --- a/apps/web/src/locales/th/messages.json +++ b/apps/web/src/locales/th/messages.json @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now 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 your All Vaults view across devices 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/web/src/locales/tr/messages.json b/apps/web/src/locales/tr/messages.json index 0856068063..65ef93eef7 100644 --- a/apps/web/src/locales/tr/messages.json +++ b/apps/web/src/locales/tr/messages.json @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now 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 your All Vaults view across devices 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/web/src/locales/uk/messages.json b/apps/web/src/locales/uk/messages.json index 5d8b803550..1baaa5c67b 100644 --- a/apps/web/src/locales/uk/messages.json +++ b/apps/web/src/locales/uk/messages.json @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "Увага: непризначені елементи організації більше не видимі у поданні \"Усі сховища\" на різних пристроях і тепер доступні лише в консолі адміністратора. Щоб зробити їх видимими, призначте ці елементи збірці в консолі адміністратора." + }, + "unassignedItemsBannerSelfHost": { + "message": "Сповіщення: 2 травня 2024 року, непризначені елементи організації більше не будуть видимі на ваших пристроях у поданні \"Усі сховища\", і будуть доступні лише через консоль адміністратора. Щоб зробити їх видимими, призначте ці елементи збірці в консолі адміністратора." } } diff --git a/apps/web/src/locales/vi/messages.json b/apps/web/src/locales/vi/messages.json index 777af538dc..ffbd72ce9d 100644 --- a/apps/web/src/locales/vi/messages.json +++ b/apps/web/src/locales/vi/messages.json @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now 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 your All Vaults view across devices 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/web/src/locales/zh_CN/messages.json b/apps/web/src/locales/zh_CN/messages.json index c197b40f3f..e584785a5a 100644 --- a/apps/web/src/locales/zh_CN/messages.json +++ b/apps/web/src/locales/zh_CN/messages.json @@ -167,7 +167,7 @@ "message": "移除" }, "unassigned": { - "message": "未分派" + "message": "未分配" }, "noneFolder": { "message": "无文件夹", @@ -7521,7 +7521,7 @@ "description": "Label indicating the most common import formats" }, "maintainYourSubscription": { - "message": "要维护 $ORG$ 的订阅,", + "message": "要保留 $ORG$ 的订阅,", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To maintain your subscription for $ORG$, add a payment method.'", "placeholders": { "org": { @@ -7653,7 +7653,7 @@ "message": "项目" }, "assignedSeats": { - "message": "分配的座席" + "message": "已分配的席位" }, "assigned": { "message": "已分配" @@ -7668,29 +7668,29 @@ "message": "脱离组织" }, "manageSeats": { - "message": "管理坐席" + "message": "管理席位" }, "manageSeatsDescription": { "message": "席位的调整将反映在下一个计费周期中。" }, "unassignedSeatsDescription": { - "message": "未分配的订阅座席" + "message": "未分配的订阅席位" }, "purchaseSeatDescription": { - "message": "已购买额外座席" + "message": "已购买附加席位" }, "assignedSeatCannotUpdate": { - "message": "无法更新已分配的坐席。请联系您的组织所有者群求协助。" + "message": "无法更新已分配的席位。请联系您的组织所有者获取协助。" }, "subscriptionUpdateFailed": { "message": "订阅更新失败" }, "trial": { - "message": "Trial", + "message": "试用", "description": "A subscription status label." }, "pastDue": { - "message": "Past due", + "message": "逾期未支付", "description": "A subscription status label" }, "subscriptionExpired": { @@ -7698,7 +7698,7 @@ "description": "The date header used when a subscription is past due." }, "pastDueWarningForChargeAutomatically": { - "message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "message": "从您的订阅到期之日起,您有 $DAYS$ 天的宽限期来保留您的订购。请在 $SUSPENSION_DATE$ 之前处理逾期未支付的账单。", "placeholders": { "days": { "content": "$1", @@ -7712,7 +7712,7 @@ "description": "A warning shown to the user when their subscription is past due and they are charged automatically." }, "pastDueWarningForSendInvoice": { - "message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "message": "从第一笔未支付的账单到期之日起,您有 $DAYS$ 天的宽限期来保留您的订购。请在 $SUSPENSION_DATE$ 之前处理逾期未支付的账单。", "placeholders": { "days": { "content": "$1", @@ -7730,7 +7730,7 @@ "description": "The header of a warning box shown to a user whose subscription is unpaid." }, "toReactivateYourSubscription": { - "message": "To reactivate your subscription, please resolve the past due invoices.", + "message": "要重新激活您的订阅,请先处理逾期未支付的账单。", "description": "The body of a warning box shown to a user whose subscription is unpaid." }, "cancellationDate": { @@ -7738,42 +7738,42 @@ "description": "The date header used when a subscription is cancelled." }, "machineAccountsCannotCreate": { - "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + "message": "无法在已停用的组织中创建机器账户。请联系您的组织所有者获取协助。" }, "machineAccount": { - "message": "Machine account", + "message": "机器账户", "description": "A machine user which can be used to automate processes and access secrets in the system." }, "machineAccounts": { - "message": "Machine accounts", + "message": "机器账户", "description": "The title for the section that deals with machine accounts." }, "newMachineAccount": { - "message": "New machine account", + "message": "新增机器账户", "description": "Title for creating a new machine account." }, "machineAccountsNoItemsMessage": { - "message": "Create a new machine account to get started automating secret access.", + "message": "创建一个新的机器账户以开始使用自动机密访问。", "description": "Message to encourage the user to start creating machine accounts." }, "machineAccountsNoItemsTitle": { - "message": "Nothing to show yet", + "message": "暂无要显示的内容", "description": "Title to indicate that there are no machine accounts to display." }, "deleteMachineAccounts": { - "message": "Delete machine accounts", + "message": "删除机器账户", "description": "Title for the action to delete one or multiple machine accounts." }, "deleteMachineAccount": { - "message": "Delete machine account", + "message": "删除机器账户", "description": "Title for the action to delete a single machine account." }, "viewMachineAccount": { - "message": "View machine account", + "message": "查看机器账户", "description": "Action to view the details of a machine account." }, "deleteMachineAccountDialogMessage": { - "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "message": "删除机器账户 $MACHINE_ACCOUNT$ 是永久性操作,无法撤销!", "placeholders": { "machine_account": { "content": "$1", @@ -7782,10 +7782,10 @@ } }, "deleteMachineAccountsDialogMessage": { - "message": "Deleting machine accounts is permanent and irreversible." + "message": "删除机器账户是永久性操作,无法撤销!" }, "deleteMachineAccountsConfirmMessage": { - "message": "Delete $COUNT$ machine accounts", + "message": "删除 $COUNT$ 个机器账户", "placeholders": { "count": { "content": "$1", @@ -7794,60 +7794,60 @@ } }, "deleteMachineAccountToast": { - "message": "Machine account deleted" + "message": "机器账户已删除" }, "deleteMachineAccountsToast": { - "message": "Machine accounts deleted" + "message": "机器账户已删除" }, "searchMachineAccounts": { - "message": "Search machine accounts", + "message": "搜索机器账户", "description": "Placeholder text for searching machine accounts." }, "editMachineAccount": { - "message": "Edit machine account", + "message": "编辑机器账户", "description": "Title for editing a machine account." }, "machineAccountName": { - "message": "Machine account name", + "message": "机器账户名称", "description": "Label for the name of a machine account" }, "machineAccountCreated": { - "message": "Machine account created", + "message": "机器账户已创建", "description": "Notifies that a new machine account has been created" }, "machineAccountUpdated": { - "message": "Machine account updated", + "message": "机器账户已更新", "description": "Notifies that a machine account has been updated" }, "projectMachineAccountsDescription": { - "message": "Grant machine accounts access to this project." + "message": "授予机器账户对此工程的访问权限。" }, "projectMachineAccountsSelectHint": { - "message": "Type or select machine accounts" + "message": "输入或选择机器账户" }, "projectEmptyMachineAccountAccessPolicies": { - "message": "Add machine accounts to grant access" + "message": "添加机器账户以授予访问权限" }, "machineAccountPeopleDescription": { - "message": "Grant groups or people access to this machine account." + "message": "授予群组或人员对此机器账户的访问权限。" }, "machineAccountProjectsDescription": { - "message": "Assign projects to this machine account. " + "message": "分配工程到此机器账户。 " }, "createMachineAccount": { - "message": "Create a machine account" + "message": "创建机器账户" }, "maPeopleWarningMessage": { - "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + "message": "从机器账户中移除人员并不会移除他们已创建的访问令牌。基于安全方面的最佳做法,建议吊销从机器账户中被移除的人员创建的访问令牌。" }, "smAccessRemovalWarningMaTitle": { - "message": "Remove access to this machine account" + "message": "移除此机器账户的访问权限" }, "smAccessRemovalWarningMaMessage": { - "message": "This action will remove your access to the machine account." + "message": "此操作将移除您对此机器账户的访问权限。" }, "machineAccountsIncluded": { - "message": "$COUNT$ machine accounts included", + "message": "包含 $COUNT$ 个机器账户", "placeholders": { "count": { "content": "$1", @@ -7856,7 +7856,7 @@ } }, "additionalMachineAccountCost": { - "message": "$COST$ per month for additional machine accounts", + "message": "附加机器账户 $COST$ 每月", "placeholders": { "cost": { "content": "$1", @@ -7865,10 +7865,10 @@ } }, "additionalMachineAccounts": { - "message": "Additional machine accounts" + "message": "附加机器账户" }, "includedMachineAccounts": { - "message": "Your plan comes with $COUNT$ machine accounts.", + "message": "您的计划包含 $COUNT$ 个机器账户。", "placeholders": { "count": { "content": "$1", @@ -7877,7 +7877,7 @@ } }, "addAdditionalMachineAccounts": { - "message": "You can add additional machine accounts for $COST$ per month.", + "message": "您也可以以 $COST$ 每月购买附加机器账户。", "placeholders": { "cost": { "content": "$1", @@ -7886,21 +7886,24 @@ } }, "limitMachineAccounts": { - "message": "Limit machine accounts (optional)" + "message": "限制机器账户(可选)" }, "limitMachineAccountsDesc": { - "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + "message": "为您的机器账户设置一个限制。达到此限制后,您将无法创建新的机器账户。" }, "machineAccountLimit": { - "message": "Machine account limit (optional)" + "message": "机器账户限制(可选)" }, "maxMachineAccountCost": { - "message": "Max potential machine account cost" + "message": "最大潜在的机器账户费用" }, "machineAccountAccessUpdated": { - "message": "Machine account access updated" + "message": "机器账户访问权限已更新" }, "unassignedItemsBanner": { "message": "注意:未分配的组织项目在您所有设备的「所有密码库」视图中不再可见,现在只能通过管理控制台访问。通过管理控制台将这些项目分配给集合以使其可见。" + }, + "unassignedItemsBannerSelfHost": { + "message": "注意:从 2024 年 5 月 2 日起,未分配的组织项目在您所有设备的「所有密码库」视图中将不再可见,只能通过管理控制台访问。通过管理控制台将这些项目分配给集合以使其可见。" } } diff --git a/apps/web/src/locales/zh_TW/messages.json b/apps/web/src/locales/zh_TW/messages.json index 6f512741f2..89bb26cacd 100644 --- a/apps/web/src/locales/zh_TW/messages.json +++ b/apps/web/src/locales/zh_TW/messages.json @@ -7902,5 +7902,8 @@ }, "unassignedItemsBanner": { "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now 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 your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } From c73b86be124a8c0605d344ee63da2f5f48a5dfb8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 15 Apr 2024 17:55:50 +0000 Subject: [PATCH 186/351] Autosync the updated translations (#8739) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/desktop/src/locales/de/messages.json | 4 ++-- apps/desktop/src/locales/fi/messages.json | 2 +- apps/desktop/src/locales/sv/messages.json | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/locales/de/messages.json b/apps/desktop/src/locales/de/messages.json index a07ad93b15..428cfd6a27 100644 --- a/apps/desktop/src/locales/de/messages.json +++ b/apps/desktop/src/locales/de/messages.json @@ -2698,9 +2698,9 @@ "message": "Hardwarebeschleunigung aktivieren und neu starten" }, "removePasskey": { - "message": "Passkey löschen" + "message": "Passkey entfernen" }, "passkeyRemoved": { - "message": "Passkey gelöscht" + "message": "Passkey entfernt" } } diff --git a/apps/desktop/src/locales/fi/messages.json b/apps/desktop/src/locales/fi/messages.json index 206588f3c3..f74136aedc 100644 --- a/apps/desktop/src/locales/fi/messages.json +++ b/apps/desktop/src/locales/fi/messages.json @@ -2689,7 +2689,7 @@ "description": "Label indicating the most common import formats" }, "troubleshooting": { - "message": "Vianselvitys" + "message": "Vianetsintä" }, "disableHardwareAccelerationRestart": { "message": "Poista laitteistokiihdytys käytöstä ja käynnistä sovellus uudelleen" diff --git a/apps/desktop/src/locales/sv/messages.json b/apps/desktop/src/locales/sv/messages.json index 4c41cee471..c07f7efef3 100644 --- a/apps/desktop/src/locales/sv/messages.json +++ b/apps/desktop/src/locales/sv/messages.json @@ -2689,7 +2689,7 @@ "description": "Label indicating the most common import formats" }, "troubleshooting": { - "message": "Troubleshooting" + "message": "Felsökning" }, "disableHardwareAccelerationRestart": { "message": "Disable hardware acceleration and restart" @@ -2698,9 +2698,9 @@ "message": "Enable hardware acceleration and restart" }, "removePasskey": { - "message": "Remove passkey" + "message": "Ta bort nyckel" }, "passkeyRemoved": { - "message": "Passkey removed" + "message": "Nyckel borttagen" } } From 07652408862d0a4011f6526768be287f25df60d3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 15 Apr 2024 17:56:11 +0000 Subject: [PATCH 187/351] Autosync the updated translations (#8740) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/browser/src/_locales/ar/messages.json | 3 +++ apps/browser/src/_locales/az/messages.json | 5 ++++- apps/browser/src/_locales/be/messages.json | 3 +++ apps/browser/src/_locales/bg/messages.json | 5 ++++- apps/browser/src/_locales/bn/messages.json | 3 +++ apps/browser/src/_locales/bs/messages.json | 3 +++ apps/browser/src/_locales/ca/messages.json | 5 ++++- apps/browser/src/_locales/cs/messages.json | 3 +++ apps/browser/src/_locales/cy/messages.json | 3 +++ apps/browser/src/_locales/da/messages.json | 3 +++ apps/browser/src/_locales/de/messages.json | 7 +++++-- apps/browser/src/_locales/el/messages.json | 3 +++ apps/browser/src/_locales/en_GB/messages.json | 3 +++ apps/browser/src/_locales/en_IN/messages.json | 3 +++ apps/browser/src/_locales/es/messages.json | 3 +++ apps/browser/src/_locales/et/messages.json | 7 +++++-- apps/browser/src/_locales/eu/messages.json | 3 +++ apps/browser/src/_locales/fa/messages.json | 3 +++ apps/browser/src/_locales/fi/messages.json | 5 ++++- apps/browser/src/_locales/fil/messages.json | 3 +++ apps/browser/src/_locales/fr/messages.json | 3 +++ apps/browser/src/_locales/gl/messages.json | 3 +++ apps/browser/src/_locales/he/messages.json | 3 +++ apps/browser/src/_locales/hi/messages.json | 3 +++ apps/browser/src/_locales/hr/messages.json | 3 +++ apps/browser/src/_locales/hu/messages.json | 3 +++ apps/browser/src/_locales/id/messages.json | 3 +++ apps/browser/src/_locales/it/messages.json | 3 +++ apps/browser/src/_locales/ja/messages.json | 3 +++ apps/browser/src/_locales/ka/messages.json | 3 +++ apps/browser/src/_locales/km/messages.json | 3 +++ apps/browser/src/_locales/kn/messages.json | 3 +++ apps/browser/src/_locales/ko/messages.json | 3 +++ apps/browser/src/_locales/lt/messages.json | 3 +++ apps/browser/src/_locales/lv/messages.json | 3 +++ apps/browser/src/_locales/ml/messages.json | 3 +++ apps/browser/src/_locales/mr/messages.json | 3 +++ apps/browser/src/_locales/my/messages.json | 3 +++ apps/browser/src/_locales/nb/messages.json | 3 +++ apps/browser/src/_locales/ne/messages.json | 3 +++ apps/browser/src/_locales/nl/messages.json | 3 +++ apps/browser/src/_locales/nn/messages.json | 3 +++ apps/browser/src/_locales/or/messages.json | 3 +++ apps/browser/src/_locales/pl/messages.json | 3 +++ apps/browser/src/_locales/pt_BR/messages.json | 3 +++ apps/browser/src/_locales/pt_PT/messages.json | 3 +++ apps/browser/src/_locales/ro/messages.json | 3 +++ apps/browser/src/_locales/ru/messages.json | 3 +++ apps/browser/src/_locales/si/messages.json | 3 +++ apps/browser/src/_locales/sk/messages.json | 3 +++ apps/browser/src/_locales/sl/messages.json | 3 +++ apps/browser/src/_locales/sr/messages.json | 5 ++++- apps/browser/src/_locales/sv/messages.json | 5 ++++- apps/browser/src/_locales/te/messages.json | 3 +++ apps/browser/src/_locales/th/messages.json | 3 +++ apps/browser/src/_locales/tr/messages.json | 3 +++ apps/browser/src/_locales/uk/messages.json | 3 +++ apps/browser/src/_locales/vi/messages.json | 3 +++ apps/browser/src/_locales/zh_CN/messages.json | 3 +++ apps/browser/src/_locales/zh_TW/messages.json | 3 +++ 60 files changed, 190 insertions(+), 10 deletions(-) diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index 7b17b114d6..1f7c5bbe98 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -3008,5 +3008,8 @@ }, "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 e12fb0dc88..2111ea6704 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -3007,6 +3007,9 @@ "message": "Parol silindi" }, "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." + "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 26ecf49cc7..08cb351abb 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -3008,5 +3008,8 @@ }, "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 2ddaf647d8..87dfc8d3be 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -3007,6 +3007,9 @@ "message": "Секретният ключ е премахнат" }, "unassignedItemsBanner": { - "message": "Забележка: неразпределените елементи на организацията вече не се виждат в изгледа с „Всички трезори“, а са достъпни само през Административната конзола. Добавете тези елементи към някоя колекция в Административната конзола, за да станат видими." + "message": "Известие: неразпределените елементи на организацията вече не се виждат в изгледа с „Всички трезори“, а са достъпни само през Административната конзола. Добавете тези елементи към някоя колекция в Административната конзола, за да станат видими." + }, + "unassignedItemsBannerSelfHost": { + "message": "Известие: от 2 май 2024г. неразпределените елементи на организациите вече няма се виждат в изгледа с „Всички трезори“, а ще бъдат достъпни само през Административната конзола. Добавете тези елементи към някоя колекция в Административната конзола, за да станат видими." } } diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index 4bdf811b3b..1bdaeef7c6 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -3008,5 +3008,8 @@ }, "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 a7f157011e..92a667afeb 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -3008,5 +3008,8 @@ }, "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 147a64233c..fc67602b60 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -3007,6 +3007,9 @@ "message": "Clau de pas suprimida" }, "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." + "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 2b6a8d4f0b..d989d25bf2 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -3008,5 +3008,8 @@ }, "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 79867bf7bf..79178bc9d5 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -3008,5 +3008,8 @@ }, "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 26e08741b2..d808d97412 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -3008,5 +3008,8 @@ }, "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 b542fbfad7..deb92e992d 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -3001,12 +3001,15 @@ "description": "Notification message for when saving credentials has failed." }, "removePasskey": { - "message": "Passkey löschen" + "message": "Passkey entfernen" }, "passkeyRemoved": { - "message": "Passkey gelöscht" + "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 7bbc615dae..36b14e447f 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -3008,5 +3008,8 @@ }, "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_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index 1c24106cdc..1ac55feb42 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -3008,5 +3008,8 @@ }, "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 f1a4979766..cbe214f0b3 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -3008,5 +3008,8 @@ }, "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 31200031f6..ee5666f3cc 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -3008,5 +3008,8 @@ }, "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 1f98d15758..ea1758468e 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -3001,12 +3001,15 @@ "description": "Notification message for when saving credentials has failed." }, "removePasskey": { - "message": "Remove passkey" + "message": "Eemalda pääsuvõti" }, "passkeyRemoved": { - "message": "Passkey removed" + "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 49086462d9..529a1e8127 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -3008,5 +3008,8 @@ }, "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 0c44026cec..669eb151f4 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -3008,5 +3008,8 @@ }, "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 ead3d43236..17aea532ba 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -3007,6 +3007,9 @@ "message": "Suojausavain poistettiin" }, "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." + "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 e9bc8e2c75..42d5060e28 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -3008,5 +3008,8 @@ }, "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 452a2b363b..6cced1cb0d 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -3008,5 +3008,8 @@ }, "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 dc91c1e6a9..023e03b834 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -3008,5 +3008,8 @@ }, "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 7a05907228..1e633f5eb9 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -3008,5 +3008,8 @@ }, "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 99daecc5f8..44f645bc47 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -3008,5 +3008,8 @@ }, "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 ec4509dbd4..e74a72bc4f 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -3008,5 +3008,8 @@ }, "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 5c10efc3ae..cadd72a475 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -3008,5 +3008,8 @@ }, "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 810d603047..9907a7520c 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -3008,5 +3008,8 @@ }, "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 86a6bf054e..65a5a1ad04 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -3008,5 +3008,8 @@ }, "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 eb78a7205f..05fb7fe5de 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -3008,5 +3008,8 @@ }, "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 66dae59f4c..d67b88ba9c 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -3008,5 +3008,8 @@ }, "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 dc91c1e6a9..023e03b834 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -3008,5 +3008,8 @@ }, "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 68fe45999d..61cfadc762 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -3008,5 +3008,8 @@ }, "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 689e618929..c71fbdf7a8 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -3008,5 +3008,8 @@ }, "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 c99d128915..0fc146c250 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -3008,5 +3008,8 @@ }, "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 c36315d10d..efac417556 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -3008,5 +3008,8 @@ }, "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 7205a54559..9b66e6f0d6 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -3008,5 +3008,8 @@ }, "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 918557feb8..f9f37b2511 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -3008,5 +3008,8 @@ }, "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 dc91c1e6a9..023e03b834 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -3008,5 +3008,8 @@ }, "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 d77aaa7d7c..82d847ff0f 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -3008,5 +3008,8 @@ }, "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 dc91c1e6a9..023e03b834 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -3008,5 +3008,8 @@ }, "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 bbf16b345a..13d59c4546 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -3008,5 +3008,8 @@ }, "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 dc91c1e6a9..023e03b834 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -3008,5 +3008,8 @@ }, "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 dc91c1e6a9..023e03b834 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -3008,5 +3008,8 @@ }, "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 98130de3be..e4b97ec956 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -3008,5 +3008,8 @@ }, "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 6126679902..a4e0688e3d 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -3008,5 +3008,8 @@ }, "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 a904867d5f..c35531e445 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -3008,5 +3008,8 @@ }, "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 3e554f4d68..885d70ca93 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -3008,5 +3008,8 @@ }, "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 aed3d58b11..69d9ca200f 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -3008,5 +3008,8 @@ }, "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 3dc18a97fb..fb026226bb 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -3008,5 +3008,8 @@ }, "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 bd4f9f9796..a7948d78f3 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -3008,5 +3008,8 @@ }, "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 3e2690be30..2fac491c9c 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -3008,5 +3008,8 @@ }, "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 fef1a4eb8a..6ec1b6181b 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -3007,6 +3007,9 @@ "message": "Приступачни кључ је уклоњен" }, "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." + "message": "Напомена: Недодељене ставке организације више нису видљиве у приказу Сви сефови и доступне су само преко Админ конзоле. Доделите ове ставке колекцији са Админ конзолом да бисте их учинили видљивим." + }, + "unassignedItemsBannerSelfHost": { + "message": "Обавештење: 2. маја 2024. недодељене ставке организације више неће бити видљиве у приказу Сви сефови и биће доступне само преко Админ конзоле. Доделите ове ставке колекцији са Админ конзолом да бисте их учинили видљивим." } } diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index 0a27862c74..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" @@ -3008,5 +3008,8 @@ }, "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 dc91c1e6a9..023e03b834 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -3008,5 +3008,8 @@ }, "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 ecc346513d..827ca72854 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -3008,5 +3008,8 @@ }, "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 f6c00a58b8..cd0e12e6b0 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -3008,5 +3008,8 @@ }, "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 873cfad2c6..4820860de2 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -3008,5 +3008,8 @@ }, "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 d71fa3322f..234e60e756 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -3008,5 +3008,8 @@ }, "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 85fb5f6d2e..519313df81 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -3008,5 +3008,8 @@ }, "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 66d9f7ce62..b6f1ff574a 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -3008,5 +3008,8 @@ }, "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." } } From 62ed7e5abce8ce4ac031b525b9f1133daf5c58c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Gon=C3=A7alves?= <cgoncalves@bitwarden.com> Date: Tue, 16 Apr 2024 15:47:12 +0100 Subject: [PATCH 188/351] [PM-2170] Update collections component (#6794) * PM-2170 Updated Collections to use Component Library * PM-2170 Removed some extra space * PM-2170 Fix typo * PM-2170 Refresh vault when saving * PM-2170 Fix PR comments * PM-2170 Refactor to use CollectionsDialogResult to fix lint error * PM-2170 Refactor subtitle * PM-4788 Fix dismiss of modal * PM-2170 Fix PR comments --- .../collections.component.html | 110 ++++++++---------- .../individual-vault/collections.component.ts | 43 ++++++- .../vault/individual-vault/vault.component.ts | 14 +-- .../vault/org-vault/collections.component.ts | 40 ++++++- .../app/vault/org-vault/vault.component.ts | 47 +++++--- .../components/collections.component.ts | 6 +- 6 files changed, 165 insertions(+), 95 deletions(-) diff --git a/apps/web/src/app/vault/individual-vault/collections.component.html b/apps/web/src/app/vault/individual-vault/collections.component.html index 46bd94b316..5adf9c4e58 100644 --- a/apps/web/src/app/vault/individual-vault/collections.component.html +++ b/apps/web/src/app/vault/individual-vault/collections.component.html @@ -1,64 +1,52 @@ -<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="collectionsTitle"> - <div class="modal-dialog modal-dialog-scrollable" role="document"> - <form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise"> - <div class="modal-header"> - <h1 class="modal-title" id="collectionsTitle"> - {{ "collections" | i18n }} - <small *ngIf="cipher">{{ cipher.name }}</small> - </h1> - <button - type="button" - class="close" - data-dismiss="modal" - appA11yTitle="{{ 'close' | i18n }}" - > - <span aria-hidden="true">&times;</span> - </button> - </div> - <div class="modal-body"> - <p>{{ "collectionsDesc" | i18n }}</p> - <div class="d-flex"> - <h3>{{ "collections" | i18n }}</h3> - <div class="ml-auto d-flex" *ngIf="collections && collections.length"> - <button type="button" (click)="selectAll(true)" class="btn btn-link btn-sm py-0"> - {{ "selectAll" | i18n }} - </button> - <button type="button" (click)="selectAll(false)" class="btn btn-link btn-sm py-0"> - {{ "unselectAll" | i18n }} - </button> - </div> +<form (ngSubmit)="submit()"> + <bit-dialog> + <span bitDialogTitle> + {{ "collections" | i18n }} + <small *ngIf="cipher">{{ cipher.name }}</small> + </span> + <ng-container bitDialogContent> + <p>{{ "collectionsDesc" | i18n }}</p> + <div class="tw-flex"> + <label class="tw-mb-1 tw-block tw-font-semibold tw-text-main">{{ + "collections" | i18n + }}</label> + <div class="tw-ml-auto tw-flex" *ngIf="collections && collections.length"> + <button bitLink type="button" (click)="selectAll(true)" class="tw-px-2"> + {{ "selectAll" | i18n }} + </button> + <button bitLink type="button" (click)="selectAll(false)" class="tw-px-2"> + {{ "unselectAll" | i18n }} + </button> </div> - <div *ngIf="!collections || !collections.length"> - {{ "noCollectionsInList" | i18n }} - </div> - <table class="table table-hover table-list mb-0" *ngIf="collections && collections.length"> - <tbody> - <tr *ngFor="let c of collections; let i = index" (click)="check(c)"> - <td class="table-list-checkbox"> - <input - type="checkbox" - [(ngModel)]="$any(c).checked" - name="Collection[{{ i }}].Checked" - appStopProp - [disabled]="!c.canEditItems(this.organization, this.flexibleCollectionsV1Enabled)" - /> - </td> - <td> - {{ c.name }} - </td> - </tr> - </tbody> - </table> </div> - <div class="modal-footer"> - <button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading"> - <i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i> - <span>{{ "save" | i18n }}</span> - </button> - <button type="button" class="btn btn-outline-secondary" data-dismiss="modal"> - {{ "cancel" | i18n }} - </button> + <div *ngIf="!collections || !collections.length"> + {{ "noCollectionsInList" | i18n }} </div> - </form> - </div> -</div> + <bit-table *ngIf="collections && collections.length"> + <ng-template body> + <tr bitRow *ngFor="let c of collections; let i = index" (click)="check(c)"> + <td bitCell> + <input + type="checkbox" + bitCheckbox + [(ngModel)]="$any(c).checked" + name="Collection[{{ i }}].Checked" + appStopProp + [disabled]="!c.canEditItems(this.organization, this.flexibleCollectionsV1Enabled)" + /> + {{ c.name }} + </td> + </tr> + </ng-template> + </bit-table> + </ng-container> + <ng-container bitDialogFooter> + <button bitButton buttonType="primary" type="submit"> + {{ "save" | i18n }} + </button> + <button bitButton bitDialogClose buttonType="secondary" type="button"> + {{ "cancel" | i18n }} + </button> + </ng-container> + </bit-dialog> +</form> diff --git a/apps/web/src/app/vault/individual-vault/collections.component.ts b/apps/web/src/app/vault/individual-vault/collections.component.ts index 6cf0901f33..6add775b4a 100644 --- a/apps/web/src/app/vault/individual-vault/collections.component.ts +++ b/apps/web/src/app/vault/individual-vault/collections.component.ts @@ -1,4 +1,5 @@ -import { Component, OnDestroy } from "@angular/core"; +import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; +import { Component, OnDestroy, Inject } from "@angular/core"; import { CollectionsComponent as BaseCollectionsComponent } from "@bitwarden/angular/admin-console/components/collections.component"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; @@ -8,6 +9,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; +import { DialogService } from "@bitwarden/components"; @Component({ selector: "app-vault-collections", @@ -21,6 +23,8 @@ export class CollectionsComponent extends BaseCollectionsComponent implements On cipherService: CipherService, organizationSerivce: OrganizationService, logService: LogService, + protected dialogRef: DialogRef, + @Inject(DIALOG_DATA) params: CollectionsDialogParams, ) { super( collectionService, @@ -30,10 +34,16 @@ export class CollectionsComponent extends BaseCollectionsComponent implements On organizationSerivce, logService, ); + this.cipherId = params?.cipherId; } - ngOnDestroy() { - this.selectAll(false); + override async submit(): Promise<boolean> { + const success = await super.submit(); + if (success) { + this.dialogRef.close(CollectionsDialogResult.Saved); + return true; + } + return false; } check(c: CollectionView, select?: boolean) { @@ -46,4 +56,31 @@ export class CollectionsComponent extends BaseCollectionsComponent implements On selectAll(select: boolean) { this.collections.forEach((c) => this.check(c, select)); } + + ngOnDestroy() { + this.selectAll(false); + } +} + +export interface CollectionsDialogParams { + cipherId: string; +} + +export enum CollectionsDialogResult { + Saved = "saved", +} + +/** + * Strongly typed helper to open a Collections dialog + * @param dialogService Instance of the dialog service that will be used to open the dialog + * @param config Optional configuration for the dialog + */ +export function openIndividualVaultCollectionsDialog( + dialogService: DialogService, + config?: DialogConfig<CollectionsDialogParams>, +) { + return dialogService.open<CollectionsDialogResult, CollectionsDialogParams>( + CollectionsComponent, + config, + ); } diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index 6fe31f29f4..a25ba6edbc 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -86,7 +86,7 @@ import { BulkShareDialogResult, openBulkShareDialog, } from "./bulk-action-dialogs/bulk-share-dialog/bulk-share-dialog.component"; -import { CollectionsComponent } from "./collections.component"; +import { openIndividualVaultCollectionsDialog } from "./collections.component"; import { FolderAddEditDialogResult, openFolderAddEditDialog } from "./folder-add-edit.component"; import { ShareComponent } from "./share.component"; import { VaultFilterComponent } from "./vault-filter/components/vault-filter.component"; @@ -568,17 +568,7 @@ export class VaultComponent implements OnInit, OnDestroy { } async editCipherCollections(cipher: CipherView) { - const [modal] = await this.modalService.openViewRef( - CollectionsComponent, - this.collectionsModalRef, - (comp) => { - comp.cipherId = cipher.id; - comp.onSavedCollections.pipe(takeUntil(this.destroy$)).subscribe(() => { - modal.close(); - this.refresh(); - }); - }, - ); + openIndividualVaultCollectionsDialog(this.dialogService, { data: { cipherId: cipher.id } }); } async addCipher() { diff --git a/apps/web/src/app/vault/org-vault/collections.component.ts b/apps/web/src/app/vault/org-vault/collections.component.ts index 020d3fbe95..67eac2098f 100644 --- a/apps/web/src/app/vault/org-vault/collections.component.ts +++ b/apps/web/src/app/vault/org-vault/collections.component.ts @@ -1,4 +1,5 @@ -import { Component } from "@angular/core"; +import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; +import { Component, Inject } from "@angular/core"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; @@ -11,8 +12,13 @@ import { CollectionService } from "@bitwarden/common/vault/abstractions/collecti import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { CipherCollectionsRequest } from "@bitwarden/common/vault/models/request/cipher-collections.request"; +import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; +import { DialogService } from "@bitwarden/components"; -import { CollectionsComponent as BaseCollectionsComponent } from "../individual-vault/collections.component"; +import { + CollectionsComponent as BaseCollectionsComponent, + CollectionsDialogResult, +} from "../individual-vault/collections.component"; @Component({ selector: "app-org-vault-collections", @@ -29,6 +35,8 @@ export class CollectionsComponent extends BaseCollectionsComponent { organizationService: OrganizationService, private apiService: ApiService, logService: LogService, + protected dialogRef: DialogRef, + @Inject(DIALOG_DATA) params: OrgVaultCollectionsDialogParams, ) { super( collectionService, @@ -37,8 +45,14 @@ export class CollectionsComponent extends BaseCollectionsComponent { cipherService, organizationService, logService, + dialogRef, + params, ); this.allowSelectNone = true; + this.collectionIds = params?.collectionIds; + this.collections = params?.collections; + this.organization = params?.organization; + this.cipherId = params?.cipherId; } protected async loadCipher() { @@ -79,3 +93,25 @@ export class CollectionsComponent extends BaseCollectionsComponent { } } } + +export interface OrgVaultCollectionsDialogParams { + collectionIds: string[]; + collections: CollectionView[]; + organization: Organization; + cipherId: string; +} + +/** + * Strongly typed helper to open a Collections dialog + * @param dialogService Instance of the dialog service that will be used to open the dialog + * @param config Optional configuration for the dialog + */ +export function openOrgVaultCollectionsDialog( + dialogService: DialogService, + config?: DialogConfig<OrgVaultCollectionsDialogParams>, +) { + return dialogService.open<CollectionsDialogResult, OrgVaultCollectionsDialogParams>( + CollectionsComponent, + config, + ); +} diff --git a/apps/web/src/app/vault/org-vault/vault.component.ts b/apps/web/src/app/vault/org-vault/vault.component.ts index 50d3216150..587758dda1 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.ts +++ b/apps/web/src/app/vault/org-vault/vault.component.ts @@ -75,6 +75,7 @@ import { BulkDeleteDialogResult, openBulkDeleteDialog, } from "../individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component"; +import { CollectionsDialogResult } from "../individual-vault/collections.component"; import { RoutedVaultFilterBridgeService } from "../individual-vault/vault-filter/services/routed-vault-filter-bridge.service"; import { RoutedVaultFilterService } from "../individual-vault/vault-filter/services/routed-vault-filter.service"; import { createFilterFunction } from "../individual-vault/vault-filter/shared/models/filter-function"; @@ -95,7 +96,7 @@ import { BulkCollectionsDialogComponent, BulkCollectionsDialogResult, } from "./bulk-collections-dialog"; -import { CollectionsComponent } from "./collections.component"; +import { openOrgVaultCollectionsDialog } from "./collections.component"; import { VaultFilterComponent } from "./vault-filter/vault-filter.component"; const BroadcasterSubscriptionId = "OrgVaultComponent"; @@ -711,21 +712,37 @@ export class VaultComponent implements OnInit, OnDestroy { } else { collections = await firstValueFrom(this.allCollectionsWithoutUnassigned$); } - const [modal] = await this.modalService.openViewRef( - CollectionsComponent, - this.collectionsModalRef, - (comp) => { - comp.flexibleCollectionsV1Enabled = this.flexibleCollectionsV1Enabled; - comp.collectionIds = cipher.collectionIds; - comp.collections = collections; - comp.organization = this.organization; - comp.cipherId = cipher.id; - comp.onSavedCollections.pipe(takeUntil(this.destroy$)).subscribe(() => { - modal.close(); - this.refresh(); - }); + const dialog = openOrgVaultCollectionsDialog(this.dialogService, { + data: { + collectionIds: cipher.collectionIds, + collections: collections.filter((c) => !c.readOnly && c.id != Unassigned), + organization: this.organization, + cipherId: cipher.id, }, - ); + }); + /** + + const [modal] = await this.modalService.openViewRef( + CollectionsComponent, + this.collectionsModalRef, + (comp) => { + comp.flexibleCollectionsV1Enabled = this.flexibleCollectionsV1Enabled; + comp.collectionIds = cipher.collectionIds; + comp.collections = collections; + comp.organization = this.organization; + comp.cipherId = cipher.id; + comp.onSavedCollections.pipe(takeUntil(this.destroy$)).subscribe(() => { + modal.close(); + this.refresh(); + }); + }, + ); + + */ + + if ((await lastValueFrom(dialog.closed)) == CollectionsDialogResult.Saved) { + await this.refresh(); + } } async addCipher() { 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<boolean> { 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; } } From 06acdefa914e3c6c84403aea2fd815e2e51e7c48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Gon=C3=A7alves?= <cgoncalves@bitwarden.com> Date: Tue, 16 Apr 2024 17:37:03 +0100 Subject: [PATCH 189/351] [PM-5273] Migrate state in CipherService (#8314) * PM-5273 Initial migration work for localData * PM-5273 Encrypted and Decrypted ciphers migration to state provider * pm-5273 Update references * pm5273 Ensure prototype on cipher * PM-5273 Add CipherId * PM-5273 Remove migrated methods and updated references * pm-5273 Fix versions * PM-5273 Added missing options * Conflict resolution * Revert "Conflict resolution" This reverts commit 0c0c2039edd4ee442456b6c5a4863a92b9a50e16. * PM-5273 Fix PR comments * Pm-5273 Fix comments * PM-5273 Changed decryptedCiphers to use ActiveUserState * PM-5273 Fix tests * PM-5273 Fix pr comments --- .../notification.background.spec.ts | 2 +- .../background/notification.background.ts | 4 +- .../background/overlay.background.spec.ts | 6 +- .../autofill/background/overlay.background.ts | 2 +- .../browser/src/background/main.background.ts | 2 +- apps/browser/src/popup/app.component.ts | 4 +- .../popup/generator/generator.component.ts | 9 +- .../cipher-service.factory.ts | 2 + .../folder-service.factory.ts | 10 +- .../components/vault/add-edit.component.ts | 2 +- apps/cli/src/bw.ts | 2 +- .../src/app/tools/generator.component.spec.ts | 5 + apps/web/src/app/core/state/state.service.ts | 14 -- .../src/services/jslib-services.module.ts | 4 +- .../vault/components/add-edit.component.ts | 6 +- .../platform/abstractions/state.service.ts | 18 -- .../src/platform/models/domain/account.ts | 16 +- .../src/platform/services/state.service.ts | 138 ----------- .../src/platform/state/state-definitions.ts | 5 + libs/common/src/state-migrations/migrate.ts | 7 +- ...e-cipher-service-to-state-provider.spec.ts | 170 +++++++++++++ ...7-move-cipher-service-to-state-provider.ts | 79 ++++++ .../src/vault/abstractions/cipher.service.ts | 8 + .../src/vault/models/data/cipher.data.ts | 6 + .../src/vault/services/cipher.service.spec.ts | 9 + .../src/vault/services/cipher.service.ts | 227 ++++++++++++------ .../services/folder/folder.service.spec.ts | 11 +- .../vault/services/folder/folder.service.ts | 10 +- .../vault/services/key-state/ciphers.state.ts | 52 ++++ 29 files changed, 525 insertions(+), 305 deletions(-) create mode 100644 libs/common/src/state-migrations/migrations/57-move-cipher-service-to-state-provider.spec.ts create mode 100644 libs/common/src/state-migrations/migrations/57-move-cipher-service-to-state-provider.ts create mode 100644 libs/common/src/vault/services/key-state/ciphers.state.ts diff --git a/apps/browser/src/autofill/background/notification.background.spec.ts b/apps/browser/src/autofill/background/notification.background.spec.ts index 93750ece07..45f095aee9 100644 --- a/apps/browser/src/autofill/background/notification.background.spec.ts +++ b/apps/browser/src/autofill/background/notification.background.spec.ts @@ -720,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 74e6147505..9b65e4db0b 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -600,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 2599c1825e..e65397a62b 100644 --- a/apps/browser/src/autofill/background/overlay.background.spec.ts +++ b/apps/browser/src/autofill/background/overlay.background.spec.ts @@ -592,7 +592,7 @@ describe("OverlayBackground", () => { beforeEach(() => { sender = mock<chrome.runtime.MessageSender>({ 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 50fb80ef1b..551263525e 100644 --- a/apps/browser/src/autofill/background/overlay.background.ts +++ b/apps/browser/src/autofill/background/overlay.background.ts @@ -636,7 +636,7 @@ class OverlayBackground implements OverlayBackgroundInterface { cipherView.type = CipherType.Login; cipherView.login = loginView; - await this.stateService.setAddEditCipherInfo({ + await this.cipherService.setAddEditCipherInfo({ cipher: cipherView, collectionIds: cipherView.collectionIds, }); diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 105e7e2a38..642510b4de 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -663,12 +663,12 @@ export default class MainBackground { this.encryptService, this.cipherFileUploadService, this.configService, + this.stateProvider, ); this.folderService = new FolderService( this.cryptoService, this.i18nService, this.cipherService, - this.stateService, this.stateProvider, ); this.folderApiService = new FolderApiService(this.folderService, this.apiService); diff --git a/apps/browser/src/popup/app.component.ts b/apps/browser/src/popup/app.component.ts index b0fdaec4fc..c224e652f6 100644 --- a/apps/browser/src/popup/app.component.ts +++ b/apps/browser/src/popup/app.component.ts @@ -7,6 +7,7 @@ import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { DialogService, SimpleDialogOptions } from "@bitwarden/components"; import { BrowserApi } from "../platform/browser/browser-api"; @@ -42,6 +43,7 @@ export class AppComponent implements OnInit, OnDestroy { private stateService: BrowserStateService, private browserSendStateService: BrowserSendStateService, private vaultBrowserStateService: VaultBrowserStateService, + private cipherService: CipherService, private changeDetectorRef: ChangeDetectorRef, private ngZone: NgZone, private platformUtilsService: ForegroundPlatformUtilsService, @@ -161,7 +163,7 @@ export class AppComponent implements OnInit, OnDestroy { await this.clearComponentStates(); } if (url.startsWith("/tabs/")) { - await this.stateService.setAddEditCipherInfo(null); + await this.cipherService.setAddEditCipherInfo(null); } (window as any).previousPopupUrl = url; diff --git a/apps/browser/src/tools/popup/generator/generator.component.ts b/apps/browser/src/tools/popup/generator/generator.component.ts index c683b6f4a6..0c11c28f27 100644 --- a/apps/browser/src/tools/popup/generator/generator.component.ts +++ b/apps/browser/src/tools/popup/generator/generator.component.ts @@ -1,6 +1,7 @@ import { Location } from "@angular/common"; import { Component } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; +import { firstValueFrom } from "rxjs"; import { GeneratorComponent as BaseGeneratorComponent } from "@bitwarden/angular/tools/generator/components/generator.component"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -9,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 { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { AddEditCipherInfo } from "@bitwarden/common/vault/types/add-edit-cipher-info"; @@ -19,6 +21,7 @@ import { AddEditCipherInfo } from "@bitwarden/common/vault/types/add-edit-cipher export class GeneratorComponent extends BaseGeneratorComponent { private addEditCipherInfo: AddEditCipherInfo; private cipherState: CipherView; + private cipherService: CipherService; constructor( passwordGenerationService: PasswordGenerationServiceAbstraction, @@ -26,6 +29,7 @@ export class GeneratorComponent extends BaseGeneratorComponent { platformUtilsService: PlatformUtilsService, i18nService: I18nService, stateService: StateService, + cipherService: CipherService, route: ActivatedRoute, logService: LogService, private location: Location, @@ -40,10 +44,11 @@ export class GeneratorComponent extends BaseGeneratorComponent { route, window, ); + this.cipherService = cipherService; } async ngOnInit() { - this.addEditCipherInfo = await this.stateService.getAddEditCipherInfo(); + this.addEditCipherInfo = await firstValueFrom(this.cipherService.addEditCipherInfo$); if (this.addEditCipherInfo != null) { this.cipherState = this.addEditCipherInfo.cipher; } @@ -64,7 +69,7 @@ export class GeneratorComponent extends BaseGeneratorComponent { this.addEditCipherInfo.cipher = this.cipherState; // 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.stateService.setAddEditCipherInfo(this.addEditCipherInfo); + this.cipherService.setAddEditCipherInfo(this.addEditCipherInfo); this.close(); } 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/popup/components/vault/add-edit.component.ts b/apps/browser/src/vault/popup/components/vault/add-edit.component.ts index b27a986231..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 @@ -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/cli/src/bw.ts b/apps/cli/src/bw.ts index 4228eba965..f02d7da49c 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -544,13 +544,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, ); 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<LogService>(), }, + { + provide: CipherService, + useValue: mock<CipherService>(), + }, ], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); diff --git a/apps/web/src/app/core/state/state.service.ts b/apps/web/src/app/core/state/state.service.ts index 54e456d34c..1ae62d8591 100644 --- a/apps/web/src/app/core/state/state.service.ts +++ b/apps/web/src/app/core/state/state.service.ts @@ -18,7 +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 { CipherData } from "@bitwarden/common/vault/models/data/cipher.data"; import { Account } from "./account"; import { GlobalState } from "./global-state"; @@ -57,19 +56,6 @@ export class StateService extends BaseStateService<GlobalState, Account> { 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<void> { - options = this.reconcileOptions(options, await this.defaultInMemoryOptions()); - return await super.setEncryptedCiphers(value, options); - } - override async getLastSync(options?: StorageOptions): Promise<string> { options = this.reconcileOptions(options, await this.defaultInMemoryOptions()); return await super.getLastSync(options); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index b311000fb8..dbb94f6753 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -411,6 +411,7 @@ const safeProviders: SafeProvider[] = [ encryptService: EncryptService, fileUploadService: CipherFileUploadServiceAbstraction, configService: ConfigService, + stateProvider: StateProvider, ) => new CipherService( cryptoService, @@ -423,6 +424,7 @@ const safeProviders: SafeProvider[] = [ encryptService, fileUploadService, configService, + stateProvider, ), deps: [ CryptoServiceAbstraction, @@ -435,6 +437,7 @@ const safeProviders: SafeProvider[] = [ EncryptService, CipherFileUploadServiceAbstraction, ConfigService, + StateProvider, ], }), safeProvider({ @@ -444,7 +447,6 @@ const safeProviders: SafeProvider[] = [ CryptoServiceAbstraction, I18nServiceAbstraction, CipherServiceAbstraction, - StateServiceAbstraction, StateProvider, ], }), diff --git a/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts index ab09d14c86..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"; @@ -687,7 +687,7 @@ export class AddEditComponent implements OnInit, OnDestroy { } async loadAddEditCipherInfo(): Promise<boolean> { - const addEditCipherInfo: any = await this.stateService.getAddEditCipherInfo(); + const addEditCipherInfo: any = await firstValueFrom(this.cipherService.addEditCipherInfo$); const loadedSavedInfo = addEditCipherInfo != null; if (loadedSavedInfo) { @@ -700,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/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index fd76cad6d1..2348c8844a 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -6,10 +6,6 @@ import { GeneratorOptions } from "../../tools/generator/generator-options"; import { GeneratedPasswordHistory, PasswordGeneratorOptions } from "../../tools/generator/password"; import { UsernameGeneratorOptions } from "../../tools/generator/username"; import { UserId } from "../../types/guid"; -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 { Account } from "../models/domain/account"; import { EncString } from "../models/domain/enc-string"; @@ -38,8 +34,6 @@ export abstract class StateService<T extends Account = Account> { clean: (options?: StorageOptions) => Promise<UserId>; init: (initOptions?: InitOptions) => Promise<void>; - getAddEditCipherInfo: (options?: StorageOptions) => Promise<AddEditCipherInfo>; - setAddEditCipherInfo: (value: AddEditCipherInfo, options?: StorageOptions) => Promise<void>; /** * Gets the user's auto key */ @@ -104,8 +98,6 @@ export abstract class StateService<T extends Account = Account> { * @deprecated For migration purposes only, use setUserKeyBiometric instead */ setCryptoMasterKeyBiometric: (value: BiometricKey, options?: StorageOptions) => Promise<void>; - getDecryptedCiphers: (options?: StorageOptions) => Promise<CipherView[]>; - setDecryptedCiphers: (value: CipherView[], options?: StorageOptions) => Promise<void>; getDecryptedPasswordGenerationHistory: ( options?: StorageOptions, ) => Promise<GeneratedPasswordHistory[]>; @@ -134,11 +126,6 @@ export abstract class StateService<T extends Account = Account> { value: boolean, options?: StorageOptions, ) => Promise<void>; - getEncryptedCiphers: (options?: StorageOptions) => Promise<{ [id: string]: CipherData }>; - setEncryptedCiphers: ( - value: { [id: string]: CipherData }, - options?: StorageOptions, - ) => Promise<void>; getEncryptedPasswordGenerationHistory: ( options?: StorageOptions, ) => Promise<GeneratedPasswordHistory[]>; @@ -165,11 +152,6 @@ export abstract class StateService<T extends Account = Account> { setLastActive: (value: number, options?: StorageOptions) => Promise<void>; getLastSync: (options?: StorageOptions) => Promise<string>; setLastSync: (value: string, options?: StorageOptions) => Promise<void>; - getLocalData: (options?: StorageOptions) => Promise<{ [cipherId: string]: LocalData }>; - setLocalData: ( - value: { [cipherId: string]: LocalData }, - options?: StorageOptions, - ) => Promise<void>; getMinimizeOnCopyToClipboard: (options?: StorageOptions) => Promise<boolean>; setMinimizeOnCopyToClipboard: (value: boolean, options?: StorageOptions) => Promise<void>; getOrganizationInvitation: (options?: StorageOptions) => Promise<any>; diff --git a/libs/common/src/platform/models/domain/account.ts b/libs/common/src/platform/models/domain/account.ts index 759a903514..ae7780ada4 100644 --- a/libs/common/src/platform/models/domain/account.ts +++ b/libs/common/src/platform/models/domain/account.ts @@ -8,9 +8,6 @@ import { } from "../../../tools/generator/password"; import { UsernameGeneratorOptions } from "../../../tools/generator/username/username-generation-options"; import { DeepJsonify } from "../../../types/deep-jsonify"; -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"; @@ -61,28 +58,17 @@ export class DataEncryptionPair<TEncrypted, TDecrypted> { } export class AccountData { - ciphers?: DataEncryptionPair<CipherData, CipherView> = new DataEncryptionPair< - CipherData, - CipherView - >(); - localData?: any; passwordGenerationHistory?: EncryptionPair< GeneratedPasswordHistory[], GeneratedPasswordHistory[] > = new EncryptionPair<GeneratedPasswordHistory[], GeneratedPasswordHistory[]>(); - addEditCipherInfo?: AddEditCipherInfo; static fromJSON(obj: DeepJsonify<AccountData>): 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); } } diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index 3d512175a8..9edc9ed1e3 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -9,10 +9,6 @@ import { GeneratorOptions } from "../../tools/generator/generator-options"; import { GeneratedPasswordHistory, PasswordGeneratorOptions } from "../../tools/generator/password"; import { UsernameGeneratorOptions } from "../../tools/generator/username"; import { UserId } from "../../types/guid"; -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 { @@ -221,34 +217,6 @@ export class StateService< return currentUser as UserId; } - async getAddEditCipherInfo(options?: StorageOptions): Promise<AddEditCipherInfo> { - 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<CipherView>), - collectionIds: raw?.collectionIds, - }; - } - - async setAddEditCipherInfo(value: AddEditCipherInfo, options?: StorageOptions): Promise<void> { - 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()), - ); - } - /** * user key when using the "never" option of vault timeout */ @@ -465,24 +433,6 @@ export class StateService< await this.saveSecureStorageKey(partialKeys.biometricKey, value, options); } - @withPrototypeForArrayMembers(CipherView, CipherView.fromJSON) - async getDecryptedCiphers(options?: StorageOptions): Promise<CipherView[]> { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) - )?.data?.ciphers?.decrypted; - } - - async setDecryptedCiphers(value: CipherView[], options?: StorageOptions): Promise<void> { - 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, @@ -621,27 +571,6 @@ export class StateService< ); } - @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<void> { - 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 */ @@ -805,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<void> { - 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<boolean> { return ( (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) @@ -1510,50 +1419,3 @@ function withPrototypeForArrayMembers<T>( }; }; } - -function withPrototypeForObjectValues<T>( - 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/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index 518e5c51d6..18df252062 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -135,3 +135,8 @@ export const VAULT_SETTINGS_DISK = new StateDefinition("vaultSettings", "disk", }); 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/state-migrations/migrate.ts b/libs/common/src/state-migrations/migrate.ts index 77b949126f..000f85519e 100644 --- a/libs/common/src/state-migrations/migrate.ts +++ b/libs/common/src/state-migrations/migrate.ts @@ -53,6 +53,7 @@ import { DeviceTrustCryptoServiceStateProviderMigrator } from "./migrations/53-m 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 { 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"; @@ -60,8 +61,7 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting import { MinVersionMigrator } from "./migrations/min-version"; export const MIN_VERSION = 3; -export const CURRENT_VERSION = 56; - +export const CURRENT_VERSION = 57; export type MinVersion = typeof MIN_VERSION; export function createMigrationBuilder() { @@ -119,7 +119,8 @@ export function createMigrationBuilder() { .with(DeviceTrustCryptoServiceStateProviderMigrator, 52, 53) .with(SendMigrator, 53, 54) .with(MoveMasterKeyStateToProviderMigrator, 54, 55) - .with(AuthRequestMigrator, 55, CURRENT_VERSION); + .with(AuthRequestMigrator, 55, 56) + .with(CipherServiceMigrator, 56, CURRENT_VERSION); } export async function currentVersion( 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<MigrationHelper>; + 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<void> { + const accounts = await helper.getAccounts<ExpectedAccountType>(); + async function migrateAccount(userId: string, account: ExpectedAccountType): Promise<void> { + 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<void> { + const accounts = await helper.getAccounts<ExpectedAccountType>(); + async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise<void> { + //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/vault/abstractions/cipher.service.ts b/libs/common/src/vault/abstractions/cipher.service.ts index a8a0a25e9b..501fd87665 100644 --- a/libs/common/src/vault/abstractions/cipher.service.ts +++ b/libs/common/src/vault/abstractions/cipher.service.ts @@ -1,3 +1,5 @@ +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"; @@ -7,8 +9,13 @@ 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, @@ -102,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/services/cipher.service.spec.ts b/libs/common/src/vault/services/cipher.service.spec.ts index c374724781..28c4bfc653 100644 --- a/libs/common/src/vault/services/cipher.service.spec.ts +++ b/libs/common/src/vault/services/cipher.service.spec.ts @@ -1,6 +1,8 @@ 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"; @@ -12,10 +14,12 @@ 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>(); @@ -109,6 +115,8 @@ describe("Cipher Service", () => { const searchService = mock<SearchService>(); const encryptService = mock<EncryptService>(); 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 dffbf5cbbe..e8544d7f98 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 } from "rxjs"; import { SemVer } from "semver"; import { ApiService } from "../../abstractions/api.service"; @@ -21,13 +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 { OrgKey, UserKey } from "../../types/key"; +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"; @@ -54,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"); @@ -62,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, @@ -73,11 +93,17 @@ export class CipherService implements CipherServiceAbstraction { private encryptService: EncryptService, private cipherFileUploadService: CipherFileUploadService, 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$; + this.ciphers$ = this.encryptedCiphersState.state$; + this.cipherViews$ = this.decryptedCiphersState.state$; + this.addEditCipherInfo$ = this.addEditCipherInfoState.state$; } async setDecryptedCipherCache(value: CipherView[]) { @@ -85,7 +111,7 @@ 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) { @@ -96,6 +122,14 @@ export class CipherService implements CipherServiceAbstraction { } } + 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); } @@ -268,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; @@ -293,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) { @@ -326,7 +374,6 @@ export class CipherService implements CipherServiceAbstraction { .flat() .sort(this.getLocaleSortingFunction()); - await this.setDecryptedCipherCache(decCiphers); return decCiphers; } @@ -336,7 +383,7 @@ export class CipherService implements CipherServiceAbstraction { this.searchService != null && ((await firstValueFrom(this.searchService.indexedEntityId$)) ?? userId) !== userId; if (reindexRequired) { - await this.searchService.indexCiphers(await this.getDecryptedCipherCache(), userId); + await this.searchService.indexCiphers(await this.getDecryptedCiphers(), userId); } } @@ -448,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; } @@ -471,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; } @@ -502,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> { @@ -711,7 +762,7 @@ export class CipherService implements CipherServiceAbstraction { await this.apiService.send("POST", "/ciphers/bulk-collections", request, true, false); // Update the local state - const ciphers = await this.stateService.getEncryptedCiphers(); + const ciphers = await firstValueFrom(this.ciphers$); for (const id of cipherIds) { const cipher = ciphers[id]; @@ -728,30 +779,29 @@ export class CipherService implements CipherServiceAbstraction { } await this.clearCache(); - await this.stateService.setEncryptedCiphers(ciphers); + 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]); + 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> { @@ -762,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 = {}; } @@ -770,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> { @@ -820,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> { @@ -917,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; } @@ -930,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> { @@ -963,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) { @@ -983,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> { @@ -1025,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 @@ -1350,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(); } 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 }; + }, + }, +); From cbb7e1840d54882fa67530b04adb0600b852ff79 Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Tue, 16 Apr 2024 19:39:31 +0200 Subject: [PATCH 190/351] [PM-2570] [PM-4649] Update change master password UI (#8416) * Update the change master password dialog on browser Change text to remove the mention of the bitwarden.com web vault Change icon to show it's external link Changes based on Figma attached to PM-2570 * Update the change master password dialog on desktop Change text to remove the mention of the bitwarden.com web vault Changes based on Figma attached to PM-2570 and to replicate what is done on browser --------- Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com> --- apps/browser/src/_locales/en/messages.json | 12 ++++++------ .../src/popup/settings/settings.component.html | 2 +- .../browser/src/popup/settings/settings.component.ts | 5 +++-- apps/desktop/src/locales/en/messages.json | 7 +++++-- apps/desktop/src/main/menu/menu.account.ts | 8 ++++---- 5 files changed, 19 insertions(+), 15 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 4108db3996..5e941083df 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?" }, diff --git a/apps/browser/src/popup/settings/settings.component.html b/apps/browser/src/popup/settings/settings.component.html index f099528918..98c218b0db 100644 --- a/apps/browser/src/popup/settings/settings.component.html +++ b/apps/browser/src/popup/settings/settings.component.html @@ -153,7 +153,7 @@ *ngIf="showChangeMasterPass" > <div class="row-main">{{ "changeMasterPassword" | i18n }}</div> - <i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i> + <i class="bwi bwi-external-link bwi-lg row-sub-icon" aria-hidden="true"></i> </button> <button type="button" diff --git a/apps/browser/src/popup/settings/settings.component.ts b/apps/browser/src/popup/settings/settings.component.ts index 52e44a2531..fa6c64fcc5 100644 --- a/apps/browser/src/popup/settings/settings.component.ts +++ b/apps/browser/src/popup/settings/settings.component.ts @@ -441,9 +441,10 @@ export class SettingsComponent implements OnInit { async changePassword() { const confirmed = await this.dialogService.openSimpleDialog({ - title: { key: "changeMasterPassword" }, - content: { key: "changeMasterPasswordConfirmation" }, + title: { key: "continueToWebApp" }, + content: { key: "changeMasterPasswordOnWebConfirmation" }, type: "info", + acceptButtonText: { key: "continue" }, }); if (confirmed) { const env = await firstValueFrom(this.environmentService.environment$); diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 394a5951e9..a0d34e4075 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", 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, From 51a6b34cc29dd3316c89c7fa706d263cbc127c9c Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Tue, 16 Apr 2024 14:05:47 -0400 Subject: [PATCH 191/351] Auth/PM-7467- Fix Refresh token issues (#8757) * PM-7467 - Login Strategy bug - VaultTimeoutSettings will be undefined before the account is activated unless you pass in user ids to retrieve the data. This resulted in refresh tokens always being set into secure storage regardless of a user's vault timeout settings (logout should translate to memory) * PM-7467 - TokenSvc - Fix bug in getRefreshToken which would retrieve the user's refresh token from secure storage even if the user had changed their vault timeout setting to log out which moved the refresh token into memory. Includes a migration to remove the no longer required REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE state provider flag. * PM-7467 - Per PR feedback, use IRREVERSIBLE for rollback. Co-authored-by: Jake Fink <jfink@bitwarden.com> * PM-7467 - fix tests * PM-7467 - Fix migrator based on PR feedback. * PM-7467 - Bump migration version --------- Co-authored-by: Jake Fink <jfink@bitwarden.com> --- .../common/login-strategies/login.strategy.ts | 4 +- .../src/auth/services/token.service.spec.ts | 30 +------- .../common/src/auth/services/token.service.ts | 35 ++++----- .../src/auth/services/token.state.spec.ts | 2 - libs/common/src/auth/services/token.state.ts | 9 --- libs/common/src/state-migrations/migrate.ts | 6 +- ...token-migrated-state-provider-flag.spec.ts | 72 +++++++++++++++++++ ...resh-token-migrated-state-provider-flag.ts | 34 +++++++++ 8 files changed, 125 insertions(+), 67 deletions(-) create mode 100644 libs/common/src/state-migrations/migrations/58-remove-refresh-token-migrated-state-provider-flag.spec.ts create mode 100644 libs/common/src/state-migrations/migrations/58-remove-refresh-token-migrated-state-provider-flag.ts diff --git a/libs/auth/src/common/login-strategies/login.strategy.ts b/libs/auth/src/common/login-strategies/login.strategy.ts index 94f96d40d0..a6dc193183 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.ts @@ -166,8 +166,8 @@ export abstract class LoginStrategy { const userId = accountInformation.sub; - 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. diff --git a/libs/common/src/auth/services/token.service.spec.ts b/libs/common/src/auth/services/token.service.spec.ts index c409263209..d32c4d8e1c 100644 --- a/libs/common/src/auth/services/token.service.spec.ts +++ b/libs/common/src/auth/services/token.service.spec.ts @@ -23,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", () => { @@ -1120,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); }); }); }); @@ -1260,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 @@ -1284,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 @@ -1305,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); @@ -1335,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(); diff --git a/libs/common/src/auth/services/token.service.ts b/libs/common/src/auth/services/token.service.ts index db39997663..c24a2c186b 100644 --- a/libs/common/src/auth/services/token.service.ts +++ b/libs/common/src/auth/services/token.service.ts @@ -32,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 { @@ -441,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: @@ -467,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( @@ -484,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; } @@ -516,18 +517,6 @@ export class TokenService implements TokenServiceAbstraction { await this.singleUserStateProvider.get(userId, REFRESH_TOKEN_DISK).update((_) => null); } - private async getRefreshTokenMigratedToSecureStorage(userId: UserId): Promise<boolean> { - return await firstValueFrom( - this.singleUserStateProvider.get(userId, REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE).state$, - ); - } - - private async setRefreshTokenMigratedToSecureStorage(userId: UserId): Promise<void> { - await this.singleUserStateProvider - .get(userId, REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE) - .update((_) => true); - } - async setClientId( clientId: string, vaultTimeoutAction: VaultTimeoutAction, diff --git a/libs/common/src/auth/services/token.state.spec.ts b/libs/common/src/auth/services/token.state.spec.ts index 55f97b7e00..dc00fec383 100644 --- a/libs/common/src/auth/services/token.state.spec.ts +++ b/libs/common/src/auth/services/token.state.spec.ts @@ -10,7 +10,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.each([ @@ -18,7 +17,6 @@ describe.each([ [ACCESS_TOKEN_MEMORY, "accessTokenMemory"], [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"], diff --git a/libs/common/src/auth/services/token.state.ts b/libs/common/src/auth/services/token.state.ts index a8c6878fbb..458d6846c1 100644 --- a/libs/common/src/auth/services/token.state.ts +++ b/libs/common/src/auth/services/token.state.ts @@ -30,15 +30,6 @@ export const REFRESH_TOKEN_MEMORY = new UserKeyDefinition<string>(TOKEN_MEMORY, clearOn: [], // Manually handled }); -export const REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE = new UserKeyDefinition<boolean>( - TOKEN_DISK, - "refreshTokenMigratedToSecureStorage", - { - deserializer: (refreshTokenMigratedToSecureStorage) => refreshTokenMigratedToSecureStorage, - clearOn: [], // Don't clear on lock/logout so that we always check the correct place (secure storage) for the refresh token if it's been migrated - }, -); - export const EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL = KeyDefinition.record<string, string>( TOKEN_DISK_LOCAL, "emailTwoFactorTokenRecord", diff --git a/libs/common/src/state-migrations/migrate.ts b/libs/common/src/state-migrations/migrate.ts index 000f85519e..f9a8734731 100644 --- a/libs/common/src/state-migrations/migrate.ts +++ b/libs/common/src/state-migrations/migrate.ts @@ -54,6 +54,7 @@ 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"; @@ -61,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 = 57; +export const CURRENT_VERSION = 58; export type MinVersion = typeof MIN_VERSION; export function createMigrationBuilder() { @@ -120,7 +121,8 @@ export function createMigrationBuilder() { .with(SendMigrator, 53, 54) .with(MoveMasterKeyStateToProviderMigrator, 54, 55) .with(AuthRequestMigrator, 55, 56) - .with(CipherServiceMigrator, 56, CURRENT_VERSION); + .with(CipherServiceMigrator, 56, 57) + .with(RemoveRefreshTokenMigratedFlagMigrator, 57, CURRENT_VERSION); } export async function currentVersion( 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<MigrationHelper>; + 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<unknown>; + +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<void> { + const accounts = await helper.getAccounts<ExpectedAccountType>(); + async function migrateAccount(userId: string, account: ExpectedAccountType): Promise<void> { + 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<void> { + throw IRREVERSIBLE; + } +} From 3c2d3669c56f0d6055732453d75b1ed496e4741b Mon Sep 17 00:00:00 2001 From: Bitwarden DevOps <106330231+bitwarden-devops-bot@users.noreply.github.com> Date: Tue, 16 Apr 2024 15:26:30 -0400 Subject: [PATCH 192/351] Bumped web version to (#8772) --- apps/web/package.json | 2 +- package-lock.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/package.json b/apps/web/package.json index 99828bb543..55fe0987d7 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/web-vault", - "version": "2024.4.0", + "version": "2024.4.1", "scripts": { "build:oss": "webpack", "build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js", diff --git a/package-lock.json b/package-lock.json index c399536cca..096f6653cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -247,7 +247,7 @@ }, "apps/web": { "name": "@bitwarden/web-vault", - "version": "2024.4.0" + "version": "2024.4.1" }, "libs/admin-console": { "name": "@bitwarden/admin-console", From d6f2965367dbf583350b3bbe19b06cc109fd5665 Mon Sep 17 00:00:00 2001 From: vinith-kovan <156108204+vinith-kovan@users.noreply.github.com> Date: Wed, 17 Apr 2024 01:07:47 +0530 Subject: [PATCH 193/351] [PM-5015] org billing history view component migration (#8302) --- .../organization-billing-history-view.component.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/billing/organizations/organization-billing-history-view.component.html b/apps/web/src/app/billing/organizations/organization-billing-history-view.component.html index 9df05d02ed..087009b291 100644 --- a/apps/web/src/app/billing/organizations/organization-billing-history-view.component.html +++ b/apps/web/src/app/billing/organizations/organization-billing-history-view.component.html @@ -16,11 +16,11 @@ <bit-container> <ng-container *ngIf="!firstLoaded && loading"> <i - class="bwi bwi-spinner bwi-spin text-muted" + class="bwi bwi-spinner bwi-spin tw-text-muted" title="{{ 'loading' | i18n }}" aria-hidden="true" ></i> - <span class="sr-only">{{ "loading" | i18n }}</span> + <span class="tw-sr-only">{{ "loading" | i18n }}</span> </ng-container> <ng-container *ngIf="billing"> <app-billing-history [billing]="billing"></app-billing-history> From f5198e86fd8a2187e3c9e528ba788edf2595fa2d Mon Sep 17 00:00:00 2001 From: KiruthigaManivannan <162679756+KiruthigaManivannan@users.noreply.github.com> Date: Wed, 17 Apr 2024 01:08:19 +0530 Subject: [PATCH 194/351] PM-5019 Migrate Adjust Payment component (#8383) * PM-5019 Migrated Adjust Payment component * PM-5019 Migrated Adjust Payment dialog component * PM-5019 Removing type any * PM-5019 Addressed review comments * PM-5019 Included deleted line space --- .../adjust-payment-dialog.component.html | 25 ++++ .../shared/adjust-payment-dialog.component.ts | 110 ++++++++++++++++++ .../shared/adjust-payment.component.html | 19 --- .../shared/adjust-payment.component.ts | 90 -------------- .../billing/shared/billing-shared.module.ts | 4 +- .../shared/payment-method.component.html | 18 +-- .../shared/payment-method.component.ts | 28 +++-- 7 files changed, 155 insertions(+), 139 deletions(-) create mode 100644 apps/web/src/app/billing/shared/adjust-payment-dialog.component.html create mode 100644 apps/web/src/app/billing/shared/adjust-payment-dialog.component.ts delete mode 100644 apps/web/src/app/billing/shared/adjust-payment.component.html delete mode 100644 apps/web/src/app/billing/shared/adjust-payment.component.ts 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 @@ +<form [formGroup]="formGroup" [bitSubmit]="submit"> + <bit-dialog + dialogSize="large" + [title]="(currentType != null ? 'changePaymentMethod' : 'addPaymentMethod') | i18n" + > + <ng-container bitDialogContent> + <app-payment [hideBank]="!organizationId" [hideCredit]="true"></app-payment> + <app-tax-info (onCountryChanged)="changeCountry()"></app-tax-info> + </ng-container> + <ng-container bitDialogFooter> + <button type="submit" bitButton bitFormButton buttonType="primary"> + {{ "submit" | i18n }} + </button> + <button + type="button" + bitButton + bitFormButton + buttonType="secondary" + [bitDialogClose]="DialogResult.Cancelled" + > + {{ "cancel" | i18n }} + </button> + </ng-container> + </bit-dialog> +</form> 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<AdjustPaymentDialogData>, +) { + return dialogService.open<AdjustPaymentDialogResult>(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 @@ -<form #form class="card" (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate> - <div class="card-body"> - <button type="button" class="close" appA11yTitle="{{ 'cancel' | i18n }}" (click)="cancel()"> - <span aria-hidden="true">&times;</span> - </button> - <h3 class="card-body-header"> - {{ (currentType != null ? "changePaymentMethod" : "addPaymentMethod") | i18n }} - </h3> - <app-payment [hideBank]="!organizationId" [hideCredit]="true"></app-payment> - <app-tax-info (onCountryChanged)="changeCountry()"></app-tax-info> - <button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading"> - <i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i> - <span>{{ "submit" | i18n }}</span> - </button> - <button type="button" class="btn btn-outline-secondary" (click)="cancel()"> - {{ "cancel" | i18n }} - </button> - </div> -</form> 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<void>; - - 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/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 @@ <bit-container> <div class="tabbed-header" *ngIf="!organizationId"> - <!-- TODO: Organization and individual should use different "page" components --> + <!--TODO: Organization and individual should use different "page" components --> <h1>{{ "paymentMethod" | i18n }}</h1> </div> @@ -102,23 +102,9 @@ {{ paymentSource.description }} </p> </ng-container> - <button - type="button" - bitButton - buttonType="secondary" - (click)="changePayment()" - *ngIf="!showAdjustPayment" - > + <button type="button" bitButton buttonType="secondary" [bitAction]="changePayment"> {{ (paymentSource ? "changePaymentMethod" : "addPaymentMethod") | i18n }} </button> - <app-adjust-payment - [organizationId]="organizationId" - [currentType]="paymentSource != null ? paymentSource.type : null" - (onAdjusted)="closePayment(true)" - (onCanceled)="closePayment(false)" - *ngIf="showAdjustPayment" - > - </app-adjust-payment> <p *ngIf="isUnpaid">{{ "paymentChargedWithUnpaidSubscription" | i18n }}</p> <ng-container *ngIf="forOrganization"> <h2 class="spaced-header">{{ "taxInformation" | i18n }}</h2> 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) { From 5d3541dd6379f422f0f740c3702367b58c8898f8 Mon Sep 17 00:00:00 2001 From: vinith-kovan <156108204+vinith-kovan@users.noreply.github.com> Date: Wed, 17 Apr 2024 01:09:05 +0530 Subject: [PATCH 195/351] [PM-5013] migrate change plan component (#8407) * change plan component migration * change plan component migration --------- Co-authored-by: Todd Martin <106564991+trmartin4@users.noreply.github.com> --- .../organizations/change-plan.component.html | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) 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 @@ -<div class="card card-org-plans"> - <div class="card-body"> - <button type="button" class="close" appA11yTitle="{{ 'cancel' | i18n }}" (click)="cancel()"> - <span aria-hidden="true">&times;</span> - </button> - <h2 class="card-body-header">{{ "changeBillingPlan" | i18n }}</h2> - <p class="mb-0">{{ "changeBillingPlanUpgrade" | i18n }}</p> +<div + class="tw-relative tw-flex tw-flex-col tw-min-w-0 tw-rounded tw-border tw-border-solid tw-border-secondary-300" +> + <div class="tw-flex-auto tw-p-5"> + <button + bitIconButton="bwi-close" + buttonType="main" + type="button" + size="small" + class="tw-float-right" + appA11yTitle="{{ 'cancel' | i18n }}" + (click)="cancel()" + ></button> + <h2 bitTypography="h2">{{ "changeBillingPlan" | i18n }}</h2> + <p bitTypography="body1" class="tw-mb-0">{{ "changeBillingPlanUpgrade" | i18n }}</p> <app-organization-plans [showFree]="false" [showCancel]="true" From cf2fefaead36b7e3705fb28423e7ada075b2a687 Mon Sep 17 00:00:00 2001 From: vinith-kovan <156108204+vinith-kovan@users.noreply.github.com> Date: Wed, 17 Apr 2024 01:09:53 +0530 Subject: [PATCH 196/351] [PM-5021] billing history component migration (#8042) * billing history component migration * billing history component migration * billing history component migration * billing history component migration * billing history component migration --------- Co-authored-by: Todd Martin <106564991+trmartin4@users.noreply.github.com> --- .../billing-history-view.component.html | 11 +- .../shared/billing-history.component.html | 135 +++++++++--------- 2 files changed, 76 insertions(+), 70 deletions(-) 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 @@ -<div class="d-flex tabbed-header"> - <h1> +<div class="tw-flex tw-justify-between tw-mb-2 tw-pb-2 tw-mt-6"> + <h2 bitTypography="h2"> {{ "billingHistory" | i18n }} - </h1> + </h2> <button type="button" bitButton buttonType="secondary" (click)="load()" - class="tw-ml-auto" *ngIf="firstLoaded" [disabled]="loading" > @@ -17,11 +16,11 @@ </div> <ng-container *ngIf="!firstLoaded && loading"> <i - class="bwi bwi-spinner bwi-spin text-muted" + class="bwi bwi-spinner bwi-spin tw-text-muted" title="{{ 'loading' | i18n }}" aria-hidden="true" ></i> - <span class="sr-only">{{ "loading" | i18n }}</span> + <span class="tw-sr-only">{{ "loading" | i18n }}</span> </ng-container> <ng-container *ngIf="billing"> <app-billing-history [billing]="billing"></app-billing-history> 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 @@ -<h2 class="mt-3">{{ "invoices" | i18n }}</h2> -<p *ngIf="!invoices || !invoices.length">{{ "noInvoices" | i18n }}</p> -<table class="table mb-2" *ngIf="invoices && invoices.length"> - <tbody> - <tr *ngFor="let i of invoices"> - <td>{{ i.date | date: "mediumDate" }}</td> - <td> - <a - href="{{ i.pdfUrl }}" - target="_blank" - rel="noreferrer" - class="mr-2" - appA11yTitle="{{ 'downloadInvoice' | i18n }}" +<bit-section> + <h3 bitTypography="h3">{{ "invoices" | i18n }}</h3> + <p bitTypography="body1" *ngIf="!invoices || !invoices.length">{{ "noInvoices" | i18n }}</p> + <bit-table> + <ng-template body> + <tr bitRow *ngFor="let i of invoices"> + <td bitCell>{{ i.date | date: "mediumDate" }}</td> + <td bitCell> + <a + href="{{ i.pdfUrl }}" + target="_blank" + rel="noreferrer" + class="tw-mr-2" + appA11yTitle="{{ 'downloadInvoice' | i18n }}" + > + <i class="bwi bwi-file-pdf" aria-hidden="true"></i + ></a> + <a href="{{ i.url }}" target="_blank" rel="noreferrer" title="{{ 'viewInvoice' | i18n }}"> + {{ "invoiceNumber" | i18n: i.number }}</a + > + </td> + <td bitCell>{{ i.amount | currency: "$" }}</td> + <td bitCell> + <span *ngIf="i.paid"> + <i class="bwi bwi-check tw-text-success" aria-hidden="true"></i> + {{ "paid" | i18n }} + </span> + <span *ngIf="!i.paid"> + <i class="bwi bwi-exclamation-circle tw-text-muted" aria-hidden="true"></i> + {{ "unpaid" | i18n }} + </span> + </td> + </tr> + </ng-template> + </bit-table> +</bit-section> +<bit-section> + <h3 bitTypography="h3">{{ "transactions" | i18n }}</h3> + <p bitTypography="body1" *ngIf="!transactions || !transactions.length"> + {{ "noTransactions" | i18n }} + </p> + <bit-table *ngIf="transactions && transactions.length"> + <ng-template body> + <tr bitRow *ngFor="let t of transactions"> + <td bitCell>{{ t.createdDate | date: "mediumDate" }}</td> + <td bitCell> + <span *ngIf="t.type === transactionType.Charge || t.type === transactionType.Credit"> + {{ "chargeNoun" | i18n }} + </span> + <span *ngIf="t.type === transactionType.Refund">{{ "refundNoun" | i18n }}</span> + </td> + <td bitCell> + <i + class="bwi bwi-fw" + *ngIf="t.paymentMethodType" + aria-hidden="true" + [ngClass]="paymentMethodClasses(t.paymentMethodType)" + ></i> + {{ t.details }} + </td> + <td + [ngClass]="{ 'text-strike': t.refunded }" + title="{{ (t.refunded ? 'refunded' : '') | i18n }}" + bitCell > - <i class="bwi bwi-file-pdf" aria-hidden="true"></i - ></a> - <a href="{{ i.url }}" target="_blank" rel="noreferrer" title="{{ 'viewInvoice' | i18n }}"> - {{ "invoiceNumber" | i18n: i.number }}</a - > - </td> - <td>{{ i.amount | currency: "$" }}</td> - <td> - <span *ngIf="i.paid"> - <i class="bwi bwi-check text-success" aria-hidden="true"></i> - {{ "paid" | i18n }} - </span> - <span *ngIf="!i.paid"> - <i class="bwi bwi-exclamation-circle text-muted" aria-hidden="true"></i> - {{ "unpaid" | i18n }} - </span> - </td> - </tr> - </tbody> -</table> -<h2 class="spaced-header">{{ "transactions" | i18n }}</h2> -<p *ngIf="!transactions || !transactions.length">{{ "noTransactions" | i18n }}</p> -<table class="table mb-2" *ngIf="transactions && transactions.length"> - <tbody> - <tr *ngFor="let t of transactions"> - <td>{{ t.createdDate | date: "mediumDate" }}</td> - <td> - <span *ngIf="t.type === transactionType.Charge || t.type === transactionType.Credit"> - {{ "chargeNoun" | i18n }} - </span> - <span *ngIf="t.type === transactionType.Refund">{{ "refundNoun" | i18n }}</span> - </td> - <td> - <i - class="bwi bwi-fw" - *ngIf="t.paymentMethodType" - aria-hidden="true" - [ngClass]="paymentMethodClasses(t.paymentMethodType)" - ></i> - {{ t.details }} - </td> - <td - [ngClass]="{ 'text-strike': t.refunded }" - title="{{ (t.refunded ? 'refunded' : '') | i18n }}" - > - {{ t.amount | currency: "$" }} - </td> - </tr> - </tbody> -</table> -<small class="text-muted">* {{ "chargesStatement" | i18n: "BITWARDEN" }}</small> + {{ t.amount | currency: "$" }} + </td> + </tr> + </ng-template> + </bit-table> + <small class="tw-text-muted">* {{ "chargesStatement" | i18n: "BITWARDEN" }}</small> +</bit-section> From 9ecf384176c21fb43117a5b324e37f9d218451f0 Mon Sep 17 00:00:00 2001 From: KiruthigaManivannan <162679756+KiruthigaManivannan@users.noreply.github.com> Date: Wed, 17 Apr 2024 01:10:10 +0530 Subject: [PATCH 197/351] [PM-5020] Adjust Storage component migration (#8301) * Migrated Add Storage component * PM-5020 Addressed review comments for Adjust Storage component * PM-5020 Changes done in dialog css * PM-5020 Latest review comments addressed * PM-5020 Add storage submit action changes done * PM-5020 Moved the paragraph to top of dialog content --- .../user-subscription.component.html | 13 +- .../individual/user-subscription.component.ts | 32 ++-- ...nization-subscription-cloud.component.html | 25 +-- ...ganization-subscription-cloud.component.ts | 35 ++-- .../shared/adjust-storage.component.html | 72 ++++---- .../shared/adjust-storage.component.ts | 163 ++++++++++-------- 6 files changed, 180 insertions(+), 160 deletions(-) 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 @@ </div> <ng-container *ngIf="subscription && !subscription.cancelled && !subscriptionMarkedForCancel"> <div class="mt-3"> - <div class="d-flex" *ngIf="!showAdjustStorage"> - <button bitButton type="button" buttonType="secondary" (click)="adjustStorage(true)"> + <div class="d-flex"> + <button bitButton type="button" buttonType="secondary" [bitAction]="adjustStorage(true)"> {{ "addStorage" | i18n }} </button> <button @@ -179,18 +179,11 @@ type="button" buttonType="secondary" class="tw-ml-1" - (click)="adjustStorage(false)" + [bitAction]="adjustStorage(false)" > {{ "removeStorage" | i18n }} </button> </div> - <app-adjust-storage - [storageGbPrice]="4" - [add]="adjustStorageAdd" - (onAdjusted)="closeStorage(true)" - (onCanceled)="closeStorage(false)" - *ngIf="showAdjustStorage" - ></app-adjust-storage> </div> </ng-container> </ng-container> 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/organization-subscription-cloud.component.html b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html index b4fac65854..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 @@ -175,23 +175,24 @@ <bit-progress [barWidth]="storagePercentage" bgColor="success"></bit-progress> <ng-container *ngIf="subscription && !subscription.cancelled && !subscriptionMarkedForCancel"> <div class="tw-mt-3"> - <div class="tw-flex tw-space-x-2" *ngIf="!showAdjustStorage"> - <button bitButton buttonType="secondary" type="button" (click)="adjustStorage(true)"> + <div class="tw-flex tw-space-x-2"> + <button + bitButton + buttonType="secondary" + type="button" + [bitAction]="adjustStorage(true)" + > {{ "addStorage" | i18n }} </button> - <button bitButton buttonType="secondary" type="button" (click)="adjustStorage(false)"> + <button + bitButton + buttonType="secondary" + type="button" + [bitAction]="adjustStorage(false)" + > {{ "removeStorage" | i18n }} </button> </div> - <app-adjust-storage - [storageGbPrice]="storageGbPrice" - [add]="adjustStorageAdd" - [organizationId]="organizationId" - [interval]="billingInterval" - (onAdjusted)="closeStorage(true)" - (onCanceled)="closeStorage(false)" - *ngIf="showAdjustStorage" - ></app-adjust-storage> </div> </ng-container> <ng-container *ngIf="showAdjustSecretsManager"> 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 2173d4c0ca..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 @@ -18,6 +18,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, @@ -36,8 +40,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy userOrg: Organization; showChangePlan = false; showDownloadLicense = false; - adjustStorageAdd = true; - showAdjustStorage = false; hasBillingSyncToken: boolean; showAdjustSecretsManager = false; showSecretsManagerSubscribe = false; @@ -361,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/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 @@ -<form #form class="card" (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate> - <div class="card-body"> - <button type="button" class="close" appA11yTitle="{{ 'cancel' | i18n }}" (click)="cancel()"> - <span aria-hidden="true">&times;</span> - </button> - <h3 class="card-body-header">{{ (add ? "addStorage" : "removeStorage") | i18n }}</h3> - <div class="row"> - <div class="form-group col-6"> - <label for="storageAdjustment">{{ - (add ? "gbStorageAdd" : "gbStorageRemove") | i18n - }}</label> - <input - id="storageAdjustment" - class="form-control" - type="number" - name="StorageGbAdjustment" - [(ngModel)]="storageAdjustment" - min="0" - max="99" - step="1" - required - /> +<form [formGroup]="formGroup" [bitSubmit]="submit"> + <bit-dialog dialogSize="default" [title]="(add ? 'addStorage' : 'removeStorage') | i18n"> + <ng-container bitDialogContent> + <p bitTypography="body1">{{ (add ? "storageAddNote" : "storageRemoveNote") | i18n }}</p> + <div class="tw-grid tw-grid-cols-12"> + <bit-form-field class="tw-col-span-7"> + <bit-label>{{ (add ? "gbStorageAdd" : "gbStorageRemove") | i18n }}</bit-label> + <input bitInput type="number" formControlName="storageAdjustment" /> + <bit-hint *ngIf="add"> + <strong>{{ "total" | i18n }}:</strong> + {{ formGroup.get("storageAdjustment").value || 0 }} GB &times; + {{ storageGbPrice | currency: "$" }} = {{ adjustedStorageTotal | currency: "$" }} /{{ + interval | i18n + }} + </bit-hint> + </bit-form-field> </div> - </div> - <div *ngIf="add" class="mb-3"> - <strong>{{ "total" | i18n }}:</strong> {{ storageAdjustment || 0 }} GB &times; - {{ storageGbPrice | currency: "$" }} = {{ adjustedStorageTotal | currency: "$" }} /{{ - interval | i18n - }} - </div> - <button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading"> - <i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i> - <span>{{ "submit" | i18n }}</span> - </button> - <button type="button" class="btn btn-outline-secondary" (click)="cancel()"> - {{ "cancel" | i18n }} - </button> - <small class="d-block text-muted mt-3"> - {{ (add ? "storageAddNote" : "storageRemoveNote") | i18n }} - </small> - </div> + </ng-container> + <ng-container bitDialogFooter> + <button type="submit" bitButton bitFormButton buttonType="primary"> + {{ "submit" | i18n }} + </button> + <button + type="button" + bitButton + bitFormButton + buttonType="secondary" + [bitDialogClose]="DialogResult.Cancelled" + > + {{ "cancel" | i18n }} + </button> + </ng-container> + </bit-dialog> </form> <app-payment [showMethods]="false"></app-payment> 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<number>(); - @Output() onCanceled = new EventEmitter(); + storageGbPrice: number; + add: boolean; + organizationId: string; + interval: string; @ViewChild(PaymentComponent, { static: true }) paymentComponent: PaymentComponent; - storageAdjustment = 0; - formPromise: Promise<PaymentResponse | void>; + 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<PaymentResponse>; - 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<PaymentResponse>; + 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<AdjustStorageDialogData>, +) { + return dialogService.open<AdjustStorageDialogResult>(AdjustStorageComponent, config); +} From 65f1bd2e3a258a42dc2a956e0f936452d610d268 Mon Sep 17 00:00:00 2001 From: Cesar Gonzalez <cesar.a.gonzalezcs@gmail.com> Date: Tue, 16 Apr 2024 16:35:53 -0500 Subject: [PATCH 198/351] [PM-7527] Get MV3 build artifacts in main branch with clear messaging that that the build is not to be released (#8771) * [PM-7527] Get MV3 build artifacts in main branch with clear messaging that that the build is not to be released * Update .github/workflows/build-browser.yml Co-authored-by: Vince Grassia <593223+vgrassia@users.noreply.github.com> --------- Co-authored-by: Vince Grassia <593223+vgrassia@users.noreply.github.com> --- .github/workflows/build-browser.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-browser.yml b/.github/workflows/build-browser.yml index 585a888ae1..4fb72a47de 100644 --- a/.github/workflows/build-browser.yml +++ b/.github/workflows/build-browser.yml @@ -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 From f45eec1a4f42329efc909f54778b559f7f1ae4fc Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Wed, 17 Apr 2024 09:31:48 +1000 Subject: [PATCH 199/351] [AC-2169] Group modal - limit admin access - members tab (#8650) * Restrict user from adding themselves to existing group --- .../manage/group-add-edit.component.html | 7 +- .../manage/group-add-edit.component.ts | 118 +++++++++++++----- apps/web/src/locales/en/messages.json | 3 + libs/common/src/abstractions/api.service.ts | 1 - libs/common/src/services/api.service.ts | 10 -- 5 files changed, 98 insertions(+), 41 deletions(-) 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 @@ </bit-tab> <bit-tab label="{{ 'members' | i18n }}"> - <p>{{ "editGroupMembersDesc" | i18n }}</p> + <p> + {{ "editGroupMembersDesc" | i18n }} + <span *ngIf="restrictGroupAccess$ | async"> + {{ "restrictedGroupAccessDesc" | i18n }} + </span> + </p> <bit-access-selector formControlName="members" [items]="members" diff --git a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts index 6d0f8e381f..dea6f4999b 100644 --- a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts @@ -1,15 +1,31 @@ import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; import { ChangeDetectorRef, Component, Inject, OnDestroy, OnInit } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; -import { catchError, combineLatest, from, map, of, Subject, switchMap, takeUntil } from "rxjs"; +import { + catchError, + combineLatest, + concatMap, + from, + map, + Observable, + of, + shareReplay, + Subject, + switchMap, + takeUntil, +} from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.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 { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; +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 { UserId } from "@bitwarden/common/types/guid"; import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; import { CollectionData } from "@bitwarden/common/vault/models/data/collection.data"; import { Collection } from "@bitwarden/common/vault/models/domain/collection"; @@ -88,10 +104,9 @@ export class GroupAddEditComponent implements OnInit, OnDestroy { tabIndex: GroupAddEditTabType; loading = true; - editMode = false; title: string; collections: AccessItemView[] = []; - members: AccessItemView[] = []; + members: Array<AccessItemView & { userId: UserId }> = []; 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<void>(); private get orgCollections$() { @@ -134,7 +153,7 @@ export class GroupAddEditComponent implements OnInit, OnDestroy { ); } - private get orgMembers$() { + private get orgMembers$(): Observable<Array<AccessItemView & { userId: UserId }>> { 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<GroupView | undefined> = 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/locales/en/messages.json b/apps/web/src/locales/en/messages.json index f28bff066a..a3fc234cab 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -7905,5 +7905,8 @@ }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." } } diff --git a/libs/common/src/abstractions/api.service.ts b/libs/common/src/abstractions/api.service.ts index 6962a44268..44a58403ed 100644 --- a/libs/common/src/abstractions/api.service.ts +++ b/libs/common/src/abstractions/api.service.ts @@ -297,7 +297,6 @@ export abstract class ApiService { ) => Promise<any>; getGroupUsers: (organizationId: string, id: string) => Promise<string[]>; - putGroupUsers: (organizationId: string, id: string, request: string[]) => Promise<any>; deleteGroupUser: (organizationId: string, id: string, organizationUserId: string) => Promise<any>; getSync: () => Promise<SyncResponse>; diff --git a/libs/common/src/services/api.service.ts b/libs/common/src/services/api.service.ts index c7a8f3f091..90671c4f04 100644 --- a/libs/common/src/services/api.service.ts +++ b/libs/common/src/services/api.service.ts @@ -866,16 +866,6 @@ export class ApiService implements ApiServiceAbstraction { return r; } - async putGroupUsers(organizationId: string, id: string, request: string[]): Promise<any> { - await this.send( - "PUT", - "/organizations/" + organizationId + "/groups/" + id + "/users", - request, - true, - false, - ); - } - deleteGroupUser(organizationId: string, id: string, organizationUserId: string): Promise<any> { return this.send( "DELETE", From 4db383850fd9f11ec531c18ee2118de8830a21e3 Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Wed, 17 Apr 2024 11:03:48 +1000 Subject: [PATCH 200/351] [AC-2172] Member modal - limit admin access (#8343) * limit admin permissions to assign members to collections that the admin doesn't have can manage permissions for --- .../member-dialog/member-dialog.component.ts | 97 ++++++++++++++----- .../vault/core/collection-admin.service.ts | 3 + .../models/response/collection.response.ts | 11 +-- 3 files changed, 81 insertions(+), 30 deletions(-) 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 752122de00..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 @@ -31,6 +31,7 @@ import { CollectionView } from "@bitwarden/common/vault/models/view/collection.v 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, @@ -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<AccessItemView>((g) => mapGroupToAccessItemView(g)), + ); - this.groupAccessItems = [].concat( - groups.map<AccessItemView>((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<AccessItemValue>((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<AccessItemValue>((selection) => ({ + id: selection.id, + type: AccessItemType.Collection, + permission: convertToPermission(selection), + })) ); } diff --git a/apps/web/src/app/vault/core/collection-admin.service.ts b/apps/web/src/app/vault/core/collection-admin.service.ts index 74f825e1ac..7f78ab214a 100644 --- a/apps/web/src/app/vault/core/collection-admin.service.ts +++ b/apps/web/src/app/vault/core/collection-admin.service.ts @@ -124,6 +124,9 @@ export class CollectionAdminService { view.groups = c.groups; view.users = c.users; view.assigned = c.assigned; + view.readOnly = c.readOnly; + view.hidePasswords = c.hidePasswords; + view.manage = c.manage; } return view; 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; From a72b7f3d214f6fc5d2b7b09e65a55c2fc3023521 Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Wed, 17 Apr 2024 14:07:26 +0100 Subject: [PATCH 201/351] [AC-1218] Add ability to delete Provider Portals (#8685) * initial commit * add changes from running prettier * resolve the linx issue * resolve the lint issue * resolving lint error * correct the redirect issue * resolve pr commit * Add a feature flag * move the new component to adminconsole * resolve some pr comments * move the endpoint from ApiService to providerApiService * move provider endpoints to the provider-api class * change the header * resolve some pr comments --- apps/cli/src/bw.ts | 5 + ...ify-recover-delete-provider.component.html | 34 ++++++ ...erify-recover-delete-provider.component.ts | 61 ++++++++++ .../organization-plans.component.ts | 4 +- apps/web/src/app/oss-routing.module.ts | 7 ++ .../src/app/shared/loose-components.module.ts | 3 + apps/web/src/locales/en/messages.json | 36 ++++++ .../providers/providers.module.ts | 2 + .../providers/settings/account.component.html | 105 ++++++++++-------- .../providers/settings/account.component.ts | 73 +++++++++++- .../providers/setup/setup.component.ts | 8 +- .../src/services/jslib-services.module.ts | 7 ++ libs/common/src/abstractions/api.service.ts | 7 -- .../provider-api.service.abstraction.ts | 15 +++ .../provider-verify-recover-delete.request.ts | 7 ++ .../services/provider/provider-api.service.ts | 47 ++++++++ libs/common/src/enums/feature-flag.enum.ts | 1 + libs/common/src/services/api.service.ts | 20 ---- 18 files changed, 359 insertions(+), 83 deletions(-) create mode 100644 apps/web/src/app/admin-console/providers/verify-recover-delete-provider.component.html create mode 100644 apps/web/src/app/admin-console/providers/verify-recover-delete-provider.component.ts create mode 100644 libs/common/src/admin-console/abstractions/provider/provider-api.service.abstraction.ts create mode 100644 libs/common/src/admin-console/models/request/provider/provider-verify-recover-delete.request.ts create mode 100644 libs/common/src/admin-console/services/provider/provider-api.service.ts diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index f02d7da49c..7fbefc10e3 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -18,11 +18,13 @@ 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"; @@ -232,6 +234,7 @@ export class Main { stateEventRunnerService: StateEventRunnerService; biometricStateService: BiometricStateService; billingAccountProfileStateService: BillingAccountProfileStateService; + providerApiService: ProviderApiServiceAbstraction; constructor() { let p = null; @@ -692,6 +695,8 @@ export class Main { this.eventUploadService, this.authService, ); + + this.providerApiService = new ProviderApiService(this.apiService); } async run() { 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 @@ +<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" class="container" ngNativeValidate> + <div class="row justify-content-md-center mt-5"> + <div class="col-5"> + <p class="lead text-center mb-4">{{ "deleteProvider" | i18n }}</p> + <div class="card"> + <div class="card-body"> + <app-callout type="warning">{{ "deleteProviderWarning" | i18n }}</app-callout> + <p class="text-center"> + <strong>{{ name }}</strong> + </p> + <p>{{ "deleteProviderRecoverConfirmDesc" | i18n }}</p> + <hr /> + <div class="d-flex"> + <button + type="submit" + class="btn btn-danger btn-block btn-submit" + [disabled]="form.loading" + > + <span>{{ "deleteProvider" | i18n }}</span> + <i + class="bwi bwi-spinner bwi-spin" + title="{{ 'loading' | i18n }}" + aria-hidden="true" + ></i> + </button> + <a routerLink="/login" class="btn btn-outline-secondary btn-block ml-2 mt-0"> + {{ "cancel" | i18n }} + </a> + </div> + </div> + </div> + </div> + </div> +</form> 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<any>; + + 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/billing/organizations/organization-plans.component.ts b/apps/web/src/app/billing/organizations/organization-plans.component.ts index f2fb296522..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"; @@ -147,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(); } @@ -182,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, ); diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index e5c2f353c0..066ed5db10 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -13,6 +13,7 @@ import { flagEnabled, Flags } from "../utils/flags"; 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"; @@ -156,6 +157,12 @@ const routes: Routes = [ canActivate: [UnauthGuard], data: { titleId: "deleteAccount" }, }, + { + 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/shared/loose-components.module.ts b/apps/web/src/app/shared/loose-components.module.ts index 586f207962..8f6a1eaedc 100644 --- a/apps/web/src/app/shared/loose-components.module.ts +++ b/apps/web/src/app/shared/loose-components.module.ts @@ -13,6 +13,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"; @@ -184,6 +185,7 @@ import { SharedModule } from "./shared.module"; VerifyEmailComponent, VerifyEmailTokenComponent, VerifyRecoverDeleteComponent, + VerifyRecoverDeleteProviderComponent, LowKdfComponent, ], exports: [ @@ -261,6 +263,7 @@ import { SharedModule } from "./shared.module"; VerifyEmailComponent, VerifyEmailTokenComponent, VerifyRecoverDeleteComponent, + VerifyRecoverDeleteProviderComponent, LowKdfComponent, HeaderModule, DangerZoneComponent, diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index a3fc234cab..c8dfa14c8b 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -7908,5 +7908,41 @@ }, "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." } } diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts index 81cc7c2919..0d75973712 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts @@ -8,6 +8,7 @@ import { OrganizationPlansComponent, TaxInfoComponent } from "@bitwarden/web-vau import { PaymentMethodWarningsModule } from "@bitwarden/web-vault/app/billing/shared"; import { OssModule } from "@bitwarden/web-vault/app/oss.module"; +import { DangerZoneComponent } from "../../../../../../apps/web/src/app/auth/settings/account/danger-zone.component"; import { ManageClientOrganizationSubscriptionComponent } from "../../billing/providers/clients/manage-client-organization-subscription.component"; import { ManageClientOrganizationsComponent } from "../../billing/providers/clients/manage-client-organizations.component"; @@ -40,6 +41,7 @@ import { SetupComponent } from "./setup/setup.component"; ProvidersLayoutComponent, PaymentMethodWarningsModule, TaxInfoComponent, + DangerZoneComponent, ], declarations: [ AcceptProviderComponent, diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.html index ea634e5ebc..10f6d14425 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.html @@ -1,51 +1,58 @@ <app-header></app-header> - -<div *ngIf="loading"> - <i - class="bwi bwi-spinner bwi-spin text-muted" - title="{{ 'loading' | i18n }}" - aria-hidden="true" - ></i> - <span class="sr-only">{{ "loading" | i18n }}</span> -</div> -<form - *ngIf="provider && !loading" - #form - (ngSubmit)="submit()" - [appApiAction]="formPromise" - ngNativeValidate -> - <div class="row"> - <div class="col-6"> - <div class="form-group"> - <label for="name">{{ "providerName" | i18n }}</label> - <input - id="name" - class="form-control" - type="text" - name="Name" - [(ngModel)]="provider.name" - [disabled]="selfHosted" - /> - </div> - <div class="form-group"> - <label for="billingEmail">{{ "billingEmail" | i18n }}</label> - <input - id="billingEmail" - class="form-control" - type="text" - name="BillingEmail" - [(ngModel)]="provider.billingEmail" - [disabled]="selfHosted" - /> - </div> - </div> - <div class="col-6"> - <bit-avatar [text]="provider.name" [id]="provider.id" size="large"></bit-avatar> - </div> +<bit-container> + <div *ngIf="loading"> + <i + class="bwi bwi-spinner bwi-spin text-muted" + title="{{ 'loading' | i18n }}" + aria-hidden="true" + ></i> + <span class="sr-only">{{ "loading" | i18n }}</span> </div> - <button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading"> - <i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i> - <span>{{ "save" | i18n }}</span> - </button> -</form> + <form + *ngIf="provider && !loading" + #form + (ngSubmit)="submit()" + [appApiAction]="formPromise" + ngNativeValidate + > + <div class="row"> + <div class="col-6"> + <div class="form-group"> + <label for="name">{{ "providerName" | i18n }}</label> + <input + id="name" + class="form-control" + type="text" + name="Name" + [(ngModel)]="provider.name" + [disabled]="selfHosted" + /> + </div> + <div class="form-group"> + <label for="billingEmail">{{ "billingEmail" | i18n }}</label> + <input + id="billingEmail" + class="form-control" + type="text" + name="BillingEmail" + [(ngModel)]="provider.billingEmail" + [disabled]="selfHosted" + /> + </div> + </div> + <div class="col-6"> + <bit-avatar [text]="provider.name" [id]="provider.id" size="large"></bit-avatar> + </div> + </div> + <button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading"> + <i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i> + <span>{{ "save" | i18n }}</span> + </button> + </form> + + <app-danger-zone *ngIf="enableDeleteProvider$ | async"> + <button type="button" bitButton buttonType="danger" (click)="deleteProvider()"> + {{ "deleteProvider" | i18n }} + </button> + </app-danger-zone> +</bit-container> diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts index 079e68fe21..83038d1bfc 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts @@ -1,13 +1,18 @@ import { Component } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; +import { UserVerificationDialogComponent } from "@bitwarden/auth/angular"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; import { ProviderUpdateRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-update.request"; import { ProviderResponse } from "@bitwarden/common/admin-console/models/response/provider/provider.response"; +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 { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { DialogService } from "@bitwarden/components"; @Component({ selector: "provider-account", @@ -23,6 +28,11 @@ export class AccountComponent { private providerId: string; + protected enableDeleteProvider$ = this.configService.getFeatureFlag$( + FeatureFlag.EnableDeleteProvider, + false, + ); + constructor( private apiService: ApiService, private i18nService: I18nService, @@ -30,6 +40,9 @@ export class AccountComponent { private syncService: SyncService, private platformUtilsService: PlatformUtilsService, private logService: LogService, + private dialogService: DialogService, + private configService: ConfigService, + private providerApiService: ProviderApiServiceAbstraction, ) {} async ngOnInit() { @@ -38,7 +51,7 @@ export class AccountComponent { this.route.parent.parent.params.subscribe(async (params) => { this.providerId = params.providerId; try { - this.provider = await this.apiService.getProvider(this.providerId); + this.provider = await this.providerApiService.getProvider(this.providerId); } catch (e) { this.logService.error(`Handled exception: ${e}`); } @@ -53,7 +66,7 @@ export class AccountComponent { request.businessName = this.provider.businessName; request.billingEmail = this.provider.billingEmail; - this.formPromise = this.apiService.putProvider(this.providerId, request).then(() => { + this.formPromise = this.providerApiService.putProvider(this.providerId, request).then(() => { return this.syncService.fullSync(true); }); await this.formPromise; @@ -62,4 +75,60 @@ export class AccountComponent { this.logService.error(`Handled exception: ${e}`); } } + + async deleteProvider() { + const providerClients = await this.apiService.getProviderClients(this.providerId); + if (providerClients.data != null && providerClients.data.length > 0) { + await this.dialogService.openSimpleDialog({ + title: { key: "deleteProviderName", placeholders: [this.provider.name] }, + content: { key: "deleteProviderWarningDesc", placeholders: [this.provider.name] }, + acceptButtonText: { key: "ok" }, + type: "danger", + }); + + return false; + } + + const userVerified = await this.verifyUser(); + if (!userVerified) { + return; + } + + this.formPromise = this.providerApiService.deleteProvider(this.providerId); + try { + await this.formPromise; + this.platformUtilsService.showToast( + "success", + this.i18nService.t("providerDeleted"), + this.i18nService.t("providerDeletedDesc"), + ); + } catch (e) { + this.logService.error(e); + } + this.formPromise = null; + } + + private async verifyUser(): Promise<boolean> { + const confirmDescription = "deleteProviderConfirmation"; + const result = await UserVerificationDialogComponent.open(this.dialogService, { + title: "deleteProvider", + bodyText: confirmDescription, + confirmButtonOptions: { + text: "deleteProvider", + type: "danger", + }, + }); + + // Handle the result of the dialog based on user action and verification success + if (result.userAction === "cancel") { + // User cancelled the dialog + return false; + } + + // User confirmed the dialog so check verification success + if (!result.verificationSuccess) { + return false; + } + return true; + } } 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 ed7b42c959..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 @@ -3,7 +3,7 @@ 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"; @@ -50,10 +50,10 @@ export class SetupComponent implements OnInit { 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() { @@ -80,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 @@ -128,7 +128,7 @@ export class SetupComponent implements OnInit { } } - const provider = await this.apiService.postProviderSetup(this.providerId, request); + 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/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index dbb94f6753..ad0881a4b3 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -38,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"; @@ -47,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 { @@ -1115,6 +1117,11 @@ const safeProviders: SafeProvider[] = [ useClass: LoggingErrorHandler, deps: [], }), + safeProvider({ + provide: ProviderApiServiceAbstraction, + useClass: ProviderApiService, + deps: [ApiServiceAbstraction], + }), ]; function encryptServiceFactory( diff --git a/libs/common/src/abstractions/api.service.ts b/libs/common/src/abstractions/api.service.ts index 44a58403ed..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"; @@ -372,10 +369,6 @@ export abstract class ApiService { getPlans: () => Promise<ListResponse<PlanResponse>>; getTaxRates: () => Promise<ListResponse<TaxRateResponse>>; - postProviderSetup: (id: string, request: ProviderSetupRequest) => Promise<ProviderResponse>; - getProvider: (id: string) => Promise<ProviderResponse>; - putProvider: (id: string, request: ProviderUpdateRequest) => Promise<ProviderResponse>; - getProviderUsers: (providerId: string) => Promise<ListResponse<ProviderUserUserDetailsResponse>>; getProviderUser: (providerId: string, id: string) => Promise<ProviderUserResponse>; postProviderUserInvite: (providerId: string, request: ProviderUserInviteRequest) => Promise<any>; 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<ProviderResponse>; + getProvider: (id: string) => Promise<ProviderResponse>; + putProvider: (id: string, request: ProviderUpdateRequest) => Promise<ProviderResponse>; + providerRecoverDeleteToken: ( + organizationId: string, + request: ProviderVerifyRecoverDeleteRequest, + ) => Promise<any>; + deleteProvider: (id: string) => Promise<void>; +} 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/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<any> { + return this.apiService.send( + "POST", + "/providers/" + providerId + "/delete-recover-token", + request, + false, + false, + ); + } + + async deleteProvider(id: string): Promise<void> { + await this.apiService.send("DELETE", "/providers/" + id, null, true, false); + } +} diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index b937e6c462..636e9bc4ce 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -10,6 +10,7 @@ export enum FeatureFlag { 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/services/api.service.ts b/libs/common/src/services/api.service.ts index 90671c4f04..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"; @@ -1151,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( From 0c557c6ab8e606a73219a053bee56481e5ccb39d Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Wed, 17 Apr 2024 09:13:05 -0500 Subject: [PATCH 202/351] Guard Background Only and MV2 Only Actions (#8773) --- .../browser/src/background/main.background.ts | 206 +++++++++--------- .../src/popup/services/services.module.ts | 2 +- 2 files changed, 104 insertions(+), 104 deletions(-) diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 642510b4de..69ed4cfa3d 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -342,11 +342,11 @@ export default class MainBackground { private syncTimeout: any; private isSafari: boolean; private nativeMessagingBackground: NativeMessagingBackground; - popupOnlyContext: boolean; - - constructor(public isPrivateMode: boolean = false) { - this.popupOnlyContext = isPrivateMode || BrowserApi.isManifestVersion(3); + constructor( + public isPrivateMode: boolean = false, + public popupOnlyContext: boolean = false, + ) { // Services const lockedCallback = async (userId?: string) => { if (this.notificationsService != null) { @@ -889,82 +889,83 @@ export default class MainBackground { this.isSafari = this.platformUtilsService.isSafari(); // Background - this.runtimeBackground = new RuntimeBackground( - this, - this.autofillService, - this.platformUtilsService as BrowserPlatformUtilsService, - this.i18nService, - this.notificationsService, - this.stateService, - this.autofillSettingsService, - this.systemService, - this.environmentService, - this.messagingService, - this.logService, - this.configService, - this.fido2Service, - ); - this.nativeMessagingBackground = new NativeMessagingBackground( - this.accountService, - this.masterPasswordService, - this.cryptoService, - this.cryptoFunctionService, - this.runtimeBackground, - this.messagingService, - this.appIdService, - this.platformUtilsService, - this.stateService, - this.logService, - this.authService, - this.biometricStateService, - ); - this.commandsBackground = new CommandsBackground( - this, - this.passwordGenerationService, - this.platformUtilsService, - this.vaultTimeoutService, - this.authService, - ); - this.notificationBackground = new NotificationBackground( - this.autofillService, - this.cipherService, - this.authService, - this.policyService, - this.folderService, - this.stateService, - this.userNotificationSettingsService, - this.domainSettingsService, - this.environmentService, - this.logService, - themeStateService, - this.configService, - ); - this.overlayBackground = new OverlayBackground( - this.cipherService, - this.autofillService, - this.authService, - this.environmentService, - this.domainSettingsService, - this.stateService, - this.autofillSettingsService, - this.i18nService, - this.platformUtilsService, - themeStateService, - ); - this.filelessImporterBackground = new FilelessImporterBackground( - this.configService, - this.authService, - this.policyService, - this.notificationBackground, - this.importService, - this.syncService, - ); - this.tabsBackground = new TabsBackground( - this, - this.notificationBackground, - this.overlayBackground, - ); if (!this.popupOnlyContext) { + this.runtimeBackground = new RuntimeBackground( + this, + this.autofillService, + this.platformUtilsService as BrowserPlatformUtilsService, + this.i18nService, + this.notificationsService, + this.stateService, + this.autofillSettingsService, + this.systemService, + this.environmentService, + this.messagingService, + this.logService, + this.configService, + this.fido2Service, + ); + this.nativeMessagingBackground = new NativeMessagingBackground( + this.accountService, + this.masterPasswordService, + this.cryptoService, + this.cryptoFunctionService, + this.runtimeBackground, + this.messagingService, + this.appIdService, + this.platformUtilsService, + this.stateService, + this.logService, + this.authService, + this.biometricStateService, + ); + this.commandsBackground = new CommandsBackground( + this, + this.passwordGenerationService, + this.platformUtilsService, + this.vaultTimeoutService, + this.authService, + ); + this.notificationBackground = new NotificationBackground( + this.autofillService, + this.cipherService, + this.authService, + this.policyService, + this.folderService, + this.stateService, + this.userNotificationSettingsService, + this.domainSettingsService, + this.environmentService, + this.logService, + themeStateService, + this.configService, + ); + this.overlayBackground = new OverlayBackground( + this.cipherService, + this.autofillService, + this.authService, + this.environmentService, + this.domainSettingsService, + this.stateService, + this.autofillSettingsService, + this.i18nService, + this.platformUtilsService, + themeStateService, + ); + this.filelessImporterBackground = new FilelessImporterBackground( + this.configService, + this.authService, + this.policyService, + this.notificationBackground, + this.importService, + this.syncService, + ); + this.tabsBackground = new TabsBackground( + this, + this.notificationBackground, + this.overlayBackground, + ); + const contextMenuClickedHandler = new ContextMenuClickedHandler( (options) => this.platformUtilsService.copyToClipboard(options.text), async (_tab) => { @@ -1006,11 +1007,6 @@ export default class MainBackground { this.notificationsService, this.accountService, ); - this.webRequestBackground = new WebRequestBackground( - this.platformUtilsService, - this.cipherService, - this.authService, - ); this.usernameGenerationService = new UsernameGenerationService( this.cryptoService, @@ -1032,34 +1028,40 @@ export default class MainBackground { this.authService, this.cipherService, ); + + if (BrowserApi.isManifestVersion(2)) { + this.webRequestBackground = new WebRequestBackground( + this.platformUtilsService, + this.cipherService, + this.authService, + ); + } } } async bootstrap() { this.containerService.attachToGlobal(self); - await this.stateService.init(); + await this.stateService.init({ runMigrations: !this.isPrivateMode }); - await this.vaultTimeoutService.init(true); await (this.i18nService as I18nService).init(); - await (this.eventUploadService as EventUploadService).init(true); - await this.runtimeBackground.init(); - await this.notificationBackground.init(); - this.filelessImporterBackground.init(); - await this.commandsBackground.init(); - + (this.eventUploadService as EventUploadService).init(true); this.twoFactorService.init(); - await this.overlayBackground.init(); - - await this.tabsBackground.init(); if (!this.popupOnlyContext) { + await this.vaultTimeoutService.init(true); + await this.runtimeBackground.init(); + await this.notificationBackground.init(); + this.filelessImporterBackground.init(); + await this.commandsBackground.init(); + await this.overlayBackground.init(); + await this.tabsBackground.init(); this.contextMenusBackground?.init(); + await this.idleBackground.init(); + if (BrowserApi.isManifestVersion(2)) { + await this.webRequestBackground.init(); + } } - await this.idleBackground.init(); - await this.webRequestBackground.init(); - - await this.fido2Service.init(); if (this.platformUtilsService.isFirefox() && !this.isPrivateMode) { // Set Private Mode windows to the default icon - they do not share state with the background page @@ -1082,9 +1084,7 @@ export default class MainBackground { if (!this.isPrivateMode) { await this.refreshBadge(); } - // 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.fullSync(true); + await this.fullSync(true); setTimeout(() => this.notificationsService.init(), 2500); resolve(); }, 500); @@ -1205,7 +1205,7 @@ export default class MainBackground { BrowserApi.sendMessage("updateBadge"); } await this.refreshBadge(); - await this.mainContextMenuHandler.noAccess(); + await this.mainContextMenuHandler?.noAccess(); // 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.notificationsService.updateConnection(false); diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 40daf1b04d..fe70058640 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -120,7 +120,7 @@ const mainBackground: MainBackground = needsBackgroundInit : BrowserApi.getBackgroundPage().bitwardenMain; function createLocalBgService() { - const localBgService = new MainBackground(isPrivateMode); + const localBgService = new MainBackground(isPrivateMode, true); // 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 localBgService.bootstrap(); From 1cde2dbaefb6c303f58678786faca89dacfd3e0b Mon Sep 17 00:00:00 2001 From: Cesar Gonzalez <cesar.a.gonzalezcs@gmail.com> Date: Wed, 17 Apr 2024 09:38:47 -0500 Subject: [PATCH 203/351] [PM-7527] Add `Build Manifest v3` build step to the build-browser.yml Github action (#8777) * [PM-7527] Get MV3 build artifacts in main branch with clear messaging that that the build is not to be released * [PM-7527] Add `Build Manifest v3` build step to the build-browser.yml Github action --- .github/workflows/build-browser.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-browser.yml b/.github/workflows/build-browser.yml index 4fb72a47de..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 From 3179867310e2fec5913b73f67e364b69273b590f Mon Sep 17 00:00:00 2001 From: Tom <144813356+ttalty@users.noreply.github.com> Date: Wed, 17 Apr 2024 10:39:15 -0400 Subject: [PATCH 204/351] [PM-7352] - Browser Send Type Groupings Fix (#8727) * Removed type counts and just calculated counts off the sends * Fixing key spec test case --- .../src/models/browserSendComponentState.ts | 10 ---------- .../popup/send/send-groupings.component.html | 4 ++-- .../popup/send/send-groupings.component.ts | 20 ++----------------- .../browser-send-state.service.spec.ts | 2 -- .../services/browser-send-state.service.ts | 2 +- .../popup/services/key-definitions.spec.ts | 5 ++--- 6 files changed, 7 insertions(+), 36 deletions(-) diff --git a/apps/browser/src/models/browserSendComponentState.ts b/apps/browser/src/models/browserSendComponentState.ts index 9158efc21d..81dd93323b 100644 --- a/apps/browser/src/models/browserSendComponentState.ts +++ b/apps/browser/src/models/browserSendComponentState.ts @@ -1,5 +1,3 @@ -import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; import { DeepJsonify } from "@bitwarden/common/types/deep-jsonify"; @@ -7,13 +5,6 @@ import { BrowserComponentState } from "./browserComponentState"; export class BrowserSendComponentState extends BrowserComponentState { sends: SendView[]; - typeCounts: Map<SendType, number>; - - toJSON() { - return Utils.merge(this, { - typeCounts: Utils.mapToRecord(this.typeCounts), - }); - } static fromJSON(json: DeepJsonify<BrowserSendComponentState>) { if (json == null) { @@ -22,7 +13,6 @@ export class BrowserSendComponentState extends BrowserComponentState { return Object.assign(new BrowserSendComponentState(), json, { sends: json.sends?.map((s) => SendView.fromJSON(s)), - typeCounts: Utils.recordToMap(json.typeCounts), }); } } diff --git a/apps/browser/src/tools/popup/send/send-groupings.component.html b/apps/browser/src/tools/popup/send/send-groupings.component.html index edeabd6546..213afdfa22 100644 --- a/apps/browser/src/tools/popup/send/send-groupings.component.html +++ b/apps/browser/src/tools/popup/send/send-groupings.component.html @@ -61,7 +61,7 @@ <div class="icon"><i class="bwi bwi-fw bwi-lg bwi-file-text"></i></div> <span class="text">{{ "sendTypeText" | i18n }}</span> </div> - <span class="row-sub-label">{{ typeCounts.get(sendType.Text) || 0 }}</span> + <span class="row-sub-label">{{ getSendCount(sends, sendType.Text) }}</span> <span><i class="bwi bwi-angle-right bwi-lg row-sub-icon"></i></span> </button> <button @@ -74,7 +74,7 @@ <div class="icon"><i class="bwi bwi-fw bwi-lg bwi-file"></i></div> <span class="text">{{ "sendTypeFile" | i18n }}</span> </div> - <span class="row-sub-label">{{ typeCounts.get(sendType.File) || 0 }}</span> + <span class="row-sub-label">{{ getSendCount(sends, sendType.File) }}</span> <span><i class="bwi bwi-angle-right bwi-lg row-sub-icon"></i></span> </button> </div> 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 a49773367d..87d03c4b76 100644 --- a/apps/browser/src/tools/popup/send/send-groupings.component.ts +++ b/apps/browser/src/tools/popup/send/send-groupings.component.ts @@ -29,8 +29,6 @@ const ComponentId = "SendComponent"; export class SendGroupingsComponent extends BaseSendComponent { // Header showLeftHeader = true; - // Send Type Calculations - typeCounts = new Map<SendType, number>(); // State Handling state: BrowserSendComponentState; private loadedTimeout: number; @@ -65,7 +63,6 @@ export class SendGroupingsComponent extends BaseSendComponent { dialogService, ); super.onSuccessfulLoad = async () => { - this.calculateTypeCounts(); this.selectAll(); }; } @@ -174,17 +171,8 @@ export class SendGroupingsComponent extends BaseSendComponent { return this.hasSearched || (!this.searchPending && this.isSearchable); } - private calculateTypeCounts() { - // Create type counts - const typeCounts = new Map<SendType, number>(); - 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() { @@ -192,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); } @@ -206,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/services/browser-send-state.service.spec.ts b/apps/browser/src/tools/popup/services/browser-send-state.service.spec.ts index 3dafc0934a..6f0ae1455a 100644 --- 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 @@ -6,7 +6,6 @@ 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 { SendType } from "@bitwarden/common/tools/send/enums/send-type"; import { UserId } from "@bitwarden/common/types/guid"; import { BrowserComponentState } from "../../../models/browserComponentState"; @@ -33,7 +32,6 @@ describe("Browser Send State Service", () => { const state = new BrowserSendComponentState(); state.scrollY = 0; state.searchText = "test"; - state.typeCounts = new Map<SendType, number>().set(SendType.File, 1); await stateService.setBrowserSendComponentState(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 index b814ee5bc9..52aeb01a92 100644 --- a/apps/browser/src/tools/popup/services/browser-send-state.service.ts +++ b/apps/browser/src/tools/popup/services/browser-send-state.service.ts @@ -42,7 +42,7 @@ export class BrowserSendStateService { } /** Set the active user's browser send component state - * @param { BrowserSendComponentState } value sets the sends and type counts along with the scroll position and search text for + * @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<void> { diff --git a/apps/browser/src/tools/popup/services/key-definitions.spec.ts b/apps/browser/src/tools/popup/services/key-definitions.spec.ts index 3ba574efa3..7517771669 100644 --- a/apps/browser/src/tools/popup/services/key-definitions.spec.ts +++ b/apps/browser/src/tools/popup/services/key-definitions.spec.ts @@ -1,7 +1,5 @@ import { Jsonify } from "type-fest"; -import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; - import { BrowserSendComponentState } from "../../../models/browserSendComponentState"; import { BROWSER_SEND_COMPONENT, BROWSER_SEND_TYPE_COMPONENT } from "./key-definitions"; @@ -12,7 +10,8 @@ describe("Key definitions", () => { const keyDef = BROWSER_SEND_COMPONENT; const expectedState = { - typeCounts: new Map<SendType, number>(), + scrollY: 0, + searchText: "test", }; const result = keyDef.deserializer( From 395f2e806e616baffcb7f5f869ddf0ce3acd9381 Mon Sep 17 00:00:00 2001 From: aj-rosado <109146700+aj-rosado@users.noreply.github.com> Date: Wed, 17 Apr 2024 16:06:25 +0100 Subject: [PATCH 205/351] Calling closeAll from dialogService on Send ngOnDestroy (#8763) --- apps/web/src/app/tools/send/send.component.ts | 1 + 1 file changed, 1 insertion(+) 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); } From 28a89ddb8605832d4ff8147dfbc2ccf30c2daf53 Mon Sep 17 00:00:00 2001 From: aj-rosado <109146700+aj-rosado@users.noreply.github.com> Date: Wed, 17 Apr 2024 16:19:06 +0100 Subject: [PATCH 206/351] [PM-7304] Add missing i18n keys for import errors (#8743) * Add static error message when import fails * Adding missing string on CLI for unassigned items * Added missing string on setImportTarget * fixed tests --- apps/browser/src/_locales/en/messages.json | 6 ++++++ apps/cli/src/locales/en/messages.json | 9 +++++++++ apps/desktop/src/locales/en/messages.json | 6 ++++++ apps/web/src/locales/en/messages.json | 6 ++++++ libs/importer/src/services/import.service.spec.ts | 4 ++-- libs/importer/src/services/import.service.ts | 4 ++-- 6 files changed, 31 insertions(+), 4 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 5e941083df..f599df470b 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -3011,5 +3011,11 @@ }, "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." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } 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/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index a0d34e4075..00eace54e2 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -2705,5 +2705,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index c8dfa14c8b..680014f1f0 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -7944,5 +7944,11 @@ }, "deleteProviderWarning": { "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/libs/importer/src/services/import.service.spec.ts b/libs/importer/src/services/import.service.spec.ts index eb21f384b5..7bbcd3287a 100644 --- a/libs/importer/src/services/import.service.spec.ts +++ b/libs/importer/src/services/import.service.spec.ts @@ -196,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 () => { @@ -206,7 +206,7 @@ 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 () => { diff --git a/libs/importer/src/services/import.service.ts b/libs/importer/src/services/import.service.ts index 62961a77c4..f5cab933f3 100644 --- a/libs/importer/src/services/import.service.ts +++ b/libs/importer/src/services/import.service.ts @@ -432,7 +432,7 @@ 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][] = []; @@ -463,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][] = []; From f15bffb040afa8b5d40405d042d8baf951ce8193 Mon Sep 17 00:00:00 2001 From: Matt Gibson <mgibson@bitwarden.com> Date: Wed, 17 Apr 2024 12:02:21 -0400 Subject: [PATCH 207/351] Handle null values coming from state (#8784) --- libs/common/src/vault/services/cipher.service.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index e8544d7f98..dce58d3ba5 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -1,4 +1,4 @@ -import { Observable, firstValueFrom } from "rxjs"; +import { Observable, firstValueFrom, map } from "rxjs"; import { SemVer } from "semver"; import { ApiService } from "../../abstractions/api.service"; @@ -100,9 +100,9 @@ export class CipherService implements CipherServiceAbstraction { this.decryptedCiphersState = this.stateProvider.getActive(DECRYPTED_CIPHERS); this.addEditCipherInfoState = this.stateProvider.getActive(ADD_EDIT_CIPHER_INFO_KEY); - this.localData$ = this.localDataState.state$; - this.ciphers$ = this.encryptedCiphersState.state$; - this.cipherViews$ = this.decryptedCiphersState.state$; + 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$; } From c0455583127770a46bb33bf6a889e24a44a25579 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Gon=C3=A7alves?= <cgoncalves@bitwarden.com> Date: Wed, 17 Apr 2024 17:09:44 +0100 Subject: [PATCH 208/351] PM-7533 Add missing value assignment (#8780) --- libs/common/src/vault/services/cipher.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index dce58d3ba5..7d06b3185f 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -785,7 +785,7 @@ export class CipherService implements CipherServiceAbstraction { async upsert(cipher: CipherData | CipherData[]): Promise<any> { const ciphers = cipher instanceof CipherData ? [cipher] : cipher; await this.updateEncryptedCipherState((current) => { - ciphers.forEach((c) => current[c.id as CipherId]); + ciphers.forEach((c) => (current[c.id as CipherId] = c)); return current; }); } From e44816800220c80b375028e1f73f4518d561a27d Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Thu, 18 Apr 2024 03:00:00 +1000 Subject: [PATCH 209/351] [AC-2444] Add deep links to unassigned items banner (#8720) * Add link to Admin Console * update date on self-hosted banner --- apps/browser/src/_locales/en/messages.json | 19 ++++++++--- .../vault/current-tab.component.html | 12 ++++++- .../layouts/header/web-header.component.html | 11 ++++++- apps/web/src/locales/en/messages.json | 20 +++++++---- .../unassigned-items-banner.service.spec.ts | 11 ++++++- .../unassigned-items-banner.service.ts | 33 +++++++++++++++++-- 6 files changed, 90 insertions(+), 16 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index f599df470b..36e3ce65a8 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -3006,11 +3006,22 @@ "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." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "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." + "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." 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 fc8b4212ba..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 @@ -40,12 +40,22 @@ *ngIf=" (unassignedItemsBannerEnabled$ | async) && (unassignedItemsBannerService.showBanner$ | async) && - (unassignedItemsBannerService.bannerText$ | async) + !(unassignedItemsBannerService.loading$ | async) " type="info" > <p> {{ unassignedItemsBannerService.bannerText$ | async | i18n }} + {{ "unassignedItemsBannerCTAPartOne" | i18n }} + <a + [href]="unassignedItemsBannerService.adminConsoleUrl$ | async" + bitLink + linkType="contrast" + target="_blank" + rel="noreferrer" + >{{ "adminConsole" | i18n }}</a + > + {{ "unassignedItemsBannerCTAPartTwo" | i18n }} <a href="https://bitwarden.com/help/unassigned-vault-items-moved-to-admin-console" bitLink 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 9346763a47..e24013de6f 100644 --- a/apps/web/src/app/layouts/header/web-header.component.html +++ b/apps/web/src/app/layouts/header/web-header.component.html @@ -4,10 +4,19 @@ *ngIf=" (unassignedItemsBannerEnabled$ | async) && (unassignedItemsBannerService.showBanner$ | async) && - (unassignedItemsBannerService.bannerText$ | async) + !(unassignedItemsBannerService.loading$ | async) " > {{ unassignedItemsBannerService.bannerText$ | async | i18n }} + {{ "unassignedItemsBannerCTAPartOne" | i18n }} + <a + [href]="unassignedItemsBannerService.adminConsoleUrl$ | async" + bitLink + linkType="contrast" + rel="noreferrer" + >{{ "adminConsole" | i18n }}</a + > + {{ "unassignedItemsBannerCTAPartTwo" | i18n }} <a href="https://bitwarden.com/help/unassigned-vault-items-moved-to-admin-console" bitLink diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 680014f1f0..32c748d4af 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -7900,15 +7900,23 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now 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 your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." - }, "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices 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." + }, "deleteProvider": { "message": "Delete provider" }, diff --git a/libs/angular/src/services/unassigned-items-banner.service.spec.ts b/libs/angular/src/services/unassigned-items-banner.service.spec.ts index ca2487a518..bf0fb23881 100644 --- a/libs/angular/src/services/unassigned-items-banner.service.spec.ts +++ b/libs/angular/src/services/unassigned-items-banner.service.spec.ts @@ -1,6 +1,7 @@ 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"; @@ -12,9 +13,15 @@ describe("UnassignedItemsBanner", () => { let stateProvider: FakeStateProvider; let apiService: MockProxy<UnassignedItemsBannerApiService>; let environmentService: MockProxy<EnvironmentService>; + let organizationService: MockProxy<OrganizationService>; const sutFactory = () => - new UnassignedItemsBannerService(stateProvider, apiService, environmentService); + new UnassignedItemsBannerService( + stateProvider, + apiService, + environmentService, + organizationService, + ); beforeEach(() => { const fakeAccountService = mockAccountServiceWith("userId" as UserId); @@ -22,6 +29,8 @@ describe("UnassignedItemsBanner", () => { apiService = mock(); environmentService = mock(); environmentService.environment$ = of(null); + organizationService = mock(); + organizationService.organizations$ = of([]); }); it("shows the banner if showBanner local state is true", async () => { diff --git a/libs/angular/src/services/unassigned-items-banner.service.ts b/libs/angular/src/services/unassigned-items-banner.service.ts index 13a745fb82..db93d4c4fc 100644 --- a/libs/angular/src/services/unassigned-items-banner.service.ts +++ b/libs/angular/src/services/unassigned-items-banner.service.ts @@ -1,6 +1,10 @@ import { Injectable } from "@angular/core"; -import { concatMap, map } from "rxjs"; +import { combineLatest, concatMap, map, startWith } from "rxjs"; +import { + OrganizationService, + canAccessOrgAdmin, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { EnvironmentService, Region, @@ -40,18 +44,41 @@ export class UnassignedItemsBannerService { }), ); + 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 - ? "unassignedItemsBannerSelfHost" - : "unassignedItemsBanner", + ? "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() { From 504fe826ea8fce23e9db1cc19605832a5f399030 Mon Sep 17 00:00:00 2001 From: Bitwarden DevOps <106330231+bitwarden-devops-bot@users.noreply.github.com> Date: Wed, 17 Apr 2024 17:50:57 -0400 Subject: [PATCH 210/351] Bumped desktop version to (#8793) --- apps/desktop/package.json | 2 +- apps/desktop/src/package-lock.json | 4 ++-- apps/desktop/src/package.json | 2 +- package-lock.json | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 0dc23b04b1..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.4.1", + "version": "2024.4.2", "keywords": [ "bitwarden", "password", diff --git a/apps/desktop/src/package-lock.json b/apps/desktop/src/package-lock.json index 0531345131..11b38bd273 100644 --- a/apps/desktop/src/package-lock.json +++ b/apps/desktop/src/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bitwarden/desktop", - "version": "2024.4.1", + "version": "2024.4.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bitwarden/desktop", - "version": "2024.4.1", + "version": "2024.4.2", "license": "GPL-3.0", "dependencies": { "@bitwarden/desktop-native": "file:../desktop_native", diff --git a/apps/desktop/src/package.json b/apps/desktop/src/package.json index 6527c21521..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.4.1", + "version": "2024.4.2", "author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)", "homepage": "https://bitwarden.com", "license": "GPL-3.0", diff --git a/package-lock.json b/package-lock.json index 096f6653cb..789f93cc83 100644 --- a/package-lock.json +++ b/package-lock.json @@ -233,7 +233,7 @@ }, "apps/desktop": { "name": "@bitwarden/desktop", - "version": "2024.4.1", + "version": "2024.4.2", "hasInstallScript": true, "license": "GPL-3.0" }, From 83b3fd83e48d0fae7939dab6af8519838e447d9f Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Thu, 18 Apr 2024 09:53:40 +1000 Subject: [PATCH 211/351] Delete unused SettingsComponents (#8778) --- .../settings/settings.component.html | 88 ------------------- .../settings/settings.component.ts | 27 ------ .../providers/settings/settings.component.ts | 23 ----- 3 files changed, 138 deletions(-) delete mode 100644 apps/web/src/app/admin-console/organizations/settings/settings.component.html delete mode 100644 apps/web/src/app/admin-console/organizations/settings/settings.component.ts delete mode 100644 bitwarden_license/bit-web/src/app/admin-console/providers/settings/settings.component.ts 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 @@ -<div class="container page-content"> - <div class="row"> - <div class="col-3"> - <div class="card"> - <div class="card-header">{{ "settings" | i18n }}</div> - <div class="list-group list-group-flush" *ngIf="organization$ | async as org"> - <a - routerLink="account" - class="list-group-item" - routerLinkActive="active" - *ngIf="org.isOwner" - > - {{ "organizationInfo" | i18n }} - </a> - <a - routerLink="policies" - class="list-group-item" - routerLinkActive="active" - *ngIf="org.canManagePolicies" - > - {{ "policies" | i18n }} - </a> - <a - routerLink="two-factor" - class="list-group-item" - routerLinkActive="active" - *ngIf="org.use2fa && org.isOwner" - > - {{ "twoStepLogin" | i18n }} - </a> - <a - routerLink="tools/import" - class="list-group-item" - routerLinkActive="active" - *ngIf="org.canAccessImportExport" - > - {{ "importData" | i18n }} - </a> - <a - routerLink="tools/export" - class="list-group-item" - routerLinkActive="active" - *ngIf="org.canAccessImportExport" - > - {{ "exportVault" | i18n }} - </a> - <a - routerLink="domain-verification" - class="list-group-item" - routerLinkActive="active" - *ngIf="org?.canManageDomainVerification" - > - {{ "domainVerification" | i18n }} - </a> - <a - routerLink="sso" - class="list-group-item" - routerLinkActive="active" - *ngIf="org.canManageSso" - > - {{ "singleSignOn" | i18n }} - </a> - <ng-container> - <a - routerLink="device-approvals" - class="list-group-item" - routerLinkActive="active" - *ngIf="org.canManageDeviceApprovals" - > - {{ "deviceApprovals" | i18n }} - </a> - </ng-container> - <a - routerLink="scim" - class="list-group-item" - routerLinkActive="active" - *ngIf="org.canManageScim" - > - {{ "scim" | i18n }} - </a> - </div> - </div> - </div> - <div class="col-9"> - <router-outlet></router-outlet> - </div> - </div> -</div> 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<Organization>; - 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/bitwarden_license/bit-web/src/app/admin-console/providers/settings/settings.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/settings/settings.component.ts deleted file mode 100644 index 2418dbed41..0000000000 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/settings/settings.component.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Component } from "@angular/core"; -import { ActivatedRoute } from "@angular/router"; - -import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; - -@Component({ - selector: "provider-settings", - templateUrl: "settings.component.html", -}) -// eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class SettingsComponent { - constructor( - private route: ActivatedRoute, - private providerService: ProviderService, - ) {} - - ngOnInit() { - // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe - this.route.parent.params.subscribe(async (params) => { - await this.providerService.get(params.providerId); - }); - } -} From cbaf3462c1a3f6577fe6dcda74c4171d04556371 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 18 Apr 2024 12:03:51 +0200 Subject: [PATCH 212/351] [PM-7475][deps] Tools: Update electron to v28.3.1 (#8742) * [deps] Tools: Update electron to v28.3.1 * Update version in electron-builder.json --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com> --- apps/desktop/electron-builder.json | 2 +- package-lock.json | 8 ++++---- package.json | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/desktop/electron-builder.json b/apps/desktop/electron-builder.json index 5fd26f32ba..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", diff --git a/package-lock.json b/package-lock.json index 789f93cc83..d72ba9cb19 100644 --- a/package-lock.json +++ b/package-lock.json @@ -127,7 +127,7 @@ "copy-webpack-plugin": "12.0.2", "cross-env": "7.0.3", "css-loader": "6.10.0", - "electron": "28.2.8", + "electron": "28.3.1", "electron-builder": "24.13.3", "electron-log": "5.0.1", "electron-reload": "2.0.0-alpha.1", @@ -16924,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": { diff --git a/package.json b/package.json index 203da2d625..057e737903 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,7 @@ "copy-webpack-plugin": "12.0.2", "cross-env": "7.0.3", "css-loader": "6.10.0", - "electron": "28.2.8", + "electron": "28.3.1", "electron-builder": "24.13.3", "electron-log": "5.0.1", "electron-reload": "2.0.0-alpha.1", From 1e0ad097577f29a2ad303404a68884fe940fbc42 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Thu, 18 Apr 2024 08:36:38 -0400 Subject: [PATCH 213/351] Added create-client-organization.component (#8767) --- apps/web/src/locales/en/messages.json | 21 +++ .../providers/providers-routing.module.ts | 2 +- .../providers/providers.module.ts | 10 +- .../services/web-provider.service.ts | 50 ++++++ .../create-client-organization.component.html | 69 +++++++++ .../create-client-organization.component.ts | 142 ++++++++++++++++++ .../app/billing/providers/clients/index.ts | 3 + ...ent-organization-subscription.component.ts | 8 +- ...manage-client-organizations.component.html | 8 +- .../manage-client-organizations.component.ts | 35 ++++- .../src/services/jslib-services.module.ts | 2 + .../billilng-api.service.abstraction.ts | 16 +- .../create-client-organization.request.ts | 12 ++ .../provider-subscription-update.request.ts | 3 - .../update-client-organization.request.ts | 3 + .../billing/services/billing-api.service.ts | 31 +++- .../services/organization-billing.service.ts | 20 ++- 17 files changed, 409 insertions(+), 26 deletions(-) create mode 100644 bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-organization.component.html create mode 100644 bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-organization.component.ts create mode 100644 bitwarden_license/bit-web/src/app/billing/providers/clients/index.ts create mode 100644 libs/common/src/billing/models/request/create-client-organization.request.ts delete mode 100644 libs/common/src/billing/models/request/provider-subscription-update.request.ts create mode 100644 libs/common/src/billing/models/request/update-client-organization.request.ts diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 32c748d4af..49611145dd 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -7958,5 +7958,26 @@ }, "errorAssigningTargetFolder": { "message": "Error assigning target folder." + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" } } diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts index a42b10d88f..3976499268 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts @@ -69,7 +69,7 @@ const routes: Routes = [ { path: "manage-client-organizations", component: ManageClientOrganizationsComponent, - data: { titleId: "manage-client-organizations" }, + data: { titleId: "clients" }, }, { path: "manage", diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts index 0d75973712..20350fc600 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts @@ -4,13 +4,16 @@ import { FormsModule } from "@angular/forms"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { SearchModule } from "@bitwarden/components"; +import { DangerZoneComponent } from "@bitwarden/web-vault/app/auth/settings/account/danger-zone.component"; import { OrganizationPlansComponent, TaxInfoComponent } from "@bitwarden/web-vault/app/billing"; import { PaymentMethodWarningsModule } from "@bitwarden/web-vault/app/billing/shared"; import { OssModule } from "@bitwarden/web-vault/app/oss.module"; -import { DangerZoneComponent } from "../../../../../../apps/web/src/app/auth/settings/account/danger-zone.component"; -import { ManageClientOrganizationSubscriptionComponent } from "../../billing/providers/clients/manage-client-organization-subscription.component"; -import { ManageClientOrganizationsComponent } from "../../billing/providers/clients/manage-client-organizations.component"; +import { + CreateClientOrganizationComponent, + ManageClientOrganizationSubscriptionComponent, + ManageClientOrganizationsComponent, +} from "../../billing/providers/clients"; import { AddOrganizationComponent } from "./clients/add-organization.component"; import { ClientsComponent } from "./clients/clients.component"; @@ -56,6 +59,7 @@ import { SetupComponent } from "./setup/setup.component"; SetupComponent, SetupProviderComponent, UserAddEditComponent, + CreateClientOrganizationComponent, ManageClientOrganizationsComponent, ManageClientOrganizationSubscriptionComponent, ], diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/services/web-provider.service.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/services/web-provider.service.ts index 4f715fd5c5..4195ffcb05 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/services/web-provider.service.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/services/web-provider.service.ts @@ -1,8 +1,15 @@ import { Injectable } from "@angular/core"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request"; import { ProviderAddOrganizationRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-add-organization.request"; +import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billilng-api.service.abstraction"; +import { PlanType } from "@bitwarden/common/billing/enums"; +import { CreateClientOrganizationRequest } from "@bitwarden/common/billing/models/request/create-client-organization.request"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { OrgKey } from "@bitwarden/common/types/key"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; @Injectable() @@ -11,6 +18,9 @@ export class WebProviderService { private cryptoService: CryptoService, private syncService: SyncService, private apiService: ApiService, + private i18nService: I18nService, + private encryptService: EncryptService, + private billingApiService: BillingApiServiceAbstraction, ) {} async addOrganizationToProvider(providerId: string, organizationId: string) { @@ -28,6 +38,46 @@ export class WebProviderService { return response; } + async createClientOrganization( + providerId: string, + name: string, + ownerEmail: string, + planType: PlanType, + seats: number, + ): Promise<void> { + const organizationKey = (await this.cryptoService.makeOrgKey<OrgKey>())[1]; + + const [publicKey, encryptedPrivateKey] = await this.cryptoService.makeKeyPair(organizationKey); + + const encryptedCollectionName = await this.encryptService.encrypt( + this.i18nService.t("defaultCollection"), + organizationKey, + ); + + const providerKey = await this.cryptoService.getProviderKey(providerId); + + const encryptedProviderKey = await this.encryptService.encrypt( + organizationKey.key, + providerKey, + ); + + const request = new CreateClientOrganizationRequest(); + request.name = name; + request.ownerEmail = ownerEmail; + request.planType = planType; + request.seats = seats; + + request.key = encryptedProviderKey.encryptedString; + request.keyPair = new OrganizationKeysRequest(publicKey, encryptedPrivateKey.encryptedString); + request.collectionName = encryptedCollectionName.encryptedString; + + await this.billingApiService.createClientOrganization(providerId, request); + + await this.apiService.refreshIdentityToken(); + + await this.syncService.fullSync(true); + } + async detachOrganization(providerId: string, organizationId: string): Promise<any> { await this.apiService.deleteProviderOrganization(providerId, organizationId); await this.syncService.fullSync(true); diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-organization.component.html b/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-organization.component.html new file mode 100644 index 0000000000..4c5d9fca9b --- /dev/null +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-organization.component.html @@ -0,0 +1,69 @@ +<form [formGroup]="formGroup" [bitSubmit]="submit"> + <bit-dialog dialogSize="large"> + <span bitDialogTitle class="tw-font-semibold"> + {{ "newClientOrganization" | i18n }} + </span> + <div bitDialogContent> + <p>{{ "createNewClientToManageAsProvider" | i18n }}</p> + <div class="tw-mb-3"> + <span class="tw-text-lg tw-pr-1">{{ "selectAPlan" | i18n }}</span> + <span bitBadge variant="success">{{ "thirtyFivePercentDiscount" | i18n }}</span> + </div> + <ng-container> + <div class="tw-grid tw-grid-flow-col tw-grid-cols-2 tw-gap-4 tw-mb-4"> + <div + *ngFor="let planCard of planCards" + [ngClass]="getPlanCardContainerClasses(planCard.selected)" + (click)="selectPlan(planCard.name)" + > + <div class="tw-relative"> + <div + *ngIf="planCard.selected" + class="tw-bg-primary-600 tw-text-center !tw-text-contrast tw-text-sm tw-font-bold tw-py-1 group-hover:tw-bg-primary-700" + > + {{ "selected" | i18n }} + </div> + <div class="tw-p-5" [ngClass]="{ 'tw-pt-12': !planCard.selected }"> + <h3 class="tw-text-2xl tw-font-bold tw-uppercase">{{ planCard.name }}</h3> + <span class="tw-text-2xl tw-font-semibold">{{ + planCard.cost | currency: "$" + }}</span> + <span class="tw-text-sm tw-font-bold">/{{ "monthPerMember" | i18n }}</span> + </div> + </div> + </div> + </div> + </ng-container> + <div class="tw-grid tw-grid-flow-col tw-grid-cols-2 tw-gap-4"> + <bit-form-field> + <bit-label> + {{ "organizationName" | i18n }} + </bit-label> + <input type="text" bitInput formControlName="organizationName" /> + </bit-form-field> + <bit-form-field> + <bit-label> + {{ "clientOwnerEmail" | i18n }} + </bit-label> + <input type="text" bitInput formControlName="clientOwnerEmail" /> + </bit-form-field> + </div> + <div class="tw-grid tw-grid-flow-col tw-grid-cols-2 tw-gap-4"> + <bit-form-field> + <bit-label> + {{ "seats" | i18n }} + </bit-label> + <input type="text" bitInput formControlName="seats" /> + </bit-form-field> + </div> + </div> + <ng-container bitDialogFooter> + <button bitButton bitFormButton buttonType="primary" type="submit"> + {{ "addOrganization" | i18n }} + </button> + <button bitButton buttonType="secondary" type="button" [bitDialogClose]="ResultType.Closed"> + {{ "close" | i18n }} + </button> + </ng-container> + </bit-dialog> +</form> diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-organization.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-organization.component.ts new file mode 100644 index 0000000000..8427572516 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-organization.component.ts @@ -0,0 +1,142 @@ +import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; +import { Component, Inject, OnInit } from "@angular/core"; +import { FormBuilder, Validators } from "@angular/forms"; + +import { PlanType } from "@bitwarden/common/billing/enums"; +import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.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"; + +import { WebProviderService } from "../../../admin-console/providers/services/web-provider.service"; + +type CreateClientOrganizationParams = { + providerId: string; + plans: PlanResponse[]; +}; + +export enum CreateClientOrganizationResultType { + Closed = "closed", + Submitted = "submitted", +} + +export const openCreateClientOrganizationDialog = ( + dialogService: DialogService, + dialogConfig: DialogConfig<CreateClientOrganizationParams>, +) => + dialogService.open<CreateClientOrganizationResultType, CreateClientOrganizationParams>( + CreateClientOrganizationComponent, + dialogConfig, + ); + +type PlanCard = { + name: string; + cost: number; + type: PlanType; + selected: boolean; +}; + +@Component({ + selector: "app-create-client-organization", + templateUrl: "./create-client-organization.component.html", +}) +export class CreateClientOrganizationComponent implements OnInit { + protected ResultType = CreateClientOrganizationResultType; + protected formGroup = this.formBuilder.group({ + clientOwnerEmail: ["", [Validators.required, Validators.email]], + organizationName: ["", Validators.required], + seats: [null, [Validators.required, Validators.min(1)]], + }); + protected planCards: PlanCard[]; + + constructor( + @Inject(DIALOG_DATA) private dialogParams: CreateClientOrganizationParams, + private dialogRef: DialogRef<CreateClientOrganizationResultType>, + private formBuilder: FormBuilder, + private i18nService: I18nService, + private platformUtilsService: PlatformUtilsService, + private webProviderService: WebProviderService, + ) {} + + protected getPlanCardContainerClasses(selected: boolean) { + switch (selected) { + case true: { + return [ + "tw-group", + "tw-cursor-pointer", + "tw-block", + "tw-rounded", + "tw-border", + "tw-border-solid", + "tw-border-primary-600", + "hover:tw-border-primary-700", + "focus:tw-border-2", + "focus:tw-border-primary-700", + "focus:tw-rounded-lg", + ]; + } + case false: { + return [ + "tw-cursor-pointer", + "tw-block", + "tw-rounded", + "tw-border", + "tw-border-solid", + "tw-border-secondary-300", + "hover:tw-border-text-main", + "focus:tw-border-2", + "focus:tw-border-primary-700", + ]; + } + } + } + + async ngOnInit(): Promise<void> { + const teamsPlan = this.dialogParams.plans.find((plan) => plan.type === PlanType.TeamsMonthly); + const enterprisePlan = this.dialogParams.plans.find( + (plan) => plan.type === PlanType.EnterpriseMonthly, + ); + + this.planCards = [ + { + name: this.i18nService.t("planNameTeams"), + cost: teamsPlan.PasswordManager.seatPrice * 0.65, // 35% off for MSPs, + type: teamsPlan.type, + selected: true, + }, + { + name: this.i18nService.t("planNameEnterprise"), + cost: enterprisePlan.PasswordManager.seatPrice * 0.65, // 35% off for MSPs, + type: enterprisePlan.type, + selected: false, + }, + ]; + } + + protected selectPlan(name: string) { + this.planCards.find((planCard) => planCard.name === name).selected = true; + this.planCards.find((planCard) => planCard.name !== name).selected = false; + } + + submit = async () => { + this.formGroup.markAllAsTouched(); + + if (this.formGroup.invalid) { + return; + } + + const selectedPlanCard = this.planCards.find((planCard) => planCard.selected); + + await this.webProviderService.createClientOrganization( + this.dialogParams.providerId, + this.formGroup.value.organizationName, + this.formGroup.value.clientOwnerEmail, + selectedPlanCard.type, + this.formGroup.value.seats, + ); + + this.platformUtilsService.showToast("success", null, this.i18nService.t("createdNewClient")); + + this.dialogRef.close(this.ResultType.Submitted); + }; +} diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/index.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/index.ts new file mode 100644 index 0000000000..fd9ef8296c --- /dev/null +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/index.ts @@ -0,0 +1,3 @@ +export * from "./create-client-organization.component"; +export * from "./manage-client-organizations.component"; +export * from "./manage-client-organization-subscription.component"; 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 index 2c8d59edc3..2182ac43ab 100644 --- 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 @@ -3,7 +3,7 @@ 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 { UpdateClientOrganizationRequest } from "@bitwarden/common/billing/models/request/update-client-organization.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"; @@ -45,7 +45,7 @@ export class ManageClientOrganizationSubscriptionComponent implements OnInit { async ngOnInit() { try { - const response = await this.billingApiService.getProviderClientSubscriptions(this.providerId); + const response = await this.billingApiService.getProviderSubscription(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); @@ -69,10 +69,10 @@ export class ManageClientOrganizationSubscriptionComponent implements OnInit { return; } - const request = new ProviderSubscriptionUpdateRequest(); + const request = new UpdateClientOrganizationRequest(); request.assignedSeats = assignedSeats; - await this.billingApiService.putProviderClientSubscriptions( + await this.billingApiService.updateClientOrganization( this.providerId, this.providerOrganizationId, request, 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 index dc303d338f..ec5df609c4 100644 --- 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 @@ -1,6 +1,12 @@ <app-header> <bit-search [placeholder]="'search' | i18n" [(ngModel)]="searchText"></bit-search> - <a bitButton routerLink="create" *ngIf="manageOrganizations" buttonType="primary"> + <a + type="button" + bitButton + *ngIf="manageOrganizations" + buttonType="primary" + (click)="createClientOrganization()" + > <i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i> {{ "addNewOrganization" | i18n }} </a> 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 index a9f341be94..2184a617cf 100644 --- 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 @@ -1,7 +1,7 @@ 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 { BehaviorSubject, firstValueFrom, from, lastValueFrom, Subject } from "rxjs"; import { first, switchMap, takeUntil } from "rxjs/operators"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -9,6 +9,8 @@ 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 { BillingApiServiceAbstraction as BillingApiService } from "@bitwarden/common/billing/abstractions/billilng-api.service.abstraction"; +import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.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"; @@ -16,6 +18,10 @@ import { DialogService, TableDataSource } from "@bitwarden/components"; import { WebProviderService } from "../../../admin-console/providers/services/web-provider.service"; +import { + CreateClientOrganizationResultType, + openCreateClientOrganizationDialog, +} from "./create-client-organization.component"; import { ManageClientOrganizationSubscriptionComponent } from "./manage-client-organization-subscription.component"; @Component({ @@ -52,6 +58,7 @@ export class ManageClientOrganizationsComponent implements OnInit, OnDestroy { private pagedClientsCount = 0; selection = new SelectionModel<string>(true, []); protected dataSource = new TableDataSource<ProviderOrganizationOrganizationDetailsResponse>(); + protected plans: PlanResponse[]; constructor( private route: ActivatedRoute, @@ -63,6 +70,7 @@ export class ManageClientOrganizationsComponent implements OnInit, OnDestroy { private validationService: ValidationService, private webProviderService: WebProviderService, private dialogService: DialogService, + private billingApiService: BillingApiService, ) {} async ngOnInit() { @@ -94,12 +102,16 @@ export class ManageClientOrganizationsComponent implements OnInit, OnDestroy { } async load() { - const response = await this.apiService.getProviderClients(this.providerId); - this.clients = response.data != null && response.data.length > 0 ? response.data : []; + const clientsResponse = await this.apiService.getProviderClients(this.providerId); + this.clients = + clientsResponse.data != null && clientsResponse.data.length > 0 ? clientsResponse.data : []; this.dataSource.data = this.clients; this.manageOrganizations = (await this.providerService.get(this.providerId)).type === ProviderUserType.ProviderAdmin; + const plansResponse = await this.billingApiService.getPlans(); + this.plans = plansResponse.data; + this.loading = false; } @@ -177,4 +189,21 @@ export class ManageClientOrganizationsComponent implements OnInit, OnDestroy { } this.actionPromise = null; } + + createClientOrganization = async () => { + const reference = openCreateClientOrganizationDialog(this.dialogService, { + data: { + providerId: this.providerId, + plans: this.plans, + }, + }); + + const result = await lastValueFrom(reference.closed); + + if (result === CreateClientOrganizationResultType.Closed) { + return; + } + + await this.load(); + }; } diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index ad0881a4b3..cc6d947b33 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -1051,10 +1051,12 @@ const safeProviders: SafeProvider[] = [ provide: OrganizationBillingServiceAbstraction, useClass: OrganizationBillingService, deps: [ + ApiServiceAbstraction, CryptoServiceAbstraction, EncryptService, I18nServiceAbstraction, OrganizationApiServiceAbstraction, + SyncServiceAbstraction, ], }), safeProvider({ 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 1311976c4b..16b86c5844 100644 --- a/libs/common/src/billing/abstractions/billilng-api.service.abstraction.ts +++ b/libs/common/src/billing/abstractions/billilng-api.service.abstraction.ts @@ -1,6 +1,9 @@ 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 { PlanResponse } from "../../billing/models/response/plan.response"; +import { ListResponse } from "../../models/response/list.response"; +import { CreateClientOrganizationRequest } from "../models/request/create-client-organization.request"; +import { UpdateClientOrganizationRequest } from "../models/request/update-client-organization.request"; import { ProviderSubscriptionResponse } from "../models/response/provider-subscription-response"; export abstract class BillingApiServiceAbstraction { @@ -9,11 +12,16 @@ export abstract class BillingApiServiceAbstraction { request: SubscriptionCancellationRequest, ) => Promise<void>; cancelPremiumUserSubscription: (request: SubscriptionCancellationRequest) => Promise<void>; + createClientOrganization: ( + providerId: string, + request: CreateClientOrganizationRequest, + ) => Promise<void>; getBillingStatus: (id: string) => Promise<OrganizationBillingStatusResponse>; - getProviderClientSubscriptions: (providerId: string) => Promise<ProviderSubscriptionResponse>; - putProviderClientSubscriptions: ( + getPlans: () => Promise<ListResponse<PlanResponse>>; + getProviderSubscription: (providerId: string) => Promise<ProviderSubscriptionResponse>; + updateClientOrganization: ( providerId: string, organizationId: string, - request: ProviderSubscriptionUpdateRequest, + request: UpdateClientOrganizationRequest, ) => Promise<any>; } diff --git a/libs/common/src/billing/models/request/create-client-organization.request.ts b/libs/common/src/billing/models/request/create-client-organization.request.ts new file mode 100644 index 0000000000..2eac23531a --- /dev/null +++ b/libs/common/src/billing/models/request/create-client-organization.request.ts @@ -0,0 +1,12 @@ +import { OrganizationKeysRequest } from "../../../admin-console/models/request/organization-keys.request"; +import { PlanType } from "../../../billing/enums"; + +export class CreateClientOrganizationRequest { + name: string; + ownerEmail: string; + planType: PlanType; + seats: number; + key: string; + keyPair: OrganizationKeysRequest; + collectionName: string; +} diff --git a/libs/common/src/billing/models/request/provider-subscription-update.request.ts b/libs/common/src/billing/models/request/provider-subscription-update.request.ts deleted file mode 100644 index f2bf4c7e97..0000000000 --- a/libs/common/src/billing/models/request/provider-subscription-update.request.ts +++ /dev/null @@ -1,3 +0,0 @@ -export class ProviderSubscriptionUpdateRequest { - assignedSeats: number; -} diff --git a/libs/common/src/billing/models/request/update-client-organization.request.ts b/libs/common/src/billing/models/request/update-client-organization.request.ts new file mode 100644 index 0000000000..16dbe1e17d --- /dev/null +++ b/libs/common/src/billing/models/request/update-client-organization.request.ts @@ -0,0 +1,3 @@ +export class UpdateClientOrganizationRequest { + assignedSeats: number; +} diff --git a/libs/common/src/billing/services/billing-api.service.ts b/libs/common/src/billing/services/billing-api.service.ts index 48866ab90d..70d1d89252 100644 --- a/libs/common/src/billing/services/billing-api.service.ts +++ b/libs/common/src/billing/services/billing-api.service.ts @@ -2,7 +2,10 @@ 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 { PlanResponse } from "../../billing/models/response/plan.response"; +import { ListResponse } from "../../models/response/list.response"; +import { CreateClientOrganizationRequest } from "../models/request/create-client-organization.request"; +import { UpdateClientOrganizationRequest } from "../models/request/update-client-organization.request"; import { ProviderSubscriptionResponse } from "../models/response/provider-subscription-response"; export class BillingApiService implements BillingApiServiceAbstraction { @@ -25,6 +28,19 @@ export class BillingApiService implements BillingApiServiceAbstraction { return this.apiService.send("POST", "/accounts/cancel", request, true, false); } + createClientOrganization( + providerId: string, + request: CreateClientOrganizationRequest, + ): Promise<void> { + return this.apiService.send( + "POST", + "/providers/" + providerId + "/clients", + request, + true, + false, + ); + } + async getBillingStatus(id: string): Promise<OrganizationBillingStatusResponse> { const r = await this.apiService.send( "GET", @@ -37,7 +53,12 @@ export class BillingApiService implements BillingApiServiceAbstraction { return new OrganizationBillingStatusResponse(r); } - async getProviderClientSubscriptions(providerId: string): Promise<ProviderSubscriptionResponse> { + async getPlans(): Promise<ListResponse<PlanResponse>> { + const r = await this.apiService.send("GET", "/plans", null, false, true); + return new ListResponse(r, PlanResponse); + } + + async getProviderSubscription(providerId: string): Promise<ProviderSubscriptionResponse> { const r = await this.apiService.send( "GET", "/providers/" + providerId + "/billing/subscription", @@ -48,14 +69,14 @@ export class BillingApiService implements BillingApiServiceAbstraction { return new ProviderSubscriptionResponse(r); } - async putProviderClientSubscriptions( + async updateClientOrganization( providerId: string, organizationId: string, - request: ProviderSubscriptionUpdateRequest, + request: UpdateClientOrganizationRequest, ): Promise<any> { return await this.apiService.send( "PUT", - "/providers/" + providerId + "/organizations/" + organizationId, + "/providers/" + providerId + "/clients/" + 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 f2df30e4e0..6b326472c9 100644 --- a/libs/common/src/billing/services/organization-billing.service.ts +++ b/libs/common/src/billing/services/organization-billing.service.ts @@ -1,3 +1,4 @@ +import { ApiService } from "../../abstractions/api.service"; import { OrganizationApiServiceAbstraction as OrganizationApiService } from "../../admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationCreateRequest } from "../../admin-console/models/request/organization-create.request"; import { OrganizationKeysRequest } from "../../admin-console/models/request/organization-keys.request"; @@ -7,6 +8,7 @@ import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; import { EncString } from "../../platform/models/domain/enc-string"; import { OrgKey } from "../../types/key"; +import { SyncService } from "../../vault/abstractions/sync/sync.service.abstraction"; import { OrganizationBillingServiceAbstraction, OrganizationInformation, @@ -25,10 +27,12 @@ interface OrganizationKeys { export class OrganizationBillingService implements OrganizationBillingServiceAbstraction { constructor( + private apiService: ApiService, private cryptoService: CryptoService, private encryptService: EncryptService, private i18nService: I18nService, private organizationApiService: OrganizationApiService, + private syncService: SyncService, ) {} async purchaseSubscription(subscription: SubscriptionInformation): Promise<OrganizationResponse> { @@ -44,7 +48,13 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs this.setPaymentInformation(request, subscription.payment); - return await this.organizationApiService.create(request); + const response = await this.organizationApiService.create(request); + + await this.apiService.refreshIdentityToken(); + + await this.syncService.fullSync(true); + + return response; } async startFree(subscription: SubscriptionInformation): Promise<OrganizationResponse> { @@ -58,7 +68,13 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs this.setPlanInformation(request, subscription.plan); - return await this.organizationApiService.create(request); + const response = await this.organizationApiService.create(request); + + await this.apiService.refreshIdentityToken(); + + await this.syncService.fullSync(true); + + return response; } private async makeOrganizationKeys(): Promise<OrganizationKeys> { From 2c8855692c70e931c1f38d425445ca9718141600 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Thu, 18 Apr 2024 14:17:34 +0100 Subject: [PATCH 214/351] [AC-2209] Update the dash in the admin console collections table permission column to no access (#8609) * [AC-2209] Update permission text to display "No access" when collection is not assigned * [AC-2209] Add permission tooltip for unassigned collections --- .../vault-items/vault-collection-row.component.html | 2 +- .../vault-items/vault-collection-row.component.ts | 12 ++++++++++-- apps/web/src/locales/en/messages.json | 6 ++++++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.html b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.html index a6d7854267..d333f92d5c 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.html +++ b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.html @@ -48,7 +48,7 @@ ></app-group-badge> </td> <td bitCell [ngClass]="RowHeightClass" *ngIf="showPermissionsColumn"> - <p class="tw-mb-0 tw-text-muted"> + <p class="tw-mb-0 tw-text-muted" [title]="permissionTooltip"> {{ permissionText }} </p> </td> diff --git a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts index 666bec7a1a..8bf7779f88 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts @@ -6,6 +6,7 @@ import { CollectionView } from "@bitwarden/common/vault/models/view/collection.v import { GroupView } from "../../../admin-console/organizations/core"; import { CollectionAdminView } from "../../core/views/collection-admin.view"; +import { Unassigned } from "../../individual-vault/vault-filter/shared/models/routed-vault-filter.model"; import { convertToPermission, @@ -52,8 +53,8 @@ export class VaultCollectionRowComponent { } get permissionText() { - if (!(this.collection as CollectionAdminView).assigned) { - return "-"; + if (this.collection.id != Unassigned && !(this.collection as CollectionAdminView).assigned) { + return this.i18nService.t("noAccess"); } else { const permissionList = getPermissionList(this.organization?.flexibleCollections); return this.i18nService.t( @@ -62,6 +63,13 @@ export class VaultCollectionRowComponent { } } + get permissionTooltip() { + if (this.collection.id == Unassigned) { + return this.i18nService.t("collectionAdminConsoleManaged"); + } + return ""; + } + protected edit() { this.onEvent.next({ type: "editCollection", item: this.collection }); } diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 49611145dd..7632392c23 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -7979,5 +7979,11 @@ }, "createdNewClient": { "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } From a45706cde7bb1d6341c3c24b4229d7cf71f2eff4 Mon Sep 17 00:00:00 2001 From: Colton Hurst <colton@coltonhurst.com> Date: Thu, 18 Apr 2024 11:01:14 -0400 Subject: [PATCH 215/351] SM-1181: Rename service accounts to machine accounts in routing (#8688) --- .../src/app/secrets-manager/layout/navigation.component.html | 2 +- .../secrets-manager/projects/project/project.component.html | 2 +- .../app/secrets-manager/projects/projects-routing.module.ts | 2 +- .../service-accounts/guards/service-account-access.guard.ts | 4 ++-- .../people/service-account-people.component.ts | 4 ++-- .../service-accounts/service-account.component.ts | 2 +- .../bit-web/src/app/secrets-manager/sm-routing.module.ts | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) 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 e71f520996..c6c7bc6efb 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 @@ -19,7 +19,7 @@ <bit-nav-item icon="bwi-wrench" [text]="'machineAccounts' | i18n" - route="service-accounts" + route="machine-accounts" [relativeTo]="route.parent" ></bit-nav-item> <bit-nav-item diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project.component.html index b399eef8d2..d95f4392eb 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project.component.html @@ -6,7 +6,7 @@ <bit-tab-link [route]="['secrets']">{{ "secrets" | i18n }}</bit-tab-link> <ng-container *ngIf="project.write"> <bit-tab-link [route]="['people']">{{ "people" | i18n }}</bit-tab-link> - <bit-tab-link [route]="['service-accounts']">{{ "machineAccounts" | i18n }}</bit-tab-link> + <bit-tab-link [route]="['machine-accounts']">{{ "machineAccounts" | i18n }}</bit-tab-link> </ng-container> </bit-tab-nav-bar> <sm-new-menu></sm-new-menu> diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects-routing.module.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects-routing.module.ts index a5248c509f..6078520989 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects-routing.module.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects-routing.module.ts @@ -30,7 +30,7 @@ const routes: Routes = [ component: ProjectPeopleComponent, }, { - path: "service-accounts", + path: "machine-accounts", component: ProjectServiceAccountsComponent, }, ], diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/guards/service-account-access.guard.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/guards/service-account-access.guard.ts index 6258f6f9db..c474ec44d5 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/guards/service-account-access.guard.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/guards/service-account-access.guard.ts @@ -21,8 +21,8 @@ export const serviceAccountAccessGuard: CanActivateFn = async (route: ActivatedR return createUrlTreeFromSnapshot(route, [ "/sm", route.params.organizationId, - "service-accounts", + "machine-accounts", ]); } - return createUrlTreeFromSnapshot(route, ["/sm", route.params.organizationId, "service-accounts"]); + return createUrlTreeFromSnapshot(route, ["/sm", route.params.organizationId, "machine-accounts"]); }; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/people/service-account-people.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/people/service-account-people.component.ts index 76b5e8928d..aeb124aa6a 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/people/service-account-people.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/people/service-account-people.component.ts @@ -43,7 +43,7 @@ export class ServiceAccountPeopleComponent implements OnInit, OnDestroy { catchError(() => { // 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(["/sm", this.organizationId, "service-accounts"]); + this.router.navigate(["/sm", this.organizationId, "machine-accounts"]); return EMPTY; }), ); @@ -200,7 +200,7 @@ export class ServiceAccountPeopleComponent implements OnInit, OnDestroy { if (showAccessRemovalWarning) { // 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(["sm", this.organizationId, "service-accounts"]); + this.router.navigate(["sm", this.organizationId, "machine-accounts"]); } else if ( this.accessPolicySelectorService.isAccessRemoval(currentAccessPolicies, selectedPolicies) ) { 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 083ec7aebb..bb687c51c6 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 @@ -45,7 +45,7 @@ export class ServiceAccountComponent implements OnInit, OnDestroy { catchError(() => { // 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(["/sm", this.organizationId, "service-accounts"]).then(() => { + this.router.navigate(["/sm", this.organizationId, "machine-accounts"]).then(() => { this.platformUtilsService.showToast( "error", null, 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 55dc2f8b71..10aa08612f 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 @@ -54,7 +54,7 @@ const routes: Routes = [ }, }, { - path: "service-accounts", + path: "machine-accounts", loadChildren: () => ServiceAccountsModule, data: { titleId: "machineAccounts", From adb1ee3d38066e81ec7d79cf7c94dfc884c2b9d6 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Thu, 18 Apr 2024 11:21:11 -0400 Subject: [PATCH 216/351] [AC-2420] Hide SM checkbox on member invite when org is on SM Standalone (#8644) * Refactoring * Hide SM toggle on member invite and default to true for SM standalone org * changed from hide sm checkbox to default and disable * Removed errant addition from conflict resolution --- .../member-dialog/member-dialog.component.html | 7 ++++++- .../member-dialog/member-dialog.component.ts | 9 +++++++++ .../organizations/members/people.component.ts | 9 +++++++++ .../angular/src/services/jslib-services.module.ts | 1 + .../billilng-api.service.abstraction.ts | 5 +++++ .../abstractions/organization-billing.service.ts | 2 ++ .../src/billing/services/billing-api.service.ts | 15 ++++++++++++++- .../services/organization-billing.service.ts | 15 +++++++++++++++ 8 files changed, 61 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.html b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.html index 95febbd3c5..4d81d070fb 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.html +++ b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.html @@ -385,7 +385,12 @@ </h3> <p class="tw-text-muted">{{ "secretsManagerAccessDescription" | i18n }}</p> <bit-form-control> - <input type="checkbox" bitCheckbox formControlName="accessSecretsManager" /> + <input + type="checkbox" + [disabled]="isOnSecretsManagerStandalone" + bitCheckbox + formControlName="accessSecretsManager" + /> <bit-label> {{ "userAccessSecretsManagerGA" | i18n }} </bit-label> 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 771b8cc505..f1af950650 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 @@ -63,6 +63,7 @@ export interface MemberDialogParams { organizationUserId: string; allOrganizationUserEmails: string[]; usesKeyConnector: boolean; + isOnSecretsManagerStandalone: boolean; initialTab?: MemberDialogTab; numConfirmedMembers: number; } @@ -88,6 +89,7 @@ export class MemberDialogComponent implements OnDestroy { organizationUserType = OrganizationUserType; PermissionMode = PermissionMode; showNoMasterPasswordWarning = false; + isOnSecretsManagerStandalone: boolean; protected organization$: Observable<Organization>; protected collectionAccessItems: AccessItemView[] = []; @@ -160,6 +162,13 @@ export class MemberDialogComponent implements OnDestroy { this.editMode = this.params.organizationUserId != null; this.tabIndex = this.params.initialTab ?? MemberDialogTab.Role; this.title = this.i18nService.t(this.editMode ? "editMember" : "inviteMember"); + this.isOnSecretsManagerStandalone = this.params.isOnSecretsManagerStandalone; + + if (this.isOnSecretsManagerStandalone) { + this.formGroup.patchValue({ + accessSecretsManager: true, + }); + } const groups$ = this.organization$.pipe( switchMap((organization) => 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 6b632dce38..0df247d7b0 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 @@ -37,6 +37,7 @@ import { import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request"; +import { OrganizationBillingServiceAbstraction as OrganizationBillingService } from "@bitwarden/common/billing/abstractions/organization-billing.service"; import { ProductType } from "@bitwarden/common/enums"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -93,6 +94,7 @@ export class PeopleComponent extends BasePeopleComponent<OrganizationUserView> { organization: Organization; status: OrganizationUserStatusType = null; orgResetPasswordPolicyEnabled = false; + orgIsOnSecretsManagerStandalone = false; protected canUseSecretsManager$: Observable<boolean>; @@ -119,6 +121,7 @@ export class PeopleComponent extends BasePeopleComponent<OrganizationUserView> { private groupService: GroupService, private collectionService: CollectionService, organizationManagementPreferencesService: OrganizationManagementPreferencesService, + private organizationBillingService: OrganizationBillingService, ) { super( apiService, @@ -187,6 +190,11 @@ export class PeopleComponent extends BasePeopleComponent<OrganizationUserView> { .find((p) => p.organizationId === this.organization.id); this.orgResetPasswordPolicyEnabled = resetPasswordPolicy?.enabled; + this.orgIsOnSecretsManagerStandalone = + await this.organizationBillingService.isOnSecretsManagerStandalone( + this.organization.id, + ); + await this.load(); this.searchText = qParams.search; @@ -446,6 +454,7 @@ export class PeopleComponent extends BasePeopleComponent<OrganizationUserView> { organizationUserId: user != null ? user.id : null, allOrganizationUserEmails: this.allUsers?.map((user) => user.email) ?? [], usesKeyConnector: user?.usesKeyConnector, + isOnSecretsManagerStandalone: this.orgIsOnSecretsManagerStandalone, initialTab: initialTab, numConfirmedMembers: this.confirmedCount, }, diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index cc6d947b33..859103474d 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -1052,6 +1052,7 @@ const safeProviders: SafeProvider[] = [ useClass: OrganizationBillingService, deps: [ ApiServiceAbstraction, + BillingApiServiceAbstraction, CryptoServiceAbstraction, EncryptService, I18nServiceAbstraction, 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 16b86c5844..15f0d4b551 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,6 @@ import { SubscriptionCancellationRequest } from "../../billing/models/request/subscription-cancellation.request"; import { OrganizationBillingStatusResponse } from "../../billing/models/response/organization-billing-status.response"; +import { OrganizationSubscriptionResponse } from "../../billing/models/response/organization-subscription.response"; import { PlanResponse } from "../../billing/models/response/plan.response"; import { ListResponse } from "../../models/response/list.response"; import { CreateClientOrganizationRequest } from "../models/request/create-client-organization.request"; @@ -11,12 +12,16 @@ export abstract class BillingApiServiceAbstraction { organizationId: string, request: SubscriptionCancellationRequest, ) => Promise<void>; + cancelPremiumUserSubscription: (request: SubscriptionCancellationRequest) => Promise<void>; createClientOrganization: ( providerId: string, request: CreateClientOrganizationRequest, ) => Promise<void>; getBillingStatus: (id: string) => Promise<OrganizationBillingStatusResponse>; + getOrganizationSubscription: ( + organizationId: string, + ) => Promise<OrganizationSubscriptionResponse>; getPlans: () => Promise<ListResponse<PlanResponse>>; getProviderSubscription: (providerId: string) => Promise<ProviderSubscriptionResponse>; updateClientOrganization: ( diff --git a/libs/common/src/billing/abstractions/organization-billing.service.ts b/libs/common/src/billing/abstractions/organization-billing.service.ts index d19724b600..0917025eec 100644 --- a/libs/common/src/billing/abstractions/organization-billing.service.ts +++ b/libs/common/src/billing/abstractions/organization-billing.service.ts @@ -41,6 +41,8 @@ export type SubscriptionInformation = { }; export abstract class OrganizationBillingServiceAbstraction { + isOnSecretsManagerStandalone: (organizationId: string) => Promise<boolean>; + purchaseSubscription: (subscription: SubscriptionInformation) => Promise<OrganizationResponse>; startFree: (subscription: SubscriptionInformation) => Promise<OrganizationResponse>; diff --git a/libs/common/src/billing/services/billing-api.service.ts b/libs/common/src/billing/services/billing-api.service.ts index 70d1d89252..1c119b971d 100644 --- a/libs/common/src/billing/services/billing-api.service.ts +++ b/libs/common/src/billing/services/billing-api.service.ts @@ -2,6 +2,7 @@ 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 { OrganizationSubscriptionResponse } from "../../billing/models/response/organization-subscription.response"; import { PlanResponse } from "../../billing/models/response/plan.response"; import { ListResponse } from "../../models/response/list.response"; import { CreateClientOrganizationRequest } from "../models/request/create-client-organization.request"; @@ -49,10 +50,22 @@ export class BillingApiService implements BillingApiServiceAbstraction { true, true, ); - return new OrganizationBillingStatusResponse(r); } + async getOrganizationSubscription( + organizationId: string, + ): Promise<OrganizationSubscriptionResponse> { + const r = await this.apiService.send( + "GET", + "/organizations/" + organizationId + "/subscription", + null, + true, + true, + ); + return new OrganizationSubscriptionResponse(r); + } + async getPlans(): Promise<ListResponse<PlanResponse>> { const r = await this.apiService.send("GET", "/plans", null, false, true); return new ListResponse(r, PlanResponse); diff --git a/libs/common/src/billing/services/organization-billing.service.ts b/libs/common/src/billing/services/organization-billing.service.ts index 6b326472c9..fb2084bb6a 100644 --- a/libs/common/src/billing/services/organization-billing.service.ts +++ b/libs/common/src/billing/services/organization-billing.service.ts @@ -9,6 +9,7 @@ import { I18nService } from "../../platform/abstractions/i18n.service"; import { EncString } from "../../platform/models/domain/enc-string"; import { OrgKey } from "../../types/key"; import { SyncService } from "../../vault/abstractions/sync/sync.service.abstraction"; +import { BillingApiServiceAbstraction as BillingApiService } from "../abstractions/billilng-api.service.abstraction"; import { OrganizationBillingServiceAbstraction, OrganizationInformation, @@ -28,6 +29,7 @@ interface OrganizationKeys { export class OrganizationBillingService implements OrganizationBillingServiceAbstraction { constructor( private apiService: ApiService, + private billingApiService: BillingApiService, private cryptoService: CryptoService, private encryptService: EncryptService, private i18nService: I18nService, @@ -35,6 +37,19 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs private syncService: SyncService, ) {} + async isOnSecretsManagerStandalone(organizationId: string): Promise<boolean> { + const response = await this.billingApiService.getOrganizationSubscription(organizationId); + if (response.customerDiscount?.id === "sm-standalone") { + const productIds = response.subscription.items.map((item) => item.productId); + return ( + response.customerDiscount?.appliesTo.filter((appliesToProductId) => + productIds.includes(appliesToProductId), + ).length > 0 + ); + } + return false; + } + async purchaseSubscription(subscription: SubscriptionInformation): Promise<OrganizationResponse> { const request = new OrganizationCreateRequest(); From 912b7c136ea4c056f7755dac17d10f3c7afa2a7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa?= <dani-garcia@users.noreply.github.com> Date: Thu, 18 Apr 2024 17:40:39 +0200 Subject: [PATCH 217/351] [PM-5796] Improve desktop biometric browser integration error handling (#7727) * Re-register native messaging host integrations on startup * Check for errors when generating the manifests * Add log to component * Switch to Promise.all * Add injectable service --- .../background/nativeMessaging.background.ts | 2 + .../src/app/accounts/settings.component.ts | 37 ++- .../native-messaging-manifest.service.ts | 13 + .../src/app/services/services.module.ts | 6 + apps/desktop/src/locales/en/messages.json | 6 + apps/desktop/src/main.ts | 20 +- apps/desktop/src/main/messaging.main.ts | 16 -- .../desktop/src/main/native-messaging.main.ts | 262 +++++++++--------- apps/desktop/src/platform/preload.ts | 7 + 9 files changed, 213 insertions(+), 156 deletions(-) create mode 100644 apps/desktop/src/app/services/native-messaging-manifest.service.ts diff --git a/apps/browser/src/background/nativeMessaging.background.ts b/apps/browser/src/background/nativeMessaging.background.ts index faf2e6e2cc..e5eed06c21 100644 --- a/apps/browser/src/background/nativeMessaging.background.ts +++ b/apps/browser/src/background/nativeMessaging.background.ts @@ -204,6 +204,8 @@ export class NativeMessagingBackground { this.privateKey = null; this.connected = false; + this.logService.error("NativeMessaging port disconnected because of error: " + error); + const reason = error != null ? "desktopIntegrationDisabled" : null; reject(new Error(reason)); }); diff --git a/apps/desktop/src/app/accounts/settings.component.ts b/apps/desktop/src/app/accounts/settings.component.ts index 5f59530d8c..06533e18fc 100644 --- a/apps/desktop/src/app/accounts/settings.component.ts +++ b/apps/desktop/src/app/accounts/settings.component.ts @@ -14,6 +14,7 @@ import { DeviceType } from "@bitwarden/common/enums"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; 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"; 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"; @@ -27,6 +28,7 @@ import { DialogService } from "@bitwarden/components"; import { SetPinComponent } from "../../auth/components/set-pin.component"; import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service"; import { DesktopSettingsService } from "../../platform/services/desktop-settings.service"; +import { NativeMessagingManifestService } from "../services/native-messaging-manifest.service"; @Component({ selector: "app-settings", @@ -126,6 +128,8 @@ export class SettingsComponent implements OnInit { private biometricStateService: BiometricStateService, private desktopAutofillSettingsService: DesktopAutofillSettingsService, private authRequestService: AuthRequestServiceAbstraction, + private logService: LogService, + private nativeMessagingManifestService: NativeMessagingManifestService, ) { const isMac = this.platformUtilsService.getDevice() === DeviceType.MacOsDesktop; @@ -628,11 +632,20 @@ export class SettingsComponent implements OnInit { } await this.stateService.setEnableBrowserIntegration(this.form.value.enableBrowserIntegration); - this.messagingService.send( - this.form.value.enableBrowserIntegration - ? "enableBrowserIntegration" - : "disableBrowserIntegration", + + const errorResult = await this.nativeMessagingManifestService.generate( + this.form.value.enableBrowserIntegration, ); + if (errorResult !== null) { + this.logService.error("Error in browser integration: " + errorResult); + await this.dialogService.openSimpleDialog({ + title: { key: "browserIntegrationErrorTitle" }, + content: { key: "browserIntegrationErrorDesc" }, + acceptButtonText: { key: "ok" }, + cancelButtonText: null, + type: "danger", + }); + } if (!this.form.value.enableBrowserIntegration) { this.form.controls.enableBrowserIntegrationFingerprint.setValue(false); @@ -651,11 +664,19 @@ export class SettingsComponent implements OnInit { await this.stateService.setDuckDuckGoSharedKey(null); } - this.messagingService.send( - this.form.value.enableDuckDuckGoBrowserIntegration - ? "enableDuckDuckGoBrowserIntegration" - : "disableDuckDuckGoBrowserIntegration", + const errorResult = await this.nativeMessagingManifestService.generateDuckDuckGo( + this.form.value.enableDuckDuckGoBrowserIntegration, ); + if (errorResult !== null) { + this.logService.error("Error in DDG browser integration: " + errorResult); + await this.dialogService.openSimpleDialog({ + title: { key: "browserIntegrationUnsupportedTitle" }, + content: errorResult.message, + acceptButtonText: { key: "ok" }, + cancelButtonText: null, + type: "warning", + }); + } } async saveBrowserIntegrationFingerprint() { diff --git a/apps/desktop/src/app/services/native-messaging-manifest.service.ts b/apps/desktop/src/app/services/native-messaging-manifest.service.ts new file mode 100644 index 0000000000..6cc58a581b --- /dev/null +++ b/apps/desktop/src/app/services/native-messaging-manifest.service.ts @@ -0,0 +1,13 @@ +import { Injectable } from "@angular/core"; + +@Injectable() +export class NativeMessagingManifestService { + constructor() {} + + async generate(create: boolean): Promise<Error | null> { + return ipc.platform.nativeMessaging.manifests.generate(create); + } + async generateDuckDuckGo(create: boolean): Promise<Error | null> { + return ipc.platform.nativeMessaging.manifests.generateDuckDuckGo(create); + } +} diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index 8e412d4977..264f26cbe2 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -76,6 +76,7 @@ import { SearchBarService } from "../layout/search/search-bar.service"; import { DesktopFileDownloadService } from "./desktop-file-download.service"; import { InitService } from "./init.service"; +import { NativeMessagingManifestService } from "./native-messaging-manifest.service"; import { RendererCryptoFunctionService } from "./renderer-crypto-function.service"; const RELOAD_CALLBACK = new SafeInjectionToken<() => any>("RELOAD_CALLBACK"); @@ -249,6 +250,11 @@ const safeProviders: SafeProvider[] = [ provide: DesktopAutofillSettingsService, deps: [StateProvider], }), + safeProvider({ + provide: NativeMessagingManifestService, + useClass: NativeMessagingManifestService, + deps: [], + }), ]; @NgModule({ diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 00eace54e2..3d2b40ac62 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -1632,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 67f08839c5..a4783e0573 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -291,12 +291,20 @@ export class Main { this.powerMonitorMain.init(); await this.updaterMain.init(); - if ( - (await this.stateService.getEnableBrowserIntegration()) || - (await firstValueFrom( - this.desktopAutofillSettingsService.enableDuckDuckGoBrowserIntegration$, - )) - ) { + const [browserIntegrationEnabled, ddgIntegrationEnabled] = await Promise.all([ + this.stateService.getEnableBrowserIntegration(), + firstValueFrom(this.desktopAutofillSettingsService.enableDuckDuckGoBrowserIntegration$), + ]); + + if (browserIntegrationEnabled || ddgIntegrationEnabled) { + // Re-register the native messaging host integrations on startup, in case they are not present + if (browserIntegrationEnabled) { + this.nativeMessagingMain.generateManifests().catch(this.logService.error); + } + if (ddgIntegrationEnabled) { + this.nativeMessagingMain.generateDdgManifests().catch(this.logService.error); + } + this.nativeMessagingMain.listen(); } diff --git a/apps/desktop/src/main/messaging.main.ts b/apps/desktop/src/main/messaging.main.ts index 256d551560..a9f80b7d20 100644 --- a/apps/desktop/src/main/messaging.main.ts +++ b/apps/desktop/src/main/messaging.main.ts @@ -75,22 +75,6 @@ export class MessagingMain { case "getWindowIsFocused": this.windowIsFocused(); break; - case "enableBrowserIntegration": - this.main.nativeMessagingMain.generateManifests(); - this.main.nativeMessagingMain.listen(); - break; - case "enableDuckDuckGoBrowserIntegration": - this.main.nativeMessagingMain.generateDdgManifests(); - this.main.nativeMessagingMain.listen(); - break; - case "disableBrowserIntegration": - this.main.nativeMessagingMain.removeManifests(); - this.main.nativeMessagingMain.stop(); - break; - case "disableDuckDuckGoBrowserIntegration": - this.main.nativeMessagingMain.removeDdgManifests(); - this.main.nativeMessagingMain.stop(); - break; default: break; } diff --git a/apps/desktop/src/main/native-messaging.main.ts b/apps/desktop/src/main/native-messaging.main.ts index 05e987e20b..d3dd25c644 100644 --- a/apps/desktop/src/main/native-messaging.main.ts +++ b/apps/desktop/src/main/native-messaging.main.ts @@ -22,7 +22,55 @@ export class NativeMessagingMain { private windowMain: WindowMain, private userPath: string, private exePath: string, - ) {} + ) { + ipcMain.handle( + "nativeMessaging.manifests", + async (_event: any, options: { create: boolean }) => { + if (options.create) { + this.listen(); + try { + await this.generateManifests(); + } catch (e) { + this.logService.error("Error generating manifests: " + e); + return e; + } + } else { + this.stop(); + try { + await this.removeManifests(); + } catch (e) { + this.logService.error("Error removing manifests: " + e); + return e; + } + } + return null; + }, + ); + + ipcMain.handle( + "nativeMessaging.ddgManifests", + async (_event: any, options: { create: boolean }) => { + if (options.create) { + this.listen(); + try { + await this.generateDdgManifests(); + } catch (e) { + this.logService.error("Error generating duckduckgo manifests: " + e); + return e; + } + } else { + this.stop(); + try { + await this.removeDdgManifests(); + } catch (e) { + this.logService.error("Error removing duckduckgo manifests: " + e); + return e; + } + } + return null; + }, + ); + } listen() { ipc.config.id = "bitwarden"; @@ -76,7 +124,7 @@ export class NativeMessagingMain { ipc.server.emit(socket, "message", message); } - generateManifests() { + async generateManifests() { const baseJson = { name: "com.8bit.bitwarden", description: "Bitwarden desktop <-> browser bridge", @@ -84,6 +132,10 @@ export class NativeMessagingMain { type: "stdio", }; + if (!existsSync(baseJson.path)) { + throw new Error(`Unable to find binary: ${baseJson.path}`); + } + const firefoxJson = { ...baseJson, ...{ allowed_extensions: ["{446900e4-71c2-419f-a6a7-df9c091e268b}"] }, @@ -92,8 +144,11 @@ export class NativeMessagingMain { ...baseJson, ...{ allowed_origins: [ + // Chrome extension "chrome-extension://nngceckbapebfimnlniiiahkandclblb/", + // Edge extension "chrome-extension://jbkfoedolllekgbhcbcoahefnbanhhlh/", + // Opera extension "chrome-extension://ccnckbpmaceehanjmeomladnmlffdjgn/", ], }, @@ -102,27 +157,17 @@ export class NativeMessagingMain { switch (process.platform) { case "win32": { const destination = path.join(this.userPath, "browsers"); - // 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.writeManifest(path.join(destination, "firefox.json"), firefoxJson); - // 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.writeManifest(path.join(destination, "chrome.json"), chromeJson); + await this.writeManifest(path.join(destination, "firefox.json"), firefoxJson); + await this.writeManifest(path.join(destination, "chrome.json"), chromeJson); - // 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.createWindowsRegistry( - "HKLM\\SOFTWARE\\Mozilla\\Firefox", - "HKCU\\SOFTWARE\\Mozilla\\NativeMessagingHosts\\com.8bit.bitwarden", - path.join(destination, "firefox.json"), - ); - // 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.createWindowsRegistry( - "HKCU\\SOFTWARE\\Google\\Chrome", - "HKCU\\SOFTWARE\\Google\\Chrome\\NativeMessagingHosts\\com.8bit.bitwarden", - path.join(destination, "chrome.json"), - ); + const nmhs = this.getWindowsNMHS(); + for (const [key, value] of Object.entries(nmhs)) { + let manifestPath = path.join(destination, "chrome.json"); + if (key === "Firefox") { + manifestPath = path.join(destination, "firefox.json"); + } + await this.createWindowsRegistry(value, manifestPath); + } break; } case "darwin": { @@ -136,38 +181,30 @@ export class NativeMessagingMain { manifest = firefoxJson; } - this.writeManifest(p, manifest).catch((e) => - this.logService.error(`Error writing manifest for ${key}. ${e}`), - ); + await this.writeManifest(p, manifest); } else { - this.logService.warning(`${key} not found skipping.`); + this.logService.warning(`${key} not found, skipping.`); } } break; } case "linux": if (existsSync(`${this.homedir()}/.mozilla/`)) { - // 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.writeManifest( + await this.writeManifest( `${this.homedir()}/.mozilla/native-messaging-hosts/com.8bit.bitwarden.json`, firefoxJson, ); } if (existsSync(`${this.homedir()}/.config/google-chrome/`)) { - // 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.writeManifest( + await this.writeManifest( `${this.homedir()}/.config/google-chrome/NativeMessagingHosts/com.8bit.bitwarden.json`, chromeJson, ); } if (existsSync(`${this.homedir()}/.config/microsoft-edge/`)) { - // 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.writeManifest( + await this.writeManifest( `${this.homedir()}/.config/microsoft-edge/NativeMessagingHosts/com.8bit.bitwarden.json`, chromeJson, ); @@ -178,20 +215,23 @@ export class NativeMessagingMain { } } - generateDdgManifests() { + async generateDdgManifests() { const manifest = { name: "com.8bit.bitwarden", description: "Bitwarden desktop <-> DuckDuckGo bridge", path: this.binaryPath(), type: "stdio", }; + + if (!existsSync(manifest.path)) { + throw new Error(`Unable to find binary: ${manifest.path}`); + } + switch (process.platform) { case "darwin": { /* eslint-disable-next-line no-useless-escape */ const path = `${this.homedir()}/Library/Containers/com.duckduckgo.macos.browser/Data/Library/Application\ Support/NativeMessagingHosts/com.8bit.bitwarden.json`; - this.writeManifest(path, manifest).catch((e) => - this.logService.error(`Error writing manifest for DuckDuckGo. ${e}`), - ); + await this.writeManifest(path, manifest); break; } default: @@ -199,86 +239,50 @@ export class NativeMessagingMain { } } - removeManifests() { + async removeManifests() { switch (process.platform) { - case "win32": - // 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 - fs.unlink(path.join(this.userPath, "browsers", "firefox.json")); - // 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 - fs.unlink(path.join(this.userPath, "browsers", "chrome.json")); - // 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.deleteWindowsRegistry( - "HKCU\\SOFTWARE\\Mozilla\\NativeMessagingHosts\\com.8bit.bitwarden", - ); - // 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.deleteWindowsRegistry( - "HKCU\\SOFTWARE\\Google\\Chrome\\NativeMessagingHosts\\com.8bit.bitwarden", - ); + case "win32": { + await this.removeIfExists(path.join(this.userPath, "browsers", "firefox.json")); + await this.removeIfExists(path.join(this.userPath, "browsers", "chrome.json")); + + const nmhs = this.getWindowsNMHS(); + for (const [, value] of Object.entries(nmhs)) { + await this.deleteWindowsRegistry(value); + } break; + } case "darwin": { const nmhs = this.getDarwinNMHS(); for (const [, value] of Object.entries(nmhs)) { - const p = path.join(value, "NativeMessagingHosts", "com.8bit.bitwarden.json"); - if (existsSync(p)) { - // 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 - fs.unlink(p); - } + await this.removeIfExists( + path.join(value, "NativeMessagingHosts", "com.8bit.bitwarden.json"), + ); } break; } - case "linux": - if ( - existsSync(`${this.homedir()}/.mozilla/native-messaging-hosts/com.8bit.bitwarden.json`) - ) { - // 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 - fs.unlink(`${this.homedir()}/.mozilla/native-messaging-hosts/com.8bit.bitwarden.json`); - } - - if ( - existsSync( - `${this.homedir()}/.config/google-chrome/NativeMessagingHosts/com.8bit.bitwarden.json`, - ) - ) { - // 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 - fs.unlink( - `${this.homedir()}/.config/google-chrome/NativeMessagingHosts/com.8bit.bitwarden.json`, - ); - } - - if ( - existsSync( - `${this.homedir()}/.config/microsoft-edge/NativeMessagingHosts/com.8bit.bitwarden.json`, - ) - ) { - // 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 - fs.unlink( - `${this.homedir()}/.config/microsoft-edge/NativeMessagingHosts/com.8bit.bitwarden.json`, - ); - } + case "linux": { + await this.removeIfExists( + `${this.homedir()}/.mozilla/native-messaging-hosts/com.8bit.bitwarden.json`, + ); + await this.removeIfExists( + `${this.homedir()}/.config/google-chrome/NativeMessagingHosts/com.8bit.bitwarden.json`, + ); + await this.removeIfExists( + `${this.homedir()}/.config/microsoft-edge/NativeMessagingHosts/com.8bit.bitwarden.json`, + ); break; + } default: break; } } - removeDdgManifests() { + async removeDdgManifests() { switch (process.platform) { case "darwin": { /* eslint-disable-next-line no-useless-escape */ const path = `${this.homedir()}/Library/Containers/com.duckduckgo.macos.browser/Data/Library/Application\ Support/NativeMessagingHosts/com.8bit.bitwarden.json`; - if (existsSync(path)) { - // 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 - fs.unlink(path); - } + await this.removeIfExists(path); break; } default: @@ -286,6 +290,16 @@ export class NativeMessagingMain { } } + private getWindowsNMHS() { + return { + Firefox: "HKCU\\SOFTWARE\\Mozilla\\NativeMessagingHosts\\com.8bit.bitwarden", + Chrome: "HKCU\\SOFTWARE\\Google\\Chrome\\NativeMessagingHosts\\com.8bit.bitwarden", + Chromium: "HKCU\\SOFTWARE\\Chromium\\NativeMessagingHosts\\com.8bit.bitwarden", + // Edge uses the same registry key as Chrome as a fallback, but it's has its own separate key as well. + "Microsoft Edge": "HKCU\\SOFTWARE\\Microsoft\\Edge\\NativeMessagingHosts\\com.8bit.bitwarden", + }; + } + private getDarwinNMHS() { /* eslint-disable no-useless-escape */ return { @@ -305,10 +319,13 @@ export class NativeMessagingMain { } private async writeManifest(destination: string, manifest: object) { + this.logService.debug(`Writing manifest: ${destination}`); + if (!existsSync(path.dirname(destination))) { await fs.mkdir(path.dirname(destination)); } - fs.writeFile(destination, JSON.stringify(manifest, null, 2)).catch(this.logService.error); + + await fs.writeFile(destination, JSON.stringify(manifest, null, 2)); } private binaryPath() { @@ -327,39 +344,26 @@ export class NativeMessagingMain { return regedit; } - private async createWindowsRegistry(check: string, location: string, jsonFile: string) { + private async createWindowsRegistry(location: string, jsonFile: string) { const regedit = this.getRegeditInstance(); - const list = util.promisify(regedit.list); const createKey = util.promisify(regedit.createKey); const putValue = util.promisify(regedit.putValue); this.logService.debug(`Adding registry: ${location}`); - // Check installed - try { - await list(check); - } catch { - this.logService.warning(`Not finding registry ${check} skipping.`); - return; - } + await createKey(location); - try { - await createKey(location); + // Insert path to manifest + const obj: any = {}; + obj[location] = { + default: { + value: jsonFile, + type: "REG_DEFAULT", + }, + }; - // Insert path to manifest - const obj: any = {}; - obj[location] = { - default: { - value: jsonFile, - type: "REG_DEFAULT", - }, - }; - - return putValue(obj); - } catch (error) { - this.logService.error(error); - } + return putValue(obj); } private async deleteWindowsRegistry(key: string) { @@ -385,4 +389,10 @@ export class NativeMessagingMain { return homedir(); } } + + private async removeIfExists(path: string) { + if (existsSync(path)) { + await fs.unlink(path); + } + } } diff --git a/apps/desktop/src/platform/preload.ts b/apps/desktop/src/platform/preload.ts index 1f6bd200e0..04819998d5 100644 --- a/apps/desktop/src/platform/preload.ts +++ b/apps/desktop/src/platform/preload.ts @@ -74,6 +74,13 @@ const nativeMessaging = { onMessage: (callback: (message: LegacyMessageWrapper | Message) => void) => { ipcRenderer.on("nativeMessaging", (_event, message) => callback(message)); }, + + manifests: { + generate: (create: boolean): Promise<Error | null> => + ipcRenderer.invoke("nativeMessaging.manifests", { create }), + generateDuckDuckGo: (create: boolean): Promise<Error | null> => + ipcRenderer.invoke("nativeMessaging.ddgManifests", { create }), + }, }; const crypto = { From 9277465951b160a98c27d55b386d4275d10d1595 Mon Sep 17 00:00:00 2001 From: Cesar Gonzalez <cesar.a.gonzalezcs@gmail.com> Date: Thu, 18 Apr 2024 11:05:16 -0500 Subject: [PATCH 218/351] [PM-5744] Adjust Fido2 Content Script Injection to Meet mv3 Requirements (#8222) * [PM-5876] Adjust LP Fileless Importer to Suppress Download with DOM Append in Manifest v3 * [PM-5876] Incorporating jest tests for affected logic * [PM-5876] Fixing jest test that leverages rxjs * [PM-5876] Updating documentation within BrowserApi.executeScriptInTab * [PM-5876] Implementing jest tests for the new LP suppress download content scripts * [PM-5876] Adding a change to webpack to ensure we do not package the mv2 side script for `lp-suppress-import-download.mv2.ts` if building the extension for mv3 * [PM-5876] Implementing changes based on feedback during code review * [PM-5876] Implementing changes based on feedback during code review * [PM-5876] Implementing changes based on feedback during code review * [PM-5876] Implementing changes based on feedback during code review * [PM-5876] Implementing a configuration to feed the script injection of the Fileless Importer CSV download supression script * [PM-5744] Adjust injection of `page-script.ts` within FIDO 2 implementation to ensure mv3 compatibility * [PM-5744] Adjusting structure of manifest.json to clean up implementation and ensure consistency between mv2 and mv3 * [PM-5744] Reverting inclusion of the ConsoleLogService * [PM-4791] Injected content scripts prevent proper XML file display and disrupt XML responses * [PM-5744] Adjust FIDO2 content script injection methodology to be compatible with manifest v3 * [PM-5744] Adjusting references to Fido2Service to mirror change of name to Fido2Background * [PM-5744] Migrating runtime background messages that are associated with Fido2 into Fido2Background * [PM-5744] Fixing named reference within Fido2Background * [PM-5744] Migrating all Fido2 messages from the runtime.background.ts script to the fido2.background.ts script * [PM-5744] Removing unnecessary dependency from runtime background * [PM-5744] Removing unnecessary dependency from runtime background * [PM-5744] Reworking how we handle init of Fido2Background * [PM-5744] Reworking page-script.ts to ensure that it can destory its global values on unload * [PM-5744] Reworking page-script.ts to ensure that it can destory its global values on unload * [PM-5744] Implementing separated injection methodology between manifest v2 and v3 * [PM-4791] Adjsuting reference for Fido2 script injection to ensure it only triggers on https protocol types * [PM-5744] Removing unnecessary message and handling reload of content scripts based on updates on observable * [PM-5744] Refactoring content-script implementation for fido2 * [PM-5744] Refactoring content-script implementation for fido2 * [PM-5744] Reworking implementation to avoid having multiple contenType checks within the FIDO2 content scripts * [PM-5744] Re-implementing the messageWithResponse within runtime.background.ts * [PM-5744] Reverting change to autofill.service.ts * [PM-5744] Removing return value from runtime.background.ts process message call * [PM-5744] Reworking how we handle injection of the fido2 page and content script elements * [PM-5744] Adjusting how we override the navigator.credentials request/reponse structure * [PM-5744] Working through jest tests for the fido2Background implementation * [PM-5744] Finalizing jest tests for the Fido2Background implementation * [PM-5744] Stubbing out jest tests for content-script and page-script * [PM-5744] Implementing a methodology that allows us to dynamically set and unset content scripts * [PM-5744] Applying cleanup to page-script.ts to lighten the footprint of the script * [PM-5744] Further simplifying page-script implementation * [PM-5744] Reworking Fido2Utils to remove references to the base Utils methods to allow the page-script.ts file to render at a lower file size * [PM-5744] Reworking Fido2Utils to remove references to the base Utils methods to allow the page-script.ts file to render at a lower file size * [PM-5744] Implementing the `RegisterContentScriptPolyfill` as a separately compiled file as opposed to an import * [PM-5744] Implementing methodology to ensure that the RegisterContentScript polyfill is not built in cases where it is not needed * [PM-5744] Implementing methodology to ensure that the RegisterContentScript polyfill is not built in cases where it is not needed * [PM-5744] Reverting package-lock.json * [PM-5744] Implementing a methodology to ensure we can instantiate the RegisterContentScript polyfill in a siloed manner * [PM-5744] Migrating chrome extension api calls to the BrowserApi class * [PM-5744] Implementing typing information within the RegisterContentScriptsPolyfill * [PM-5744] Removing any eslint-disable references within the RegisterContentScriptsPolyfill * [PM-5744] Refactoring polyfill implementation * [PM-5744] Refactoring polyfill implementation * [PM-5744] Fixing an issue where Safari was not resolving the await chrome proxy * [PM-5744] Fixing jest tests for the page-script append method * [PM-5744] Fixing an issue found where collection of page details can trigger a context invalidated message when the extension is refreshed * [PM-5744] Implementing jest tests for the added BrowserApi methods * [PM-5744] Refactoring Fido2Background implementation * [PM-5744] Refactoring Fido2Background implementation * [PM-5744] Adding enums to the implementation for the Fido2 Content Scripts and working through jest tests for the BrowserApi and Fido2Background classes * [PM-5744] Adding comments to the FIDO2 content-script.ts file * [PM-5744] Adding jest tests for the Fido2 content-script.ts * [PM-5744] Adding jest tests for the Fido2 content-script.ts * [PM-5744] Adding jest tests for the Fido2 page-script.ts * [PM-5744] Working through an attempt to jest test the page-script.ts file * [PM-5744] Finalizing jest tests for the page-script.ts implementation * [PM-5744] Applying stricter type information for the passed params within fido2-testing-utils.ts * [PM-5744] Adjusting documentation * [PM-5744] Adjusting implementation of jest tests to use mock proxies * [PM-5744] Adjusting jest tests to simply implementation * [PM-5744] Adjust jest tests based on code review feedback * [PM-5744] Adjust jest tests based on code review feedback * [PM-5744] Adjust jest tests based on code review feedback * [PM-5744] Adjusting jest tests to based on feedback * [PM-5744] Adjusting jest tests to based on feedback * [PM-5744] Adjusting jest tests to based on feedback * [PM-5744] Adjusting conditional within page-script.ts * [PM-5744] Removing unnecessary global reference to the messager * [PM-5744] Updating jest tests * [PM-5744] Updating jest tests * [PM-5744] Updating jest tests * [PM-5744] Updating jest tests * [PM-5744] Updating how we export the Fido2Background class * [PM-5744] Adding duplciate jest tests to fido2-utils.ts to ensure we maintain functionality for utils methods pulled from platform utils * [PM-5189] Addressing code review feedback * [PM-5744] Applying code review feedback, reworking obserable subscription within fido2 background * [PM-5744] Reworking jest tests to avoid mocking `firstValueFrom` * [PM-5744] Reworking jest tests to avoid usage of private methods * [PM-5744] Reworking jest tests to avoid usage of private methods * [PM-5744] Implementing jest tests for the ScriptInjectorService and updating references within the Browser Extension to use the new service * [PM-5744] Converting ScriptInjectorService to a dependnecy instead of a static class * [PM-5744] Reworking typing for the ScriptInjectorService * [PM-5744] Adjusting implementation based on feedback provided during code review * [PM-5744] Adjusting implementation based on feedback provided during code review * [PM-5744] Adjusting implementation based on feedback provided during code review * [PM-5744] Adjusting implementation based on feedback provided during code review * [PM-5744] Adjusting how the ScriptInjectorService accepts the config to simplify the data structure * [PM-5744] Updating jest tests to cover edge cases within ScriptInjectorService * [PM-5744] Updating jest tests to reference the ScriptInjectorService directly rather than the underlying ExecuteScript api call * [PM-5744] Updating jest tests to reflect provided feedback during code review * [PM-5744] Updating jest tests to reflect provided feedback during code review * [PM-5744] Updating documentation based on code review feedback * [PM-5744] Updating how we extend the abstract ScriptInjectorService * [PM-5744] Updating reference to the frame property on the ScriptInjectionConfig --- .../autofill-service.factory.ts | 8 +- .../autofill/content/autofill-init.spec.ts | 11 + .../src/autofill/content/autofill-init.ts | 21 +- .../services/autofill.service.spec.ts | 5 + .../src/autofill/services/autofill.service.ts | 19 +- .../src/autofill/spec/autofill-mocks.ts | 1 + .../src/autofill/spec/fido2-testing-utils.ts | 74 +++ .../browser/src/background/main.background.ts | 24 +- .../src/background/runtime.background.ts | 65 +-- apps/browser/src/manifest.json | 10 +- apps/browser/src/manifest.v3.json | 7 - ...browser-script-injector-service.factory.ts | 19 + ...r-api.register-content-scripts-polyfill.ts | 435 ++++++++++++++++++ .../src/platform/browser/browser-api.spec.ts | 31 ++ .../src/platform/browser/browser-api.ts | 39 ++ .../abstractions/script-injector.service.ts | 45 ++ .../browser-script-injector.service.spec.ts | 173 +++++++ .../browser-script-injector.service.ts | 78 ++++ .../src/popup/services/services.module.ts | 8 + .../fileless-importer.background.ts | 6 - .../fileless-importer.background.spec.ts | 19 +- .../fileless-importer.background.ts | 33 +- .../fileless-importer-injected-scripts.ts | 11 +- .../abstractions/fido2.background.ts | 57 +++ .../fido2/background/fido2.background.spec.ts | 414 +++++++++++++++++ .../fido2/background/fido2.background.ts | 356 ++++++++++++++ .../fido2/content/content-script.spec.ts | 164 +++++++ .../src/vault/fido2/content/content-script.ts | 234 +++++----- .../fido2/content/messaging/messenger.spec.ts | 2 +- .../fido2/content/messaging/messenger.ts | 21 +- .../content/page-script-append.mv2.spec.ts | 69 +++ .../fido2/content/page-script-append.mv2.ts | 19 + .../src/vault/fido2/content/page-script.ts | 385 ++++++++-------- .../page-script.webauthn-supported.spec.ts | 121 +++++ .../page-script.webauthn-unsupported.spec.ts | 96 ++++ ...ger-fido2-content-script-injection.spec.ts | 16 - .../trigger-fido2-content-script-injection.ts | 5 - .../fido2/enums/fido2-content-script.enum.ts | 10 + .../vault/fido2/enums/fido2-port-name.enum.ts | 3 + .../services/abstractions/fido2.service.ts | 4 - .../src/vault/services/fido2.service.spec.ts | 35 -- .../src/vault/services/fido2.service.ts | 35 -- apps/browser/test.setup.ts | 17 + apps/browser/tsconfig.json | 1 + apps/browser/webpack.config.js | 4 +- .../services/fido2/fido2-client.service.ts | 17 +- .../vault/services/fido2/fido2-utils.spec.ts | 40 ++ .../src/vault/services/fido2/fido2-utils.ts | 64 ++- 48 files changed, 2767 insertions(+), 564 deletions(-) create mode 100644 apps/browser/src/autofill/spec/fido2-testing-utils.ts create mode 100644 apps/browser/src/platform/background/service-factories/browser-script-injector-service.factory.ts create mode 100644 apps/browser/src/platform/browser/browser-api.register-content-scripts-polyfill.ts create mode 100644 apps/browser/src/platform/services/abstractions/script-injector.service.ts create mode 100644 apps/browser/src/platform/services/browser-script-injector.service.spec.ts create mode 100644 apps/browser/src/platform/services/browser-script-injector.service.ts create mode 100644 apps/browser/src/vault/fido2/background/abstractions/fido2.background.ts create mode 100644 apps/browser/src/vault/fido2/background/fido2.background.spec.ts create mode 100644 apps/browser/src/vault/fido2/background/fido2.background.ts create mode 100644 apps/browser/src/vault/fido2/content/content-script.spec.ts create mode 100644 apps/browser/src/vault/fido2/content/page-script-append.mv2.spec.ts create mode 100644 apps/browser/src/vault/fido2/content/page-script-append.mv2.ts create mode 100644 apps/browser/src/vault/fido2/content/page-script.webauthn-supported.spec.ts create mode 100644 apps/browser/src/vault/fido2/content/page-script.webauthn-unsupported.spec.ts delete mode 100644 apps/browser/src/vault/fido2/content/trigger-fido2-content-script-injection.spec.ts delete mode 100644 apps/browser/src/vault/fido2/content/trigger-fido2-content-script-injection.ts create mode 100644 apps/browser/src/vault/fido2/enums/fido2-content-script.enum.ts create mode 100644 apps/browser/src/vault/fido2/enums/fido2-port-name.enum.ts delete mode 100644 apps/browser/src/vault/services/abstractions/fido2.service.ts delete mode 100644 apps/browser/src/vault/services/fido2.service.spec.ts delete mode 100644 apps/browser/src/vault/services/fido2.service.ts create mode 100644 libs/common/src/vault/services/fido2/fido2-utils.spec.ts 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 c948f7aa94..7b423ca4f4 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 @@ -7,6 +7,10 @@ import { eventCollectionServiceFactory, } from "../../../background/service-factories/event-collection-service.factory"; import { billingAccountProfileStateServiceFactory } from "../../../platform/background/service-factories/billing-account-profile-state-service.factory"; +import { + browserScriptInjectorServiceFactory, + BrowserScriptInjectorServiceInitOptions, +} from "../../../platform/background/service-factories/browser-script-injector-service.factory"; import { CachedServices, factory, @@ -45,7 +49,8 @@ export type AutoFillServiceInitOptions = AutoFillServiceOptions & EventCollectionServiceInitOptions & LogServiceInitOptions & UserVerificationServiceInitOptions & - DomainSettingsServiceInitOptions; + DomainSettingsServiceInitOptions & + BrowserScriptInjectorServiceInitOptions; export function autofillServiceFactory( cache: { autofillService?: AbstractAutoFillService } & CachedServices, @@ -65,6 +70,7 @@ export function autofillServiceFactory( await domainSettingsServiceFactory(cache, opts), await userVerificationServiceFactory(cache, opts), await billingAccountProfileStateServiceFactory(cache, opts), + await browserScriptInjectorServiceFactory(cache, opts), ), ); } diff --git a/apps/browser/src/autofill/content/autofill-init.spec.ts b/apps/browser/src/autofill/content/autofill-init.spec.ts index b299ddccbf..8f1a8bf992 100644 --- a/apps/browser/src/autofill/content/autofill-init.spec.ts +++ b/apps/browser/src/autofill/content/autofill-init.spec.ts @@ -560,6 +560,17 @@ describe("AutofillInit", () => { }); describe("destroy", () => { + it("clears the timeout used to collect page details on load", () => { + jest.spyOn(window, "clearTimeout"); + + autofillInit.init(); + autofillInit.destroy(); + + expect(window.clearTimeout).toHaveBeenCalledWith( + autofillInit["collectPageDetailsOnLoadTimeout"], + ); + }); + it("removes the extension message listeners", () => { autofillInit.destroy(); diff --git a/apps/browser/src/autofill/content/autofill-init.ts b/apps/browser/src/autofill/content/autofill-init.ts index 2de35dee20..e78a1fb5ee 100644 --- a/apps/browser/src/autofill/content/autofill-init.ts +++ b/apps/browser/src/autofill/content/autofill-init.ts @@ -16,6 +16,7 @@ class AutofillInit implements AutofillInitInterface { private readonly domElementVisibilityService: DomElementVisibilityService; private readonly collectAutofillContentService: CollectAutofillContentService; private readonly insertAutofillContentService: InsertAutofillContentService; + private collectPageDetailsOnLoadTimeout: number | NodeJS.Timeout | undefined; private readonly extensionMessageHandlers: AutofillExtensionMessageHandlers = { collectPageDetails: ({ message }) => this.collectPageDetails(message), collectPageDetailsImmediately: ({ message }) => this.collectPageDetails(message, true), @@ -66,17 +67,19 @@ class AutofillInit implements AutofillInitInterface { * to act on the page. */ private collectPageDetailsOnLoad() { - const sendCollectDetailsMessage = () => - setTimeout( + const sendCollectDetailsMessage = () => { + this.clearCollectPageDetailsOnLoadTimeout(); + this.collectPageDetailsOnLoadTimeout = setTimeout( () => sendExtensionMessage("bgCollectPageDetails", { sender: "autofillInit" }), 250, ); + }; - if (document.readyState === "complete") { + if (globalThis.document.readyState === "complete") { sendCollectDetailsMessage(); } - window.addEventListener("load", sendCollectDetailsMessage); + globalThis.addEventListener("load", sendCollectDetailsMessage); } /** @@ -247,6 +250,15 @@ class AutofillInit implements AutofillInitInterface { this.autofillOverlayContentService.autofillOverlayVisibility = data?.autofillOverlayVisibility; } + /** + * Clears the send collect details message timeout. + */ + private clearCollectPageDetailsOnLoadTimeout() { + if (this.collectPageDetailsOnLoadTimeout) { + clearTimeout(this.collectPageDetailsOnLoadTimeout); + } + } + /** * Sets up the extension message listeners for the content script. */ @@ -288,6 +300,7 @@ class AutofillInit implements AutofillInitInterface { * listeners, timeouts, and object instances to prevent memory leaks. */ destroy() { + this.clearCollectPageDetailsOnLoadTimeout(); chrome.runtime.onMessage.removeListener(this.handleExtensionMessage); this.collectAutofillContentService.destroy(); this.autofillOverlayContentService?.destroy(); diff --git a/apps/browser/src/autofill/services/autofill.service.spec.ts b/apps/browser/src/autofill/services/autofill.service.spec.ts index 4db64f417d..5064d2e7df 100644 --- a/apps/browser/src/autofill/services/autofill.service.spec.ts +++ b/apps/browser/src/autofill/services/autofill.service.spec.ts @@ -32,6 +32,7 @@ import { CipherService } from "@bitwarden/common/vault/services/cipher.service"; import { TotpService } from "@bitwarden/common/vault/services/totp.service"; import { BrowserApi } from "../../platform/browser/browser-api"; +import { BrowserScriptInjectorService } from "../../platform/services/browser-script-injector.service"; import { AutofillPort } from "../enums/autofill-port.enums"; import AutofillField from "../models/autofill-field"; import AutofillPageDetails from "../models/autofill-page-details"; @@ -67,6 +68,7 @@ describe("AutofillService", () => { const accountService: FakeAccountService = mockAccountServiceWith(mockUserId); const fakeStateProvider: FakeStateProvider = new FakeStateProvider(accountService); let domainSettingsService: DomainSettingsService; + let scriptInjectorService: BrowserScriptInjectorService; const totpService = mock<TotpService>(); const eventCollectionService = mock<EventCollectionService>(); const logService = mock<LogService>(); @@ -74,6 +76,7 @@ describe("AutofillService", () => { const billingAccountProfileStateService = mock<BillingAccountProfileStateService>(); beforeEach(() => { + scriptInjectorService = new BrowserScriptInjectorService(); autofillService = new AutofillService( cipherService, autofillSettingsService, @@ -83,6 +86,7 @@ describe("AutofillService", () => { domainSettingsService, userVerificationService, billingAccountProfileStateService, + scriptInjectorService, ); domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider); @@ -250,6 +254,7 @@ describe("AutofillService", () => { expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith(tabMock.id, { file: "content/content-message-handler.js", + frameId: 0, ...defaultExecuteScriptOptions, }); }); diff --git a/apps/browser/src/autofill/services/autofill.service.ts b/apps/browser/src/autofill/services/autofill.service.ts index 8b33d03419..6cf58558dc 100644 --- a/apps/browser/src/autofill/services/autofill.service.ts +++ b/apps/browser/src/autofill/services/autofill.service.ts @@ -20,6 +20,7 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FieldView } from "@bitwarden/common/vault/models/view/field.view"; import { BrowserApi } from "../../platform/browser/browser-api"; +import { ScriptInjectorService } from "../../platform/services/abstractions/script-injector.service"; import { openVaultItemPasswordRepromptPopout } from "../../vault/popup/utils/vault-popout-window"; import { AutofillPort } from "../enums/autofill-port.enums"; import AutofillField from "../models/autofill-field"; @@ -55,6 +56,7 @@ export default class AutofillService implements AutofillServiceInterface { private domainSettingsService: DomainSettingsService, private userVerificationService: UserVerificationService, private billingAccountProfileStateService: BillingAccountProfileStateService, + private scriptInjectorService: ScriptInjectorService, ) {} /** @@ -113,19 +115,22 @@ export default class AutofillService implements AutofillServiceInterface { if (triggeringOnPageLoad && autoFillOnPageLoadIsEnabled) { injectedScripts.push("autofiller.js"); } else { - await BrowserApi.executeScriptInTab(tab.id, { - file: "content/content-message-handler.js", - runAt: "document_start", + await this.scriptInjectorService.inject({ + tabId: tab.id, + injectDetails: { file: "content/content-message-handler.js", runAt: "document_start" }, }); } injectedScripts.push("notificationBar.js", "contextMenuHandler.js"); for (const injectedScript of injectedScripts) { - await BrowserApi.executeScriptInTab(tab.id, { - file: `content/${injectedScript}`, - frameId, - runAt: "document_start", + await this.scriptInjectorService.inject({ + tabId: tab.id, + injectDetails: { + file: `content/${injectedScript}`, + runAt: "document_start", + frame: frameId, + }, }); } } diff --git a/apps/browser/src/autofill/spec/autofill-mocks.ts b/apps/browser/src/autofill/spec/autofill-mocks.ts index 708489c57e..9c957f6b1b 100644 --- a/apps/browser/src/autofill/spec/autofill-mocks.ts +++ b/apps/browser/src/autofill/spec/autofill-mocks.ts @@ -267,6 +267,7 @@ function createPortSpyMock(name: string) { disconnect: jest.fn(), sender: { tab: createChromeTabMock(), + url: "https://jest-testing-website.com", }, }); } diff --git a/apps/browser/src/autofill/spec/fido2-testing-utils.ts b/apps/browser/src/autofill/spec/fido2-testing-utils.ts new file mode 100644 index 0000000000..c9b39c16cc --- /dev/null +++ b/apps/browser/src/autofill/spec/fido2-testing-utils.ts @@ -0,0 +1,74 @@ +import { mock } from "jest-mock-extended"; + +import { + AssertCredentialResult, + CreateCredentialResult, +} from "@bitwarden/common/vault/abstractions/fido2/fido2-client.service.abstraction"; + +export function createCredentialCreationOptionsMock( + customFields: Partial<CredentialCreationOptions> = {}, +): CredentialCreationOptions { + return mock<CredentialCreationOptions>({ + publicKey: mock<PublicKeyCredentialCreationOptions>({ + authenticatorSelection: { authenticatorAttachment: "platform" }, + excludeCredentials: [{ id: new ArrayBuffer(32), type: "public-key" }], + pubKeyCredParams: [{ alg: -7, type: "public-key" }], + user: { id: new ArrayBuffer(32), name: "test", displayName: "test" }, + }), + ...customFields, + }); +} + +export function createCreateCredentialResultMock( + customFields: Partial<CreateCredentialResult> = {}, +): CreateCredentialResult { + return mock<CreateCredentialResult>({ + credentialId: "mock", + clientDataJSON: "mock", + attestationObject: "mock", + authData: "mock", + publicKey: "mock", + publicKeyAlgorithm: -7, + transports: ["internal"], + ...customFields, + }); +} + +export function createCredentialRequestOptionsMock( + customFields: Partial<CredentialRequestOptions> = {}, +): CredentialRequestOptions { + return mock<CredentialRequestOptions>({ + mediation: "optional", + publicKey: mock<PublicKeyCredentialRequestOptions>({ + allowCredentials: [{ id: new ArrayBuffer(32), type: "public-key" }], + }), + ...customFields, + }); +} + +export function createAssertCredentialResultMock( + customFields: Partial<AssertCredentialResult> = {}, +): AssertCredentialResult { + return mock<AssertCredentialResult>({ + credentialId: "mock", + clientDataJSON: "mock", + authenticatorData: "mock", + signature: "mock", + userHandle: "mock", + ...customFields, + }); +} + +export function setupMockedWebAuthnSupport() { + (globalThis as any).PublicKeyCredential = class PolyfillPublicKeyCredential { + static isUserVerifyingPlatformAuthenticatorAvailable = () => Promise.resolve(true); + }; + (globalThis as any).AuthenticatorAttestationResponse = + class PolyfillAuthenticatorAttestationResponse {}; + (globalThis as any).AuthenticatorAssertionResponse = + class PolyfillAuthenticatorAssertionResponse {}; + (globalThis as any).navigator.credentials = { + create: jest.fn().mockResolvedValue({}), + get: jest.fn().mockResolvedValue({}), + }; +} diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 69ed4cfa3d..325a7f1943 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -214,6 +214,7 @@ import BrowserLocalStorageService from "../platform/services/browser-local-stora import BrowserMemoryStorageService from "../platform/services/browser-memory-storage.service"; import BrowserMessagingPrivateModeBackgroundService from "../platform/services/browser-messaging-private-mode-background.service"; import BrowserMessagingService from "../platform/services/browser-messaging.service"; +import { BrowserScriptInjectorService } from "../platform/services/browser-script-injector.service"; import { DefaultBrowserStateService } from "../platform/services/default-browser-state.service"; import I18nService from "../platform/services/i18n.service"; import { LocalBackedSessionStorageService } from "../platform/services/local-backed-session-storage.service"; @@ -223,9 +224,9 @@ import { BackgroundDerivedStateProvider } from "../platform/state/background-der import { BackgroundMemoryStorageService } from "../platform/storage/background-memory-storage.service"; import VaultTimeoutService from "../services/vault-timeout/vault-timeout.service"; import FilelessImporterBackground from "../tools/background/fileless-importer.background"; +import { Fido2Background as Fido2BackgroundAbstraction } from "../vault/fido2/background/abstractions/fido2.background"; +import { Fido2Background } from "../vault/fido2/background/fido2.background"; import { BrowserFido2UserInterfaceService } from "../vault/fido2/browser-fido2-user-interface.service"; -import { Fido2Service as Fido2ServiceAbstraction } from "../vault/services/abstractions/fido2.service"; -import Fido2Service from "../vault/services/fido2.service"; import { VaultFilterService } from "../vault/services/vault-filter.service"; import CommandsBackground from "./commands.background"; @@ -316,7 +317,7 @@ export default class MainBackground { activeUserStateProvider: ActiveUserStateProvider; derivedStateProvider: DerivedStateProvider; stateProvider: StateProvider; - fido2Service: Fido2ServiceAbstraction; + fido2Background: Fido2BackgroundAbstraction; individualVaultExportService: IndividualVaultExportServiceAbstraction; organizationVaultExportService: OrganizationVaultExportServiceAbstraction; vaultSettingsService: VaultSettingsServiceAbstraction; @@ -324,6 +325,7 @@ export default class MainBackground { stateEventRunnerService: StateEventRunnerService; ssoLoginService: SsoLoginServiceAbstraction; billingAccountProfileStateService: BillingAccountProfileStateService; + scriptInjectorService: BrowserScriptInjectorService; onUpdatedRan: boolean; onReplacedRan: boolean; @@ -791,6 +793,7 @@ export default class MainBackground { ); this.totpService = new TotpService(this.cryptoFunctionService, this.logService); + this.scriptInjectorService = new BrowserScriptInjectorService(); this.autofillService = new AutofillService( this.cipherService, this.autofillSettingsService, @@ -800,6 +803,7 @@ export default class MainBackground { this.domainSettingsService, this.userVerificationService, this.billingAccountProfileStateService, + this.scriptInjectorService, ); this.auditService = new AuditService(this.cryptoFunctionService, this.apiService); @@ -849,7 +853,6 @@ export default class MainBackground { this.messagingService, ); - this.fido2Service = new Fido2Service(); this.fido2UserInterfaceService = new BrowserFido2UserInterfaceService(this.authService); this.fido2AuthenticatorService = new Fido2AuthenticatorService( this.cipherService, @@ -890,11 +893,16 @@ export default class MainBackground { // Background if (!this.popupOnlyContext) { + this.fido2Background = new Fido2Background( + this.logService, + this.fido2ClientService, + this.vaultSettingsService, + this.scriptInjectorService, + ); this.runtimeBackground = new RuntimeBackground( this, this.autofillService, this.platformUtilsService as BrowserPlatformUtilsService, - this.i18nService, this.notificationsService, this.stateService, this.autofillSettingsService, @@ -903,7 +911,7 @@ export default class MainBackground { this.messagingService, this.logService, this.configService, - this.fido2Service, + this.fido2Background, ); this.nativeMessagingBackground = new NativeMessagingBackground( this.accountService, @@ -959,6 +967,7 @@ export default class MainBackground { this.notificationBackground, this.importService, this.syncService, + this.scriptInjectorService, ); this.tabsBackground = new TabsBackground( this, @@ -1045,11 +1054,12 @@ export default class MainBackground { await this.stateService.init({ runMigrations: !this.isPrivateMode }); await (this.i18nService as I18nService).init(); - (this.eventUploadService as EventUploadService).init(true); + await (this.eventUploadService as EventUploadService).init(true); this.twoFactorService.init(); if (!this.popupOnlyContext) { await this.vaultTimeoutService.init(true); + this.fido2Background.init(); await this.runtimeBackground.init(); await this.notificationBackground.init(); this.filelessImporterBackground.init(); diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts index a88bc051d8..44fe4818e0 100644 --- a/apps/browser/src/background/runtime.background.ts +++ b/apps/browser/src/background/runtime.background.ts @@ -4,7 +4,6 @@ import { NotificationsService } from "@bitwarden/common/abstractions/notificatio import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; 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"; import { SystemService } from "@bitwarden/common/platform/abstractions/system.service"; @@ -22,8 +21,7 @@ import { BrowserApi } from "../platform/browser/browser-api"; import { BrowserStateService } from "../platform/services/abstractions/browser-state.service"; import { BrowserEnvironmentService } from "../platform/services/browser-environment.service"; import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service"; -import { AbortManager } from "../vault/background/abort-manager"; -import { Fido2Service } from "../vault/services/abstractions/fido2.service"; +import { Fido2Background } from "../vault/fido2/background/abstractions/fido2.background"; import MainBackground from "./main.background"; @@ -32,13 +30,11 @@ export default class RuntimeBackground { private pageDetailsToAutoFill: any[] = []; private onInstalledReason: string = null; private lockedVaultPendingNotifications: LockedVaultPendingNotificationsData[] = []; - private abortManager = new AbortManager(); constructor( private main: MainBackground, private autofillService: AutofillService, private platformUtilsService: BrowserPlatformUtilsService, - private i18nService: I18nService, private notificationsService: NotificationsService, private stateService: BrowserStateService, private autofillSettingsService: AutofillSettingsServiceAbstraction, @@ -47,7 +43,7 @@ export default class RuntimeBackground { private messagingService: MessagingService, private logService: LogService, private configService: ConfigService, - private fido2Service: Fido2Service, + private fido2Background: Fido2Background, ) { // onInstalled listener must be wired up before anything else, so we do it in the ctor chrome.runtime.onInstalled.addListener((details: any) => { @@ -66,12 +62,7 @@ export default class RuntimeBackground { sender: chrome.runtime.MessageSender, sendResponse: any, ) => { - const messagesWithResponse = [ - "checkFido2FeatureEnabled", - "fido2RegisterCredentialRequest", - "fido2GetCredentialRequest", - "biometricUnlock", - ]; + const messagesWithResponse = ["biometricUnlock"]; if (messagesWithResponse.includes(msg.command)) { this.processMessage(msg, sender).then( @@ -81,10 +72,7 @@ export default class RuntimeBackground { return true; } - // 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.processMessage(msg, sender); - return false; + this.processMessage(msg, sender).catch((e) => this.logService.error(e)); }; BrowserApi.messageListener("runtime.background", backgroundMessageListener); @@ -269,46 +257,6 @@ export default class RuntimeBackground { case "getClickedElementResponse": this.platformUtilsService.copyToClipboard(msg.identifier); break; - case "triggerFido2ContentScriptInjection": - await this.fido2Service.injectFido2ContentScripts(sender); - break; - case "fido2AbortRequest": - this.abortManager.abort(msg.abortedRequestId); - break; - case "checkFido2FeatureEnabled": - return await this.main.fido2ClientService.isFido2FeatureEnabled(msg.hostname, msg.origin); - case "fido2RegisterCredentialRequest": - return await this.abortManager.runWithAbortController( - msg.requestId, - async (abortController) => { - try { - return await this.main.fido2ClientService.createCredential( - msg.data, - sender.tab, - abortController, - ); - } finally { - await BrowserApi.focusTab(sender.tab.id); - await BrowserApi.focusWindow(sender.tab.windowId); - } - }, - ); - case "fido2GetCredentialRequest": - return await this.abortManager.runWithAbortController( - msg.requestId, - async (abortController) => { - try { - return await this.main.fido2ClientService.assertCredential( - msg.data, - sender.tab, - abortController, - ); - } finally { - await BrowserApi.focusTab(sender.tab.id); - await BrowserApi.focusWindow(sender.tab.windowId); - } - }, - ); case "switchAccount": { await this.main.switchAccount(msg.userId); break; @@ -343,9 +291,8 @@ export default class RuntimeBackground { private async checkOnInstalled() { setTimeout(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 - this.autofillService.loadAutofillScriptsOnInstall(); + void this.fido2Background.injectFido2ContentScriptsInAllTabs(); + void this.autofillService.loadAutofillScriptsOnInstall(); if (this.onInstalledReason != null) { if (this.onInstalledReason === "install") { diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index aec7523d5e..78f1e2cc41 100644 --- a/apps/browser/src/manifest.json +++ b/apps/browser/src/manifest.json @@ -22,13 +22,6 @@ "exclude_matches": ["*://*/*.xml*", "file:///*.xml*"], "run_at": "document_start" }, - { - "all_frames": true, - "js": ["content/fido2/trigger-fido2-content-script-injection.js"], - "matches": ["https://*/*"], - "exclude_matches": ["https://*/*.xml*"], - "run_at": "document_start" - }, { "all_frames": true, "css": ["content/autofill.css"], @@ -67,7 +60,8 @@ "clipboardWrite", "idle", "webRequest", - "webRequestBlocking" + "webRequestBlocking", + "webNavigation" ], "optional_permissions": ["nativeMessaging", "privacy"], "content_security_policy": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'", diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index d67b4affab..6c58b405f4 100644 --- a/apps/browser/src/manifest.v3.json +++ b/apps/browser/src/manifest.v3.json @@ -23,13 +23,6 @@ "exclude_matches": ["*://*/*.xml*", "file:///*.xml*"], "run_at": "document_start" }, - { - "all_frames": true, - "js": ["content/fido2/trigger-fido2-content-script-injection.js"], - "matches": ["https://*/*"], - "exclude_matches": ["https://*/*.xml*"], - "run_at": "document_start" - }, { "all_frames": true, "css": ["content/autofill.css"], diff --git a/apps/browser/src/platform/background/service-factories/browser-script-injector-service.factory.ts b/apps/browser/src/platform/background/service-factories/browser-script-injector-service.factory.ts new file mode 100644 index 0000000000..e3bc687f28 --- /dev/null +++ b/apps/browser/src/platform/background/service-factories/browser-script-injector-service.factory.ts @@ -0,0 +1,19 @@ +import { BrowserScriptInjectorService } from "../../services/browser-script-injector.service"; + +import { CachedServices, FactoryOptions, factory } from "./factory-options"; + +type BrowserScriptInjectorServiceOptions = FactoryOptions; + +export type BrowserScriptInjectorServiceInitOptions = BrowserScriptInjectorServiceOptions; + +export function browserScriptInjectorServiceFactory( + cache: { browserScriptInjectorService?: BrowserScriptInjectorService } & CachedServices, + opts: BrowserScriptInjectorServiceInitOptions, +): Promise<BrowserScriptInjectorService> { + return factory( + cache, + "browserScriptInjectorService", + opts, + async () => new BrowserScriptInjectorService(), + ); +} diff --git a/apps/browser/src/platform/browser/browser-api.register-content-scripts-polyfill.ts b/apps/browser/src/platform/browser/browser-api.register-content-scripts-polyfill.ts new file mode 100644 index 0000000000..8a20f3e999 --- /dev/null +++ b/apps/browser/src/platform/browser/browser-api.register-content-scripts-polyfill.ts @@ -0,0 +1,435 @@ +/** + * MIT License + * + * Copyright (c) Federico Brigante <me@fregante.com> (https://fregante.com) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * @see https://github.com/fregante/content-scripts-register-polyfill + * @version 4.0.2 + */ +import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; + +import { BrowserApi } from "./browser-api"; + +let registerContentScripts: ( + contentScriptOptions: browser.contentScripts.RegisteredContentScriptOptions, + callback?: (registeredContentScript: browser.contentScripts.RegisteredContentScript) => void, +) => Promise<browser.contentScripts.RegisteredContentScript>; +export async function registerContentScriptsPolyfill( + contentScriptOptions: browser.contentScripts.RegisteredContentScriptOptions, + callback?: (registeredContentScript: browser.contentScripts.RegisteredContentScript) => void, +) { + if (!registerContentScripts) { + registerContentScripts = buildRegisterContentScriptsPolyfill(); + } + + return registerContentScripts(contentScriptOptions, callback); +} + +function buildRegisterContentScriptsPolyfill() { + const logService = new ConsoleLogService(false); + const chromeProxy = globalThis.chrome && NestedProxy<typeof globalThis.chrome>(globalThis.chrome); + const patternValidationRegex = + /^(https?|wss?|file|ftp|\*):\/\/(\*|\*\.[^*/]+|[^*/]+)\/.*$|^file:\/\/\/.*$|^resource:\/\/(\*|\*\.[^*/]+|[^*/]+)\/.*$|^about:/; + const isFirefox = globalThis.navigator?.userAgent.includes("Firefox/"); + const gotScripting = Boolean(globalThis.chrome?.scripting); + const gotNavigation = typeof chrome === "object" && "webNavigation" in chrome; + + function NestedProxy<T extends object>(target: T): T { + return new Proxy(target, { + get(target, prop) { + if (!target[prop as keyof T]) { + return; + } + + if (typeof target[prop as keyof T] !== "function") { + return NestedProxy(target[prop as keyof T]); + } + + return (...arguments_: any[]) => + new Promise((resolve, reject) => { + target[prop as keyof T](...arguments_, (result: any) => { + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError.message)); + } else { + resolve(result); + } + }); + }); + }, + }); + } + + function assertValidPattern(matchPattern: string) { + if (!isValidPattern(matchPattern)) { + throw new Error( + `${matchPattern} is an invalid pattern, it must match ${String(patternValidationRegex)}`, + ); + } + } + + function isValidPattern(matchPattern: string) { + return matchPattern === "<all_urls>" || patternValidationRegex.test(matchPattern); + } + + function getRawPatternRegex(matchPattern: string) { + assertValidPattern(matchPattern); + let [, protocol, host = "", pathname] = matchPattern.split(/(^[^:]+:[/][/])([^/]+)?/); + protocol = protocol + .replace("*", isFirefox ? "(https?|wss?)" : "https?") + .replaceAll(/[/]/g, "[/]"); + + if (host === "*") { + host = "[^/]+"; + } else if (host) { + host = host + .replace(/^[*][.]/, "([^/]+.)*") + .replaceAll(/[.]/g, "[.]") + .replace(/[*]$/, "[^.]+"); + } + + pathname = pathname + .replaceAll(/[/]/g, "[/]") + .replaceAll(/[.]/g, "[.]") + .replaceAll(/[*]/g, ".*"); + + return "^" + protocol + host + "(" + pathname + ")?$"; + } + + function patternToRegex(...matchPatterns: string[]) { + if (matchPatterns.length === 0) { + return /$./; + } + + if (matchPatterns.includes("<all_urls>")) { + // <all_urls> regex + return /^(https?|file|ftp):[/]+/; + } + + if (matchPatterns.includes("*://*/*")) { + // all stars regex + return isFirefox ? /^(https?|wss?):[/][/][^/]+([/].*)?$/ : /^https?:[/][/][^/]+([/].*)?$/; + } + + return new RegExp(matchPatterns.map((x) => getRawPatternRegex(x)).join("|")); + } + + function castAllFramesTarget(target: number | { tabId: number; frameId: number }) { + if (typeof target === "object") { + return { ...target, allFrames: false }; + } + + return { + tabId: target, + frameId: undefined, + allFrames: true, + }; + } + + function castArray(possibleArray: any | any[]) { + if (Array.isArray(possibleArray)) { + return possibleArray; + } + + return [possibleArray]; + } + + function arrayOrUndefined(value?: number) { + return value === undefined ? undefined : [value]; + } + + async function insertCSS( + { + tabId, + frameId, + files, + allFrames, + matchAboutBlank, + runAt, + }: { + tabId: number; + frameId?: number; + files: browser.extensionTypes.ExtensionFileOrCode[]; + allFrames: boolean; + matchAboutBlank: boolean; + runAt: browser.extensionTypes.RunAt; + }, + { ignoreTargetErrors }: { ignoreTargetErrors?: boolean } = {}, + ) { + const everyInsertion = Promise.all( + files.map(async (content) => { + if (typeof content === "string") { + content = { file: content }; + } + + if (gotScripting) { + return chrome.scripting.insertCSS({ + target: { + tabId, + frameIds: arrayOrUndefined(frameId), + allFrames: frameId === undefined ? allFrames : undefined, + }, + files: "file" in content ? [content.file] : undefined, + css: "code" in content ? content.code : undefined, + }); + } + + return chromeProxy.tabs.insertCSS(tabId, { + ...content, + matchAboutBlank, + allFrames, + frameId, + runAt: runAt ?? "document_start", + }); + }), + ); + + if (ignoreTargetErrors) { + await catchTargetInjectionErrors(everyInsertion); + } else { + await everyInsertion; + } + } + function assertNoCode(files: browser.extensionTypes.ExtensionFileOrCode[]) { + if (files.some((content) => "code" in content)) { + throw new Error("chrome.scripting does not support injecting strings of `code`"); + } + } + + async function executeScript( + { + tabId, + frameId, + files, + allFrames, + matchAboutBlank, + runAt, + }: { + tabId: number; + frameId?: number; + files: browser.extensionTypes.ExtensionFileOrCode[]; + allFrames: boolean; + matchAboutBlank: boolean; + runAt: browser.extensionTypes.RunAt; + }, + { ignoreTargetErrors }: { ignoreTargetErrors?: boolean } = {}, + ) { + const normalizedFiles = files.map((file) => (typeof file === "string" ? { file } : file)); + + if (gotScripting) { + assertNoCode(normalizedFiles); + const injection = chrome.scripting.executeScript({ + target: { + tabId, + frameIds: arrayOrUndefined(frameId), + allFrames: frameId === undefined ? allFrames : undefined, + }, + files: normalizedFiles.map(({ file }: { file: string }) => file), + }); + + if (ignoreTargetErrors) { + await catchTargetInjectionErrors(injection); + } else { + await injection; + } + + return; + } + + const executions = []; + for (const content of normalizedFiles) { + if ("code" in content) { + await executions.at(-1); + } + + executions.push( + chromeProxy.tabs.executeScript(tabId, { + ...content, + matchAboutBlank, + allFrames, + frameId, + runAt, + }), + ); + } + + if (ignoreTargetErrors) { + await catchTargetInjectionErrors(Promise.all(executions)); + } else { + await Promise.all(executions); + } + } + + async function injectContentScript( + where: { tabId: number; frameId: number }, + scripts: { + css: browser.extensionTypes.ExtensionFileOrCode[]; + js: browser.extensionTypes.ExtensionFileOrCode[]; + matchAboutBlank: boolean; + runAt: browser.extensionTypes.RunAt; + }, + options = {}, + ) { + const targets = castArray(where); + await Promise.all( + targets.map(async (target) => + injectContentScriptInSpecificTarget(castAllFramesTarget(target), scripts, options), + ), + ); + } + + async function injectContentScriptInSpecificTarget( + { frameId, tabId, allFrames }: { frameId?: number; tabId: number; allFrames: boolean }, + scripts: { + css: browser.extensionTypes.ExtensionFileOrCode[]; + js: browser.extensionTypes.ExtensionFileOrCode[]; + matchAboutBlank: boolean; + runAt: browser.extensionTypes.RunAt; + }, + options = {}, + ) { + const injections = castArray(scripts).flatMap((script) => [ + insertCSS( + { + tabId, + frameId, + allFrames, + files: script.css ?? [], + matchAboutBlank: script.matchAboutBlank ?? script.match_about_blank, + runAt: script.runAt ?? script.run_at, + }, + options, + ), + executeScript( + { + tabId, + frameId, + allFrames, + files: script.js ?? [], + matchAboutBlank: script.matchAboutBlank ?? script.match_about_blank, + runAt: script.runAt ?? script.run_at, + }, + options, + ), + ]); + await Promise.all(injections); + } + + async function catchTargetInjectionErrors(promise: Promise<any>) { + try { + await promise; + } catch (error) { + const targetErrors = + /^No frame with id \d+ in tab \d+.$|^No tab with id: \d+.$|^The tab was closed.$|^The frame was removed.$/; + if (!targetErrors.test(error?.message)) { + throw error; + } + } + } + + async function isOriginPermitted(url: string) { + return chromeProxy.permissions.contains({ + origins: [new URL(url).origin + "/*"], + }); + } + + return async ( + contentScriptOptions: browser.contentScripts.RegisteredContentScriptOptions, + callback: CallableFunction, + ) => { + const { + js = [], + css = [], + matchAboutBlank, + matches = [], + excludeMatches, + runAt, + } = contentScriptOptions; + let { allFrames } = contentScriptOptions; + + if (gotNavigation) { + allFrames = false; + } else if (allFrames) { + logService.warning( + "`allFrames: true` requires the `webNavigation` permission to work correctly: https://github.com/fregante/content-scripts-register-polyfill#permissions", + ); + } + + if (matches.length === 0) { + throw new Error( + "Type error for parameter contentScriptOptions (Error processing matches: Array requires at least 1 items; you have 0) for contentScripts.register.", + ); + } + + await Promise.all( + matches.map(async (pattern: string) => { + if (!(await chromeProxy.permissions.contains({ origins: [pattern] }))) { + throw new Error(`Permission denied to register a content script for ${pattern}`); + } + }), + ); + + const matchesRegex = patternToRegex(...matches); + const excludeMatchesRegex = patternToRegex( + ...(excludeMatches !== null && excludeMatches !== void 0 ? excludeMatches : []), + ); + const inject = async (url: string, tabId: number, frameId = 0) => { + if ( + !matchesRegex.test(url) || + excludeMatchesRegex.test(url) || + !(await isOriginPermitted(url)) + ) { + return; + } + + await injectContentScript( + { tabId, frameId }, + { css, js, matchAboutBlank, runAt }, + { ignoreTargetErrors: true }, + ); + }; + const tabListener = async ( + tabId: number, + { status }: chrome.tabs.TabChangeInfo, + { url }: chrome.tabs.Tab, + ) => { + if (status === "loading" && url) { + void inject(url, tabId); + } + }; + const navListener = async ({ + tabId, + frameId, + url, + }: chrome.webNavigation.WebNavigationTransitionCallbackDetails) => { + void inject(url, tabId, frameId); + }; + + if (gotNavigation) { + BrowserApi.addListener(chrome.webNavigation.onCommitted, navListener); + } else { + BrowserApi.addListener(chrome.tabs.onUpdated, tabListener); + } + + const registeredContentScript = { + async unregister() { + if (gotNavigation) { + chrome.webNavigation.onCommitted.removeListener(navListener); + } else { + chrome.tabs.onUpdated.removeListener(tabListener); + } + }, + }; + + if (typeof callback === "function") { + callback(registeredContentScript); + } + + return registeredContentScript; + }; +} diff --git a/apps/browser/src/platform/browser/browser-api.spec.ts b/apps/browser/src/platform/browser/browser-api.spec.ts index a1dafb38ec..e452d6d8ee 100644 --- a/apps/browser/src/platform/browser/browser-api.spec.ts +++ b/apps/browser/src/platform/browser/browser-api.spec.ts @@ -550,4 +550,35 @@ describe("BrowserApi", () => { expect(callbackMock).toHaveBeenCalled(); }); }); + + describe("registerContentScriptsMv2", () => { + const details: browser.contentScripts.RegisteredContentScriptOptions = { + matches: ["<all_urls>"], + js: [{ file: "content/fido2/page-script.js" }], + }; + + it("registers content scripts through the `browser.contentScripts` API when the API is available", async () => { + globalThis.browser = mock<typeof browser>({ + contentScripts: { register: jest.fn() }, + }); + + await BrowserApi.registerContentScriptsMv2(details); + + expect(browser.contentScripts.register).toHaveBeenCalledWith(details); + }); + + it("registers content scripts through the `registerContentScriptsPolyfill` when the `browser.contentScripts.register` API is not available", async () => { + globalThis.browser = mock<typeof browser>({ + contentScripts: { register: undefined }, + }); + jest.spyOn(BrowserApi, "addListener"); + + await BrowserApi.registerContentScriptsMv2(details); + + expect(BrowserApi.addListener).toHaveBeenCalledWith( + chrome.webNavigation.onCommitted, + expect.any(Function), + ); + }); + }); }); diff --git a/apps/browser/src/platform/browser/browser-api.ts b/apps/browser/src/platform/browser/browser-api.ts index b2ee66f051..b793777d8b 100644 --- a/apps/browser/src/platform/browser/browser-api.ts +++ b/apps/browser/src/platform/browser/browser-api.ts @@ -5,6 +5,8 @@ import { DeviceType } from "@bitwarden/common/enums"; import { TabMessage } from "../../types/tab-messages"; import { BrowserPlatformUtilsService } from "../services/platform-utils/browser-platform-utils.service"; +import { registerContentScriptsPolyfill } from "./browser-api.register-content-scripts-polyfill"; + export class BrowserApi { static isWebExtensionsApi: boolean = typeof browser !== "undefined"; static isSafariApi: boolean = @@ -591,4 +593,41 @@ export class BrowserApi { } }); } + + /** + * Handles registration of static content scripts within manifest v2. + * + * @param contentScriptOptions - Details of the registered content scripts + */ + static async registerContentScriptsMv2( + contentScriptOptions: browser.contentScripts.RegisteredContentScriptOptions, + ): Promise<browser.contentScripts.RegisteredContentScript> { + if (typeof browser !== "undefined" && !!browser.contentScripts?.register) { + return await browser.contentScripts.register(contentScriptOptions); + } + + return await registerContentScriptsPolyfill(contentScriptOptions); + } + + /** + * Handles registration of static content scripts within manifest v3. + * + * @param scripts - Details of the registered content scripts + */ + static async registerContentScriptsMv3( + scripts: chrome.scripting.RegisteredContentScript[], + ): Promise<void> { + await chrome.scripting.registerContentScripts(scripts); + } + + /** + * Handles unregistering of static content scripts within manifest v3. + * + * @param filter - Optional filter to unregister content scripts. Passing an empty object will unregister all content scripts. + */ + static async unregisterContentScriptsMv3( + filter?: chrome.scripting.ContentScriptFilter, + ): Promise<void> { + await chrome.scripting.unregisterContentScripts(filter); + } } diff --git a/apps/browser/src/platform/services/abstractions/script-injector.service.ts b/apps/browser/src/platform/services/abstractions/script-injector.service.ts new file mode 100644 index 0000000000..b41e5c7617 --- /dev/null +++ b/apps/browser/src/platform/services/abstractions/script-injector.service.ts @@ -0,0 +1,45 @@ +export type CommonScriptInjectionDetails = { + /** + * Script injected into the document. + * Overridden by `mv2Details` and `mv3Details`. + */ + file?: string; + /** + * Identifies the frame targeted for script injection. Defaults to the top level frame (0). + * Can also be set to "all_frames" to inject into all frames in a tab. + */ + frame?: "all_frames" | number; + /** + * When the script executes. Defaults to "document_start". + * @see https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/content_scripts + */ + runAt?: "document_start" | "document_end" | "document_idle"; +}; + +export type Mv2ScriptInjectionDetails = { + file: string; +}; + +export type Mv3ScriptInjectionDetails = { + file: string; + /** + * The world in which the script should be executed. Defaults to "ISOLATED". + * @see https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting/ExecutionWorld + */ + world?: chrome.scripting.ExecutionWorld; +}; + +/** + * Configuration for injecting a script into a tab. The `file` property should present as a + * path that is relative to the root directory of the extension build, ie "content/script.js". + */ +export type ScriptInjectionConfig = { + tabId: number; + injectDetails: CommonScriptInjectionDetails; + mv2Details?: Mv2ScriptInjectionDetails; + mv3Details?: Mv3ScriptInjectionDetails; +}; + +export abstract class ScriptInjectorService { + abstract inject(config: ScriptInjectionConfig): Promise<void>; +} diff --git a/apps/browser/src/platform/services/browser-script-injector.service.spec.ts b/apps/browser/src/platform/services/browser-script-injector.service.spec.ts new file mode 100644 index 0000000000..6ae84c6464 --- /dev/null +++ b/apps/browser/src/platform/services/browser-script-injector.service.spec.ts @@ -0,0 +1,173 @@ +import { BrowserApi } from "../browser/browser-api"; + +import { + CommonScriptInjectionDetails, + Mv3ScriptInjectionDetails, +} from "./abstractions/script-injector.service"; +import { BrowserScriptInjectorService } from "./browser-script-injector.service"; + +describe("ScriptInjectorService", () => { + const tabId = 1; + const combinedManifestVersionFile = "content/autofill-init.js"; + const mv2SpecificFile = "content/autofill-init-mv2.js"; + const mv2Details = { file: mv2SpecificFile }; + const mv3SpecificFile = "content/autofill-init-mv3.js"; + const mv3Details: Mv3ScriptInjectionDetails = { file: mv3SpecificFile, world: "MAIN" }; + const sharedInjectDetails: CommonScriptInjectionDetails = { + runAt: "document_start", + }; + const manifestVersionSpy = jest.spyOn(BrowserApi, "manifestVersion", "get"); + let scriptInjectorService: BrowserScriptInjectorService; + jest.spyOn(BrowserApi, "executeScriptInTab").mockImplementation(); + jest.spyOn(BrowserApi, "isManifestVersion"); + + beforeEach(() => { + scriptInjectorService = new BrowserScriptInjectorService(); + }); + + describe("inject", () => { + describe("injection of a single script that functions in both manifest v2 and v3", () => { + it("injects the script in manifest v2 when given combined injection details", async () => { + manifestVersionSpy.mockReturnValue(2); + + await scriptInjectorService.inject({ + tabId, + injectDetails: { + file: combinedManifestVersionFile, + frame: "all_frames", + ...sharedInjectDetails, + }, + }); + + expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith(tabId, { + ...sharedInjectDetails, + allFrames: true, + file: combinedManifestVersionFile, + }); + }); + + it("injects the script in manifest v3 when given combined injection details", async () => { + manifestVersionSpy.mockReturnValue(3); + + await scriptInjectorService.inject({ + tabId, + injectDetails: { + file: combinedManifestVersionFile, + frame: 10, + ...sharedInjectDetails, + }, + }); + + expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith( + tabId, + { ...sharedInjectDetails, frameId: 10, file: combinedManifestVersionFile }, + { world: "ISOLATED" }, + ); + }); + }); + + describe("injection of mv2 specific details", () => { + describe("given the extension is running manifest v2", () => { + it("injects the mv2 script injection details file", async () => { + manifestVersionSpy.mockReturnValue(2); + + await scriptInjectorService.inject({ + mv2Details, + tabId, + injectDetails: sharedInjectDetails, + }); + + expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith(tabId, { + ...sharedInjectDetails, + frameId: 0, + file: mv2SpecificFile, + }); + }); + }); + + describe("given the extension is running manifest v3", () => { + it("injects the common script injection details file", async () => { + manifestVersionSpy.mockReturnValue(3); + + await scriptInjectorService.inject({ + mv2Details, + tabId, + injectDetails: { ...sharedInjectDetails, file: combinedManifestVersionFile }, + }); + + expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith( + tabId, + { + ...sharedInjectDetails, + frameId: 0, + file: combinedManifestVersionFile, + }, + { world: "ISOLATED" }, + ); + }); + + it("throws an error if no common script injection details file is specified", async () => { + manifestVersionSpy.mockReturnValue(3); + + await expect( + scriptInjectorService.inject({ + mv2Details, + tabId, + injectDetails: { ...sharedInjectDetails, file: null }, + }), + ).rejects.toThrow("No file specified for script injection"); + }); + }); + }); + + describe("injection of mv3 specific details", () => { + describe("given the extension is running manifest v3", () => { + it("injects the mv3 script injection details file", async () => { + manifestVersionSpy.mockReturnValue(3); + + await scriptInjectorService.inject({ + mv3Details, + tabId, + injectDetails: sharedInjectDetails, + }); + + expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith( + tabId, + { ...sharedInjectDetails, frameId: 0, file: mv3SpecificFile }, + { world: "MAIN" }, + ); + }); + }); + + describe("given the extension is running manifest v2", () => { + it("injects the common script injection details file", async () => { + manifestVersionSpy.mockReturnValue(2); + + await scriptInjectorService.inject({ + mv3Details, + tabId, + injectDetails: { ...sharedInjectDetails, file: combinedManifestVersionFile }, + }); + + expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith(tabId, { + ...sharedInjectDetails, + frameId: 0, + file: combinedManifestVersionFile, + }); + }); + + it("throws an error if no common script injection details file is specified", async () => { + manifestVersionSpy.mockReturnValue(2); + + await expect( + scriptInjectorService.inject({ + mv3Details, + tabId, + injectDetails: { ...sharedInjectDetails, file: "" }, + }), + ).rejects.toThrow("No file specified for script injection"); + }); + }); + }); + }); +}); diff --git a/apps/browser/src/platform/services/browser-script-injector.service.ts b/apps/browser/src/platform/services/browser-script-injector.service.ts new file mode 100644 index 0000000000..54513188d5 --- /dev/null +++ b/apps/browser/src/platform/services/browser-script-injector.service.ts @@ -0,0 +1,78 @@ +import { BrowserApi } from "../browser/browser-api"; + +import { + CommonScriptInjectionDetails, + ScriptInjectionConfig, + ScriptInjectorService, +} from "./abstractions/script-injector.service"; + +export class BrowserScriptInjectorService extends ScriptInjectorService { + /** + * Facilitates the injection of a script into a tab context. Will adjust + * behavior between manifest v2 and v3 based on the passed configuration. + * + * @param config - The configuration for the script injection. + */ + async inject(config: ScriptInjectionConfig): Promise<void> { + const { tabId, injectDetails, mv3Details } = config; + const file = this.getScriptFile(config); + if (!file) { + throw new Error("No file specified for script injection"); + } + + const injectionDetails = this.buildInjectionDetails(injectDetails, file); + + if (BrowserApi.isManifestVersion(3)) { + await BrowserApi.executeScriptInTab(tabId, injectionDetails, { + world: mv3Details?.world ?? "ISOLATED", + }); + + return; + } + + await BrowserApi.executeScriptInTab(tabId, injectionDetails); + } + + /** + * Retrieves the script file to inject based on the configuration. + * + * @param config - The configuration for the script injection. + */ + private getScriptFile(config: ScriptInjectionConfig): string { + const { injectDetails, mv2Details, mv3Details } = config; + + if (BrowserApi.isManifestVersion(3)) { + return mv3Details?.file ?? injectDetails?.file; + } + + return mv2Details?.file ?? injectDetails?.file; + } + + /** + * Builds the injection details for the script injection. + * + * @param injectDetails - The details for the script injection. + * @param file - The file to inject. + */ + private buildInjectionDetails( + injectDetails: CommonScriptInjectionDetails, + file: string, + ): chrome.tabs.InjectDetails { + const { frame, runAt } = injectDetails; + const injectionDetails: chrome.tabs.InjectDetails = { file }; + + if (runAt) { + injectionDetails.runAt = runAt; + } + + if (!frame) { + return { ...injectionDetails, frameId: 0 }; + } + + if (frame !== "all_frames") { + return { ...injectionDetails, frameId: frame }; + } + + return { ...injectionDetails, allFrames: true }; + } +} diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index fe70058640..4906198047 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -94,10 +94,12 @@ import { BrowserApi } from "../../platform/browser/browser-api"; import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; import { BrowserFileDownloadService } from "../../platform/popup/services/browser-file-download.service"; import { BrowserStateService as StateServiceAbstraction } from "../../platform/services/abstractions/browser-state.service"; +import { ScriptInjectorService } from "../../platform/services/abstractions/script-injector.service"; import { BrowserEnvironmentService } from "../../platform/services/browser-environment.service"; import BrowserLocalStorageService from "../../platform/services/browser-local-storage.service"; import BrowserMessagingPrivateModePopupService from "../../platform/services/browser-messaging-private-mode-popup.service"; import BrowserMessagingService from "../../platform/services/browser-messaging.service"; +import { BrowserScriptInjectorService } from "../../platform/services/browser-script-injector.service"; import { DefaultBrowserStateService } from "../../platform/services/default-browser-state.service"; import I18nService from "../../platform/services/i18n.service"; import { ForegroundPlatformUtilsService } from "../../platform/services/platform-utils/foreground-platform-utils.service"; @@ -319,8 +321,14 @@ const safeProviders: SafeProvider[] = [ DomainSettingsService, UserVerificationService, BillingAccountProfileStateService, + ScriptInjectorService, ], }), + safeProvider({ + provide: ScriptInjectorService, + useClass: BrowserScriptInjectorService, + deps: [], + }), safeProvider({ provide: KeyConnectorService, useFactory: getBgService<KeyConnectorService>("keyConnectorService"), diff --git a/apps/browser/src/tools/background/abstractions/fileless-importer.background.ts b/apps/browser/src/tools/background/abstractions/fileless-importer.background.ts index e4b8413718..2ade5bf767 100644 --- a/apps/browser/src/tools/background/abstractions/fileless-importer.background.ts +++ b/apps/browser/src/tools/background/abstractions/fileless-importer.background.ts @@ -1,10 +1,5 @@ import { FilelessImportTypeKeys } from "../../enums/fileless-import.enums"; -type SuppressDownloadScriptInjectionConfig = { - file: string; - scriptingApiDetails?: { world: chrome.scripting.ExecutionWorld }; -}; - type FilelessImportPortMessage = { command?: string; importType?: FilelessImportTypeKeys; @@ -32,7 +27,6 @@ interface FilelessImporterBackground { } export { - SuppressDownloadScriptInjectionConfig, FilelessImportPortMessage, ImportNotificationMessageHandlers, LpImporterMessageHandlers, diff --git a/apps/browser/src/tools/background/fileless-importer.background.spec.ts b/apps/browser/src/tools/background/fileless-importer.background.spec.ts index 858889b887..7d226fcd9d 100644 --- a/apps/browser/src/tools/background/fileless-importer.background.spec.ts +++ b/apps/browser/src/tools/background/fileless-importer.background.spec.ts @@ -16,6 +16,7 @@ import { triggerRuntimeOnConnectEvent, } from "../../autofill/spec/testing-utils"; import { BrowserApi } from "../../platform/browser/browser-api"; +import { BrowserScriptInjectorService } from "../../platform/services/browser-script-injector.service"; import { FilelessImportPort, FilelessImportType } from "../enums/fileless-import.enums"; import FilelessImporterBackground from "./fileless-importer.background"; @@ -37,8 +38,10 @@ describe("FilelessImporterBackground ", () => { const notificationBackground = mock<NotificationBackground>(); const importService = mock<ImportServiceAbstraction>(); const syncService = mock<SyncService>(); + let scriptInjectorService: BrowserScriptInjectorService; beforeEach(() => { + scriptInjectorService = new BrowserScriptInjectorService(); filelessImporterBackground = new FilelessImporterBackground( configService, authService, @@ -46,6 +49,7 @@ describe("FilelessImporterBackground ", () => { notificationBackground, importService, syncService, + scriptInjectorService, ); filelessImporterBackground.init(); }); @@ -138,7 +142,7 @@ describe("FilelessImporterBackground ", () => { expect(executeScriptInTabSpy).toHaveBeenCalledWith( lpImporterPort.sender.tab.id, - { file: "content/lp-suppress-import-download.js", runAt: "document_start" }, + { file: "content/lp-suppress-import-download.js", runAt: "document_start", frameId: 0 }, { world: "MAIN" }, ); }); @@ -149,14 +153,11 @@ describe("FilelessImporterBackground ", () => { triggerRuntimeOnConnectEvent(lpImporterPort); await flushPromises(); - expect(executeScriptInTabSpy).toHaveBeenCalledWith( - lpImporterPort.sender.tab.id, - { - file: "content/lp-suppress-import-download-script-append-mv2.js", - runAt: "document_start", - }, - undefined, - ); + expect(executeScriptInTabSpy).toHaveBeenCalledWith(lpImporterPort.sender.tab.id, { + file: "content/lp-suppress-import-download-script-append-mv2.js", + runAt: "document_start", + frameId: 0, + }); }); }); diff --git a/apps/browser/src/tools/background/fileless-importer.background.ts b/apps/browser/src/tools/background/fileless-importer.background.ts index 57c2faa930..07c6408e8d 100644 --- a/apps/browser/src/tools/background/fileless-importer.background.ts +++ b/apps/browser/src/tools/background/fileless-importer.background.ts @@ -11,6 +11,7 @@ import { ImportServiceAbstraction } from "@bitwarden/importer/core"; import NotificationBackground from "../../autofill/background/notification.background"; import { BrowserApi } from "../../platform/browser/browser-api"; +import { ScriptInjectorService } from "../../platform/services/abstractions/script-injector.service"; import { FilelessImporterInjectedScriptsConfig } from "../config/fileless-importer-injected-scripts"; import { FilelessImportPort, @@ -23,7 +24,6 @@ import { LpImporterMessageHandlers, FilelessImporterBackground as FilelessImporterBackgroundInterface, FilelessImportPortMessage, - SuppressDownloadScriptInjectionConfig, } from "./abstractions/fileless-importer.background"; class FilelessImporterBackground implements FilelessImporterBackgroundInterface { @@ -53,6 +53,7 @@ class FilelessImporterBackground implements FilelessImporterBackgroundInterface * @param notificationBackground - Used to inject the notification bar into the tab. * @param importService - Used to import the export data into the vault. * @param syncService - Used to trigger a full sync after the import is completed. + * @param scriptInjectorService - Used to inject content scripts that initialize the import process */ constructor( private configService: ConfigService, @@ -61,6 +62,7 @@ class FilelessImporterBackground implements FilelessImporterBackgroundInterface private notificationBackground: NotificationBackground, private importService: ImportServiceAbstraction, private syncService: SyncService, + private scriptInjectorService: ScriptInjectorService, ) {} /** @@ -110,23 +112,6 @@ class FilelessImporterBackground implements FilelessImporterBackgroundInterface await this.notificationBackground.requestFilelessImport(tab, importType); } - /** - * Injects the script used to suppress the download of the LP importer export file. - * - * @param sender - The sender of the message. - * @param injectionConfig - The configuration for the injection. - */ - private async injectScriptConfig( - sender: chrome.runtime.MessageSender, - injectionConfig: SuppressDownloadScriptInjectionConfig, - ) { - await BrowserApi.executeScriptInTab( - sender.tab.id, - { file: injectionConfig.file, runAt: "document_start" }, - injectionConfig.scriptingApiDetails, - ); - } - /** * Triggers the download of the CSV file from the LP importer. This is triggered * when the user opts to not save the export to Bitwarden within the notification bar. @@ -219,12 +204,12 @@ class FilelessImporterBackground implements FilelessImporterBackgroundInterface switch (port.name) { case FilelessImportPort.LpImporter: this.lpImporterPort = port; - await this.injectScriptConfig( - port.sender, - BrowserApi.manifestVersion === 3 - ? FilelessImporterInjectedScriptsConfig.LpSuppressImportDownload.mv3 - : FilelessImporterInjectedScriptsConfig.LpSuppressImportDownload.mv2, - ); + await this.scriptInjectorService.inject({ + tabId: port.sender.tab.id, + injectDetails: { runAt: "document_start" }, + mv2Details: FilelessImporterInjectedScriptsConfig.LpSuppressImportDownload.mv2, + mv3Details: FilelessImporterInjectedScriptsConfig.LpSuppressImportDownload.mv3, + }); break; case FilelessImportPort.NotificationBar: this.importNotificationsPort = port; diff --git a/apps/browser/src/tools/config/fileless-importer-injected-scripts.ts b/apps/browser/src/tools/config/fileless-importer-injected-scripts.ts index dbc05fe18c..898ee1205a 100644 --- a/apps/browser/src/tools/config/fileless-importer-injected-scripts.ts +++ b/apps/browser/src/tools/config/fileless-importer-injected-scripts.ts @@ -1,9 +1,12 @@ -import { SuppressDownloadScriptInjectionConfig } from "../background/abstractions/fileless-importer.background"; +import { + Mv2ScriptInjectionDetails, + Mv3ScriptInjectionDetails, +} from "../../platform/services/abstractions/script-injector.service"; type FilelessImporterInjectedScriptsConfigurations = { LpSuppressImportDownload: { - mv2: SuppressDownloadScriptInjectionConfig; - mv3: SuppressDownloadScriptInjectionConfig; + mv2: Mv2ScriptInjectionDetails; + mv3: Mv3ScriptInjectionDetails; }; }; @@ -14,7 +17,7 @@ const FilelessImporterInjectedScriptsConfig: FilelessImporterInjectedScriptsConf }, mv3: { file: "content/lp-suppress-import-download.js", - scriptingApiDetails: { world: "MAIN" }, + world: "MAIN", }, }, } as const; diff --git a/apps/browser/src/vault/fido2/background/abstractions/fido2.background.ts b/apps/browser/src/vault/fido2/background/abstractions/fido2.background.ts new file mode 100644 index 0000000000..49f248c7b8 --- /dev/null +++ b/apps/browser/src/vault/fido2/background/abstractions/fido2.background.ts @@ -0,0 +1,57 @@ +import { + AssertCredentialParams, + AssertCredentialResult, + CreateCredentialParams, + CreateCredentialResult, +} from "@bitwarden/common/vault/abstractions/fido2/fido2-client.service.abstraction"; + +type SharedFido2ScriptInjectionDetails = { + runAt: browser.contentScripts.RegisteredContentScriptOptions["runAt"]; +}; + +type SharedFido2ScriptRegistrationOptions = SharedFido2ScriptInjectionDetails & { + matches: string[]; + excludeMatches: string[]; + allFrames: true; +}; + +type Fido2ExtensionMessage = { + [key: string]: any; + command: string; + hostname?: string; + origin?: string; + requestId?: string; + abortedRequestId?: string; + data?: AssertCredentialParams | CreateCredentialParams; +}; + +type Fido2ExtensionMessageEventParams = { + message: Fido2ExtensionMessage; + sender: chrome.runtime.MessageSender; +}; + +type Fido2BackgroundExtensionMessageHandlers = { + [key: string]: CallableFunction; + fido2AbortRequest: ({ message }: Fido2ExtensionMessageEventParams) => void; + fido2RegisterCredentialRequest: ({ + message, + sender, + }: Fido2ExtensionMessageEventParams) => Promise<CreateCredentialResult>; + fido2GetCredentialRequest: ({ + message, + sender, + }: Fido2ExtensionMessageEventParams) => Promise<AssertCredentialResult>; +}; + +interface Fido2Background { + init(): void; + injectFido2ContentScriptsInAllTabs(): Promise<void>; +} + +export { + SharedFido2ScriptInjectionDetails, + SharedFido2ScriptRegistrationOptions, + Fido2ExtensionMessage, + Fido2BackgroundExtensionMessageHandlers, + Fido2Background, +}; diff --git a/apps/browser/src/vault/fido2/background/fido2.background.spec.ts b/apps/browser/src/vault/fido2/background/fido2.background.spec.ts new file mode 100644 index 0000000000..534d8a99c5 --- /dev/null +++ b/apps/browser/src/vault/fido2/background/fido2.background.spec.ts @@ -0,0 +1,414 @@ +import { mock, MockProxy } from "jest-mock-extended"; +import { BehaviorSubject } from "rxjs"; + +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { + AssertCredentialParams, + CreateCredentialParams, +} from "@bitwarden/common/vault/abstractions/fido2/fido2-client.service.abstraction"; +import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; +import { Fido2ClientService } from "@bitwarden/common/vault/services/fido2/fido2-client.service"; + +import { createPortSpyMock } from "../../../autofill/spec/autofill-mocks"; +import { + flushPromises, + sendExtensionRuntimeMessage, + triggerPortOnDisconnectEvent, + triggerRuntimeOnConnectEvent, +} from "../../../autofill/spec/testing-utils"; +import { BrowserApi } from "../../../platform/browser/browser-api"; +import { BrowserScriptInjectorService } from "../../../platform/services/browser-script-injector.service"; +import { AbortManager } from "../../background/abort-manager"; +import { Fido2ContentScript, Fido2ContentScriptId } from "../enums/fido2-content-script.enum"; +import { Fido2PortName } from "../enums/fido2-port-name.enum"; + +import { Fido2ExtensionMessage } from "./abstractions/fido2.background"; +import { Fido2Background } from "./fido2.background"; + +const sharedExecuteScriptOptions = { runAt: "document_start" }; +const sharedScriptInjectionDetails = { frame: "all_frames", ...sharedExecuteScriptOptions }; +const contentScriptDetails = { + file: Fido2ContentScript.ContentScript, + ...sharedScriptInjectionDetails, +}; +const sharedRegistrationOptions = { + matches: ["https://*/*"], + excludeMatches: ["https://*/*.xml*"], + allFrames: true, + ...sharedExecuteScriptOptions, +}; + +describe("Fido2Background", () => { + const tabsQuerySpy: jest.SpyInstance = jest.spyOn(BrowserApi, "tabsQuery"); + const isManifestVersionSpy: jest.SpyInstance = jest.spyOn(BrowserApi, "isManifestVersion"); + const focusTabSpy: jest.SpyInstance = jest.spyOn(BrowserApi, "focusTab").mockResolvedValue(); + const focusWindowSpy: jest.SpyInstance = jest + .spyOn(BrowserApi, "focusWindow") + .mockResolvedValue(); + let abortManagerMock!: MockProxy<AbortManager>; + let abortController!: MockProxy<AbortController>; + let registeredContentScripsMock!: MockProxy<browser.contentScripts.RegisteredContentScript>; + let tabMock!: MockProxy<chrome.tabs.Tab>; + let senderMock!: MockProxy<chrome.runtime.MessageSender>; + let logService!: MockProxy<LogService>; + let fido2ClientService!: MockProxy<Fido2ClientService>; + let vaultSettingsService!: MockProxy<VaultSettingsService>; + let scriptInjectorServiceMock!: MockProxy<BrowserScriptInjectorService>; + let enablePasskeysMock$!: BehaviorSubject<boolean>; + let fido2Background!: Fido2Background; + + beforeEach(() => { + tabMock = mock<chrome.tabs.Tab>({ + id: 123, + url: "https://example.com", + windowId: 456, + }); + senderMock = mock<chrome.runtime.MessageSender>({ id: "1", tab: tabMock }); + logService = mock<LogService>(); + fido2ClientService = mock<Fido2ClientService>(); + vaultSettingsService = mock<VaultSettingsService>(); + abortManagerMock = mock<AbortManager>(); + abortController = mock<AbortController>(); + registeredContentScripsMock = mock<browser.contentScripts.RegisteredContentScript>(); + scriptInjectorServiceMock = mock<BrowserScriptInjectorService>(); + + enablePasskeysMock$ = new BehaviorSubject(true); + vaultSettingsService.enablePasskeys$ = enablePasskeysMock$; + fido2ClientService.isFido2FeatureEnabled.mockResolvedValue(true); + fido2Background = new Fido2Background( + logService, + fido2ClientService, + vaultSettingsService, + scriptInjectorServiceMock, + ); + fido2Background["abortManager"] = abortManagerMock; + abortManagerMock.runWithAbortController.mockImplementation((_requestId, runner) => + runner(abortController), + ); + isManifestVersionSpy.mockImplementation((manifestVersion) => manifestVersion === 2); + }); + + afterEach(() => { + jest.resetModules(); + jest.clearAllMocks(); + }); + + describe("injectFido2ContentScriptsInAllTabs", () => { + it("does not inject any FIDO2 content scripts when no tabs have a secure url protocol", async () => { + const insecureTab = mock<chrome.tabs.Tab>({ id: 789, url: "http://example.com" }); + tabsQuerySpy.mockResolvedValueOnce([insecureTab]); + + await fido2Background.injectFido2ContentScriptsInAllTabs(); + + expect(scriptInjectorServiceMock.inject).not.toHaveBeenCalled(); + }); + + it("only injects the FIDO2 content script into tabs that contain a secure url protocol", async () => { + const secondTabMock = mock<chrome.tabs.Tab>({ id: 456, url: "https://example.com" }); + const insecureTab = mock<chrome.tabs.Tab>({ id: 789, url: "http://example.com" }); + const noUrlTab = mock<chrome.tabs.Tab>({ id: 101, url: undefined }); + tabsQuerySpy.mockResolvedValueOnce([tabMock, secondTabMock, insecureTab, noUrlTab]); + + await fido2Background.injectFido2ContentScriptsInAllTabs(); + + expect(scriptInjectorServiceMock.inject).toHaveBeenCalledWith({ + tabId: tabMock.id, + injectDetails: contentScriptDetails, + }); + expect(scriptInjectorServiceMock.inject).toHaveBeenCalledWith({ + tabId: secondTabMock.id, + injectDetails: contentScriptDetails, + }); + expect(scriptInjectorServiceMock.inject).not.toHaveBeenCalledWith({ + tabId: insecureTab.id, + injectDetails: contentScriptDetails, + }); + expect(scriptInjectorServiceMock.inject).not.toHaveBeenCalledWith({ + tabId: noUrlTab.id, + injectDetails: contentScriptDetails, + }); + }); + + it("injects the `page-script.js` content script into the provided tab", async () => { + tabsQuerySpy.mockResolvedValueOnce([tabMock]); + + await fido2Background.injectFido2ContentScriptsInAllTabs(); + + expect(scriptInjectorServiceMock.inject).toHaveBeenCalledWith({ + tabId: tabMock.id, + injectDetails: sharedScriptInjectionDetails, + mv2Details: { file: Fido2ContentScript.PageScriptAppend }, + mv3Details: { file: Fido2ContentScript.PageScript, world: "MAIN" }, + }); + }); + }); + + describe("handleEnablePasskeysUpdate", () => { + let portMock!: MockProxy<chrome.runtime.Port>; + + beforeEach(() => { + fido2Background.init(); + jest.spyOn(BrowserApi, "registerContentScriptsMv2"); + jest.spyOn(BrowserApi, "registerContentScriptsMv3"); + jest.spyOn(BrowserApi, "unregisterContentScriptsMv3"); + portMock = createPortSpyMock(Fido2PortName.InjectedScript); + triggerRuntimeOnConnectEvent(portMock); + triggerRuntimeOnConnectEvent(createPortSpyMock("some-other-port")); + + tabsQuerySpy.mockResolvedValue([tabMock]); + }); + + it("does not destroy and re-inject the content scripts when triggering `handleEnablePasskeysUpdate` with an undefined currentEnablePasskeysSetting property", async () => { + await flushPromises(); + + expect(portMock.disconnect).not.toHaveBeenCalled(); + expect(scriptInjectorServiceMock.inject).not.toHaveBeenCalled(); + }); + + it("destroys the content scripts but skips re-injecting them when the enablePasskeys setting is set to `false`", async () => { + enablePasskeysMock$.next(false); + await flushPromises(); + + expect(portMock.disconnect).toHaveBeenCalled(); + expect(scriptInjectorServiceMock.inject).not.toHaveBeenCalled(); + }); + + it("destroys and re-injects the content scripts when the enablePasskeys setting is set to `true`", async () => { + enablePasskeysMock$.next(true); + await flushPromises(); + + expect(portMock.disconnect).toHaveBeenCalled(); + expect(scriptInjectorServiceMock.inject).toHaveBeenCalledWith({ + tabId: tabMock.id, + injectDetails: sharedScriptInjectionDetails, + mv2Details: { file: Fido2ContentScript.PageScriptAppend }, + mv3Details: { file: Fido2ContentScript.PageScript, world: "MAIN" }, + }); + expect(scriptInjectorServiceMock.inject).toHaveBeenCalledWith({ + tabId: tabMock.id, + injectDetails: contentScriptDetails, + }); + }); + + describe("given manifest v2", () => { + it("registers the page-script-append-mv2.js and content-script.js content scripts when the enablePasskeys setting is set to `true`", async () => { + isManifestVersionSpy.mockImplementation((manifestVersion) => manifestVersion === 2); + + enablePasskeysMock$.next(true); + await flushPromises(); + + expect(BrowserApi.registerContentScriptsMv2).toHaveBeenCalledWith({ + js: [ + { file: Fido2ContentScript.PageScriptAppend }, + { file: Fido2ContentScript.ContentScript }, + ], + ...sharedRegistrationOptions, + }); + }); + + it("unregisters any existing registered content scripts when the enablePasskeys setting is set to `false`", async () => { + isManifestVersionSpy.mockImplementation((manifestVersion) => manifestVersion === 2); + fido2Background["registeredContentScripts"] = registeredContentScripsMock; + + enablePasskeysMock$.next(false); + await flushPromises(); + + expect(registeredContentScripsMock.unregister).toHaveBeenCalled(); + expect(BrowserApi.registerContentScriptsMv2).not.toHaveBeenCalledTimes(2); + }); + }); + + describe("given manifest v3", () => { + it("registers the page-script.js and content-script.js content scripts when the enablePasskeys setting is set to `true`", async () => { + isManifestVersionSpy.mockImplementation((manifestVersion) => manifestVersion === 3); + + enablePasskeysMock$.next(true); + await flushPromises(); + + expect(BrowserApi.registerContentScriptsMv3).toHaveBeenCalledWith([ + { + id: Fido2ContentScriptId.PageScript, + js: [Fido2ContentScript.PageScript], + world: "MAIN", + ...sharedRegistrationOptions, + }, + { + id: Fido2ContentScriptId.ContentScript, + js: [Fido2ContentScript.ContentScript], + ...sharedRegistrationOptions, + }, + ]); + expect(BrowserApi.unregisterContentScriptsMv3).not.toHaveBeenCalled(); + }); + + it("unregisters the page-script.js and content-script.js content scripts when the enablePasskeys setting is set to `false`", async () => { + isManifestVersionSpy.mockImplementation((manifestVersion) => manifestVersion === 3); + + enablePasskeysMock$.next(false); + await flushPromises(); + + expect(BrowserApi.unregisterContentScriptsMv3).toHaveBeenCalledWith({ + ids: [Fido2ContentScriptId.PageScript, Fido2ContentScriptId.ContentScript], + }); + expect(BrowserApi.registerContentScriptsMv3).not.toHaveBeenCalledTimes(2); + }); + }); + }); + + describe("extension message handlers", () => { + beforeEach(() => { + fido2Background.init(); + }); + + it("ignores messages that do not have a handler associated with a command within the message", () => { + const message = mock<Fido2ExtensionMessage>({ command: "nonexistentCommand" }); + + sendExtensionRuntimeMessage(message); + + expect(abortManagerMock.abort).not.toHaveBeenCalled(); + }); + + it("sends a response for rejected promises returned by a handler", async () => { + const message = mock<Fido2ExtensionMessage>({ command: "fido2RegisterCredentialRequest" }); + const sender = mock<chrome.runtime.MessageSender>(); + const sendResponse = jest.fn(); + fido2ClientService.createCredential.mockRejectedValue(new Error("error")); + + sendExtensionRuntimeMessage(message, sender, sendResponse); + await flushPromises(); + + expect(sendResponse).toHaveBeenCalledWith({ error: { message: "error" } }); + }); + + describe("fido2AbortRequest message", () => { + it("aborts the request associated with the passed abortedRequestId", async () => { + const message = mock<Fido2ExtensionMessage>({ + command: "fido2AbortRequest", + abortedRequestId: "123", + }); + + sendExtensionRuntimeMessage(message); + await flushPromises(); + + expect(abortManagerMock.abort).toHaveBeenCalledWith(message.abortedRequestId); + }); + }); + + describe("fido2RegisterCredentialRequest message", () => { + it("creates a credential within the Fido2ClientService", async () => { + const message = mock<Fido2ExtensionMessage>({ + command: "fido2RegisterCredentialRequest", + requestId: "123", + data: mock<CreateCredentialParams>(), + }); + + sendExtensionRuntimeMessage(message, senderMock); + await flushPromises(); + + expect(fido2ClientService.createCredential).toHaveBeenCalledWith( + message.data, + tabMock, + abortController, + ); + expect(focusTabSpy).toHaveBeenCalledWith(tabMock.id); + expect(focusWindowSpy).toHaveBeenCalledWith(tabMock.windowId); + }); + }); + + describe("fido2GetCredentialRequest", () => { + it("asserts a credential within the Fido2ClientService", async () => { + const message = mock<Fido2ExtensionMessage>({ + command: "fido2GetCredentialRequest", + requestId: "123", + data: mock<AssertCredentialParams>(), + }); + + sendExtensionRuntimeMessage(message, senderMock); + await flushPromises(); + + expect(fido2ClientService.assertCredential).toHaveBeenCalledWith( + message.data, + tabMock, + abortController, + ); + expect(focusTabSpy).toHaveBeenCalledWith(tabMock.id); + expect(focusWindowSpy).toHaveBeenCalledWith(tabMock.windowId); + }); + }); + }); + + describe("handle ports onConnect", () => { + let portMock!: MockProxy<chrome.runtime.Port>; + + beforeEach(() => { + fido2Background.init(); + portMock = createPortSpyMock(Fido2PortName.InjectedScript); + fido2ClientService.isFido2FeatureEnabled.mockResolvedValue(true); + }); + + it("ignores port connections that do not have the correct port name", async () => { + const port = createPortSpyMock("nonexistentPort"); + + triggerRuntimeOnConnectEvent(port); + await flushPromises(); + + expect(port.onDisconnect.addListener).not.toHaveBeenCalled(); + }); + + it("ignores port connections that do not have a sender url", async () => { + portMock.sender = undefined; + + triggerRuntimeOnConnectEvent(portMock); + await flushPromises(); + + expect(portMock.onDisconnect.addListener).not.toHaveBeenCalled(); + }); + + it("disconnects the port connection when the Fido2 feature is not enabled", async () => { + fido2ClientService.isFido2FeatureEnabled.mockResolvedValue(false); + + triggerRuntimeOnConnectEvent(portMock); + await flushPromises(); + + expect(portMock.disconnect).toHaveBeenCalled(); + }); + + it("disconnects the port connection when the url is malformed", async () => { + portMock.sender.url = "malformed-url"; + + triggerRuntimeOnConnectEvent(portMock); + await flushPromises(); + + expect(portMock.disconnect).toHaveBeenCalled(); + expect(logService.error).toHaveBeenCalled(); + }); + + it("adds the port to the fido2ContentScriptPortsSet when the Fido2 feature is enabled", async () => { + triggerRuntimeOnConnectEvent(portMock); + await flushPromises(); + + expect(portMock.onDisconnect.addListener).toHaveBeenCalled(); + }); + }); + + describe("handleInjectScriptPortOnDisconnect", () => { + let portMock!: MockProxy<chrome.runtime.Port>; + + beforeEach(() => { + fido2Background.init(); + portMock = createPortSpyMock(Fido2PortName.InjectedScript); + triggerRuntimeOnConnectEvent(portMock); + fido2Background["fido2ContentScriptPortsSet"].add(portMock); + }); + + it("does not destroy or inject the content script when the port has already disconnected before the enablePasskeys setting is set to `false`", async () => { + triggerPortOnDisconnectEvent(portMock); + + enablePasskeysMock$.next(false); + await flushPromises(); + + expect(portMock.disconnect).not.toHaveBeenCalled(); + expect(scriptInjectorServiceMock.inject).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/apps/browser/src/vault/fido2/background/fido2.background.ts b/apps/browser/src/vault/fido2/background/fido2.background.ts new file mode 100644 index 0000000000..856874cee3 --- /dev/null +++ b/apps/browser/src/vault/fido2/background/fido2.background.ts @@ -0,0 +1,356 @@ +import { firstValueFrom, startWith } from "rxjs"; +import { pairwise } from "rxjs/operators"; + +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { + AssertCredentialParams, + AssertCredentialResult, + CreateCredentialParams, + CreateCredentialResult, + Fido2ClientService, +} from "@bitwarden/common/vault/abstractions/fido2/fido2-client.service.abstraction"; +import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; + +import { BrowserApi } from "../../../platform/browser/browser-api"; +import { ScriptInjectorService } from "../../../platform/services/abstractions/script-injector.service"; +import { AbortManager } from "../../background/abort-manager"; +import { Fido2ContentScript, Fido2ContentScriptId } from "../enums/fido2-content-script.enum"; +import { Fido2PortName } from "../enums/fido2-port-name.enum"; + +import { + Fido2Background as Fido2BackgroundInterface, + Fido2BackgroundExtensionMessageHandlers, + Fido2ExtensionMessage, + SharedFido2ScriptInjectionDetails, + SharedFido2ScriptRegistrationOptions, +} from "./abstractions/fido2.background"; + +export class Fido2Background implements Fido2BackgroundInterface { + private abortManager = new AbortManager(); + private fido2ContentScriptPortsSet = new Set<chrome.runtime.Port>(); + private registeredContentScripts: browser.contentScripts.RegisteredContentScript; + private readonly sharedInjectionDetails: SharedFido2ScriptInjectionDetails = { + runAt: "document_start", + }; + private readonly sharedRegistrationOptions: SharedFido2ScriptRegistrationOptions = { + matches: ["https://*/*"], + excludeMatches: ["https://*/*.xml*"], + allFrames: true, + ...this.sharedInjectionDetails, + }; + private readonly extensionMessageHandlers: Fido2BackgroundExtensionMessageHandlers = { + fido2AbortRequest: ({ message }) => this.abortRequest(message), + fido2RegisterCredentialRequest: ({ message, sender }) => + this.registerCredentialRequest(message, sender), + fido2GetCredentialRequest: ({ message, sender }) => this.getCredentialRequest(message, sender), + }; + + constructor( + private logService: LogService, + private fido2ClientService: Fido2ClientService, + private vaultSettingsService: VaultSettingsService, + private scriptInjectorService: ScriptInjectorService, + ) {} + + /** + * Initializes the FIDO2 background service. Sets up the extension message + * and port listeners. Subscribes to the enablePasskeys$ observable to + * handle passkey enable/disable events. + */ + init() { + BrowserApi.messageListener("fido2.background", this.handleExtensionMessage); + BrowserApi.addListener(chrome.runtime.onConnect, this.handleInjectedScriptPortConnection); + this.vaultSettingsService.enablePasskeys$ + .pipe(startWith(undefined), pairwise()) + .subscribe(([previous, current]) => this.handleEnablePasskeysUpdate(previous, current)); + } + + /** + * Injects the FIDO2 content and page script into all existing browser tabs. + */ + async injectFido2ContentScriptsInAllTabs() { + const tabs = await BrowserApi.tabsQuery({}); + for (let index = 0; index < tabs.length; index++) { + const tab = tabs[index]; + if (!tab.url?.startsWith("https")) { + continue; + } + + void this.injectFido2ContentScripts(tab); + } + } + + /** + * Handles reacting to the enablePasskeys setting being updated. If the setting + * is enabled, the FIDO2 content scripts are injected into all tabs. If the setting + * is disabled, the FIDO2 content scripts will be from all tabs. This logic will + * not trigger until after the first setting update. + * + * @param previousEnablePasskeysSetting - The previous value of the enablePasskeys setting. + * @param enablePasskeys - The new value of the enablePasskeys setting. + */ + private async handleEnablePasskeysUpdate( + previousEnablePasskeysSetting: boolean, + enablePasskeys: boolean, + ) { + await this.updateContentScriptRegistration(); + + if (previousEnablePasskeysSetting === undefined) { + return; + } + + this.destroyLoadedFido2ContentScripts(); + if (enablePasskeys) { + void this.injectFido2ContentScriptsInAllTabs(); + } + } + + /** + * Updates the registration status of static FIDO2 content + * scripts based on the enablePasskeys setting. + */ + private async updateContentScriptRegistration() { + if (BrowserApi.isManifestVersion(2)) { + await this.updateMv2ContentScriptsRegistration(); + + return; + } + + await this.updateMv3ContentScriptsRegistration(); + } + + /** + * Updates the registration status of static FIDO2 content + * scripts based on the enablePasskeys setting for manifest v2. + */ + private async updateMv2ContentScriptsRegistration() { + if (!(await this.isPasskeySettingEnabled())) { + await this.registeredContentScripts?.unregister(); + + return; + } + + this.registeredContentScripts = await BrowserApi.registerContentScriptsMv2({ + js: [ + { file: Fido2ContentScript.PageScriptAppend }, + { file: Fido2ContentScript.ContentScript }, + ], + ...this.sharedRegistrationOptions, + }); + } + + /** + * Updates the registration status of static FIDO2 content + * scripts based on the enablePasskeys setting for manifest v3. + */ + private async updateMv3ContentScriptsRegistration() { + if (await this.isPasskeySettingEnabled()) { + void BrowserApi.registerContentScriptsMv3([ + { + id: Fido2ContentScriptId.PageScript, + js: [Fido2ContentScript.PageScript], + world: "MAIN", + ...this.sharedRegistrationOptions, + }, + { + id: Fido2ContentScriptId.ContentScript, + js: [Fido2ContentScript.ContentScript], + ...this.sharedRegistrationOptions, + }, + ]); + + return; + } + + void BrowserApi.unregisterContentScriptsMv3({ + ids: [Fido2ContentScriptId.PageScript, Fido2ContentScriptId.ContentScript], + }); + } + + /** + * Injects the FIDO2 content and page script into the current tab. + * + * @param tab - The current tab to inject the scripts into. + */ + private async injectFido2ContentScripts(tab: chrome.tabs.Tab): Promise<void> { + void this.scriptInjectorService.inject({ + tabId: tab.id, + injectDetails: { frame: "all_frames", ...this.sharedInjectionDetails }, + mv2Details: { file: Fido2ContentScript.PageScriptAppend }, + mv3Details: { file: Fido2ContentScript.PageScript, world: "MAIN" }, + }); + + void this.scriptInjectorService.inject({ + tabId: tab.id, + injectDetails: { + file: Fido2ContentScript.ContentScript, + frame: "all_frames", + ...this.sharedInjectionDetails, + }, + }); + } + + /** + * Iterates over the set of injected FIDO2 content script ports + * and disconnects them, destroying the content scripts. + */ + private destroyLoadedFido2ContentScripts() { + this.fido2ContentScriptPortsSet.forEach((port) => { + port.disconnect(); + this.fido2ContentScriptPortsSet.delete(port); + }); + } + + /** + * Aborts the FIDO2 request with the provided requestId. + * + * @param message - The FIDO2 extension message containing the requestId to abort. + */ + private abortRequest(message: Fido2ExtensionMessage) { + this.abortManager.abort(message.abortedRequestId); + } + + /** + * Registers a new FIDO2 credential with the provided request data. + * + * @param message - The FIDO2 extension message containing the request data. + * @param sender - The sender of the message. + */ + private async registerCredentialRequest( + message: Fido2ExtensionMessage, + sender: chrome.runtime.MessageSender, + ): Promise<CreateCredentialResult> { + return await this.handleCredentialRequest<CreateCredentialResult>( + message, + sender.tab, + this.fido2ClientService.createCredential.bind(this.fido2ClientService), + ); + } + + /** + * Gets a FIDO2 credential with the provided request data. + * + * @param message - The FIDO2 extension message containing the request data. + * @param sender - The sender of the message. + */ + private async getCredentialRequest( + message: Fido2ExtensionMessage, + sender: chrome.runtime.MessageSender, + ): Promise<AssertCredentialResult> { + return await this.handleCredentialRequest<AssertCredentialResult>( + message, + sender.tab, + this.fido2ClientService.assertCredential.bind(this.fido2ClientService), + ); + } + + /** + * Handles Fido2 credential requests by calling the provided callback with the + * request data, tab, and abort controller. The callback is expected to return + * a promise that resolves with the result of the credential request. + * + * @param requestId - The request ID associated with the request. + * @param data - The request data to handle. + * @param tab - The tab associated with the request. + * @param callback - The callback to call with the request data, tab, and abort controller. + */ + private handleCredentialRequest = async <T>( + { requestId, data }: Fido2ExtensionMessage, + tab: chrome.tabs.Tab, + callback: ( + data: AssertCredentialParams | CreateCredentialParams, + tab: chrome.tabs.Tab, + abortController: AbortController, + ) => Promise<T>, + ) => { + return await this.abortManager.runWithAbortController(requestId, async (abortController) => { + try { + return await callback(data, tab, abortController); + } finally { + await BrowserApi.focusTab(tab.id); + await BrowserApi.focusWindow(tab.windowId); + } + }); + }; + + /** + * Checks if the enablePasskeys setting is enabled. + */ + private async isPasskeySettingEnabled() { + return await firstValueFrom(this.vaultSettingsService.enablePasskeys$); + } + + /** + * Handles the FIDO2 extension message by calling the + * appropriate handler based on the message command. + * + * @param message - The FIDO2 extension message to handle. + * @param sender - The sender of the message. + * @param sendResponse - The function to call with the response. + */ + private handleExtensionMessage = ( + message: Fido2ExtensionMessage, + sender: chrome.runtime.MessageSender, + sendResponse: (response?: any) => void, + ) => { + const handler: CallableFunction | undefined = this.extensionMessageHandlers[message?.command]; + if (!handler) { + return; + } + + const messageResponse = handler({ message, sender }); + if (!messageResponse) { + return; + } + + Promise.resolve(messageResponse) + .then( + (response) => sendResponse(response), + (error) => sendResponse({ error: { ...error, message: error.message } }), + ) + .catch(this.logService.error); + + return true; + }; + + /** + * Handles the connection of a FIDO2 content script port by checking if the + * FIDO2 feature is enabled for the sender's hostname and origin. If the feature + * is not enabled, the port is disconnected. + * + * @param port - The port which is connecting + */ + private handleInjectedScriptPortConnection = async (port: chrome.runtime.Port) => { + if (port.name !== Fido2PortName.InjectedScript || !port.sender?.url) { + return; + } + + try { + const { hostname, origin } = new URL(port.sender.url); + if (!(await this.fido2ClientService.isFido2FeatureEnabled(hostname, origin))) { + port.disconnect(); + return; + } + + this.fido2ContentScriptPortsSet.add(port); + port.onDisconnect.addListener(this.handleInjectScriptPortOnDisconnect); + } catch (error) { + this.logService.error(error); + port.disconnect(); + } + }; + + /** + * Handles the disconnection of a FIDO2 content script port + * by removing it from the set of connected ports. + * + * @param port - The port which is disconnecting + */ + private handleInjectScriptPortOnDisconnect = (port: chrome.runtime.Port) => { + if (port.name !== Fido2PortName.InjectedScript) { + return; + } + + this.fido2ContentScriptPortsSet.delete(port); + }; +} diff --git a/apps/browser/src/vault/fido2/content/content-script.spec.ts b/apps/browser/src/vault/fido2/content/content-script.spec.ts new file mode 100644 index 0000000000..29d3e9c257 --- /dev/null +++ b/apps/browser/src/vault/fido2/content/content-script.spec.ts @@ -0,0 +1,164 @@ +import { mock, MockProxy } from "jest-mock-extended"; + +import { CreateCredentialResult } from "@bitwarden/common/vault/abstractions/fido2/fido2-client.service.abstraction"; + +import { createPortSpyMock } from "../../../autofill/spec/autofill-mocks"; +import { triggerPortOnDisconnectEvent } from "../../../autofill/spec/testing-utils"; +import { Fido2PortName } from "../enums/fido2-port-name.enum"; + +import { InsecureCreateCredentialParams, MessageType } from "./messaging/message"; +import { MessageWithMetadata, Messenger } from "./messaging/messenger"; + +jest.mock("../../../autofill/utils", () => ({ + sendExtensionMessage: jest.fn((command, options) => { + return chrome.runtime.sendMessage(Object.assign({ command }, options)); + }), +})); + +describe("Fido2 Content Script", () => { + let messenger: Messenger; + const messengerForDOMCommunicationSpy = jest + .spyOn(Messenger, "forDOMCommunication") + .mockImplementation((window) => { + const windowOrigin = window.location.origin; + + messenger = new Messenger({ + postMessage: (message, port) => window.postMessage(message, windowOrigin, [port]), + addEventListener: (listener) => window.addEventListener("message", listener), + removeEventListener: (listener) => window.removeEventListener("message", listener), + }); + messenger.destroy = jest.fn(); + return messenger; + }); + const portSpy: MockProxy<chrome.runtime.Port> = createPortSpyMock(Fido2PortName.InjectedScript); + chrome.runtime.connect = jest.fn(() => portSpy); + + afterEach(() => { + Object.defineProperty(document, "contentType", { + value: "text/html", + writable: true, + }); + + jest.clearAllMocks(); + jest.resetModules(); + }); + + it("destroys the messenger when the port is disconnected", () => { + require("./content-script"); + + triggerPortOnDisconnectEvent(portSpy); + + expect(messenger.destroy).toHaveBeenCalled(); + }); + + it("handles a FIDO2 credential creation request message from the window message listener, formats the message and sends the formatted message to the extension background", async () => { + const message = mock<MessageWithMetadata>({ + type: MessageType.CredentialCreationRequest, + data: mock<InsecureCreateCredentialParams>(), + }); + const mockResult = { credentialId: "mock" } as CreateCredentialResult; + jest.spyOn(chrome.runtime, "sendMessage").mockResolvedValue(mockResult); + + require("./content-script"); + + const response = await messenger.handler!(message, new AbortController()); + + expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({ + command: "fido2RegisterCredentialRequest", + data: expect.objectContaining({ + origin: globalThis.location.origin, + sameOriginWithAncestors: true, + }), + requestId: expect.any(String), + }); + expect(response).toEqual({ + type: MessageType.CredentialCreationResponse, + result: mockResult, + }); + }); + + it("handles a FIDO2 credential get request message from the window message listener, formats the message and sends the formatted message to the extension background", async () => { + const message = mock<MessageWithMetadata>({ + type: MessageType.CredentialGetRequest, + data: mock<InsecureCreateCredentialParams>(), + }); + + require("./content-script"); + + await messenger.handler!(message, new AbortController()); + + expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({ + command: "fido2GetCredentialRequest", + data: expect.objectContaining({ + origin: globalThis.location.origin, + sameOriginWithAncestors: true, + }), + requestId: expect.any(String), + }); + }); + + it("removes the abort handler when the FIDO2 request is complete", async () => { + const message = mock<MessageWithMetadata>({ + type: MessageType.CredentialCreationRequest, + data: mock<InsecureCreateCredentialParams>(), + }); + const abortController = new AbortController(); + const abortSpy = jest.spyOn(abortController.signal, "removeEventListener"); + + require("./content-script"); + + await messenger.handler!(message, abortController); + + expect(abortSpy).toHaveBeenCalled(); + }); + + it("sends an extension message to abort the FIDO2 request when the abort controller is signaled", async () => { + const message = mock<MessageWithMetadata>({ + type: MessageType.CredentialCreationRequest, + data: mock<InsecureCreateCredentialParams>(), + }); + const abortController = new AbortController(); + const abortSpy = jest.spyOn(abortController.signal, "addEventListener"); + jest + .spyOn(chrome.runtime, "sendMessage") + .mockImplementationOnce(async (extensionId: string, message: unknown, options: any) => { + abortController.abort(); + }); + + require("./content-script"); + + await messenger.handler!(message, abortController); + + expect(abortSpy).toHaveBeenCalled(); + expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({ + command: "fido2AbortRequest", + abortedRequestId: expect.any(String), + }); + }); + + it("rejects credential requests and returns an error result", async () => { + const errorMessage = "Test error"; + const message = mock<MessageWithMetadata>({ + type: MessageType.CredentialCreationRequest, + data: mock<InsecureCreateCredentialParams>(), + }); + const abortController = new AbortController(); + jest.spyOn(chrome.runtime, "sendMessage").mockResolvedValue({ error: errorMessage }); + + require("./content-script"); + const result = messenger.handler!(message, abortController); + + await expect(result).rejects.toEqual(errorMessage); + }); + + it("skips initializing the content script if the document content type is not 'text/html'", () => { + Object.defineProperty(document, "contentType", { + value: "application/json", + writable: true, + }); + + require("./content-script"); + + expect(messengerForDOMCommunicationSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/browser/src/vault/fido2/content/content-script.ts b/apps/browser/src/vault/fido2/content/content-script.ts index c2fc862f55..809db11553 100644 --- a/apps/browser/src/vault/fido2/content/content-script.ts +++ b/apps/browser/src/vault/fido2/content/content-script.ts @@ -3,142 +3,134 @@ import { CreateCredentialParams, } from "@bitwarden/common/vault/abstractions/fido2/fido2-client.service.abstraction"; -import { Message, MessageType } from "./messaging/message"; -import { Messenger } from "./messaging/messenger"; +import { sendExtensionMessage } from "../../../autofill/utils"; +import { Fido2PortName } from "../enums/fido2-port-name.enum"; -function isFido2FeatureEnabled(): Promise<boolean> { - return new Promise((resolve) => { - chrome.runtime.sendMessage( - { - command: "checkFido2FeatureEnabled", - hostname: window.location.hostname, - origin: window.location.origin, - }, - (response: { result?: boolean }) => resolve(response.result), - ); - }); -} - -function isSameOriginWithAncestors() { - try { - return window.self === window.top; - } catch { - return false; - } -} -const messenger = Messenger.forDOMCommunication(window); - -function injectPageScript() { - // Locate an existing page-script on the page - const existingPageScript = document.getElementById("bw-fido2-page-script"); - - // Inject the page-script if it doesn't exist - if (!existingPageScript) { - const s = document.createElement("script"); - s.src = chrome.runtime.getURL("content/fido2/page-script.js"); - s.id = "bw-fido2-page-script"; - (document.head || document.documentElement).appendChild(s); +import { + InsecureAssertCredentialParams, + InsecureCreateCredentialParams, + Message, + MessageType, +} from "./messaging/message"; +import { MessageWithMetadata, Messenger } from "./messaging/messenger"; +(function (globalContext) { + if (globalContext.document.contentType !== "text/html") { return; } - // If the page-script already exists, send a reconnect message to the page-script - // 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 - messenger.sendReconnectCommand(); -} + // Initialization logic, set up the messenger and connect a port to the background script. + const messenger = Messenger.forDOMCommunication(globalContext.window); + messenger.handler = handleFido2Message; + const port = chrome.runtime.connect({ name: Fido2PortName.InjectedScript }); + port.onDisconnect.addListener(handlePortOnDisconnect); -function initializeFido2ContentScript() { - injectPageScript(); - - messenger.handler = async (message, abortController) => { + /** + * Handles FIDO2 credential requests and returns the result. + * + * @param message - The message to handle. + * @param abortController - The abort controller used to handle exit conditions from the FIDO2 request. + */ + async function handleFido2Message( + message: MessageWithMetadata, + abortController: AbortController, + ) { const requestId = Date.now().toString(); const abortHandler = () => - chrome.runtime.sendMessage({ - command: "fido2AbortRequest", - abortedRequestId: requestId, - }); + sendExtensionMessage("fido2AbortRequest", { abortedRequestId: requestId }); abortController.signal.addEventListener("abort", abortHandler); - if (message.type === MessageType.CredentialCreationRequest) { - return new Promise<Message | undefined>((resolve, reject) => { - const data: CreateCredentialParams = { - ...message.data, - origin: window.location.origin, - sameOriginWithAncestors: isSameOriginWithAncestors(), - }; - - chrome.runtime.sendMessage( - { - command: "fido2RegisterCredentialRequest", - data, - requestId: requestId, - }, - (response) => { - if (response && response.error !== undefined) { - return reject(response.error); - } - - resolve({ - type: MessageType.CredentialCreationResponse, - result: response.result, - }); - }, + try { + if (message.type === MessageType.CredentialCreationRequest) { + return handleCredentialCreationRequestMessage( + requestId, + message.data as InsecureCreateCredentialParams, ); - }); - } + } - if (message.type === MessageType.CredentialGetRequest) { - return new Promise<Message | undefined>((resolve, reject) => { - const data: AssertCredentialParams = { - ...message.data, - origin: window.location.origin, - sameOriginWithAncestors: isSameOriginWithAncestors(), - }; - - chrome.runtime.sendMessage( - { - command: "fido2GetCredentialRequest", - data, - requestId: requestId, - }, - (response) => { - if (response && response.error !== undefined) { - return reject(response.error); - } - - resolve({ - type: MessageType.CredentialGetResponse, - result: response.result, - }); - }, + if (message.type === MessageType.CredentialGetRequest) { + return handleCredentialGetRequestMessage( + requestId, + message.data as InsecureAssertCredentialParams, ); - }).finally(() => - abortController.signal.removeEventListener("abort", abortHandler), - ) as Promise<Message>; + } + } finally { + abortController.signal.removeEventListener("abort", abortHandler); } - - return undefined; - }; -} - -async function run() { - if (!(await isFido2FeatureEnabled())) { - return; } - initializeFido2ContentScript(); + /** + * Handles the credential creation request message and returns the result. + * + * @param requestId - The request ID of the message. + * @param data - Data associated with the credential request. + */ + async function handleCredentialCreationRequestMessage( + requestId: string, + data: InsecureCreateCredentialParams, + ): Promise<Message | undefined> { + return respondToCredentialRequest( + "fido2RegisterCredentialRequest", + MessageType.CredentialCreationResponse, + requestId, + data, + ); + } - const port = chrome.runtime.connect({ name: "fido2ContentScriptReady" }); - port.onDisconnect.addListener(() => { - // Cleanup the messenger and remove the event listener - // 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 - messenger.destroy(); - }); -} + /** + * Handles the credential get request message and returns the result. + * + * @param requestId - The request ID of the message. + * @param data - Data associated with the credential request. + */ + async function handleCredentialGetRequestMessage( + requestId: string, + data: InsecureAssertCredentialParams, + ): Promise<Message | undefined> { + return respondToCredentialRequest( + "fido2GetCredentialRequest", + MessageType.CredentialGetResponse, + requestId, + data, + ); + } -// Only run the script if the document is an HTML document -if (document.contentType === "text/html") { - void run(); -} + /** + * Sends a message to the extension to handle the + * credential request and returns the result. + * + * @param command - The command to send to the extension. + * @param type - The type of message, either CredentialCreationResponse or CredentialGetResponse. + * @param requestId - The request ID of the message. + * @param messageData - Data associated with the credential request. + */ + async function respondToCredentialRequest( + command: string, + type: MessageType.CredentialCreationResponse | MessageType.CredentialGetResponse, + requestId: string, + messageData: InsecureCreateCredentialParams | InsecureAssertCredentialParams, + ): Promise<Message | undefined> { + const data: CreateCredentialParams | AssertCredentialParams = { + ...messageData, + origin: globalContext.location.origin, + sameOriginWithAncestors: globalContext.self === globalContext.top, + }; + + const result = await sendExtensionMessage(command, { data, requestId }); + + if (result && result.error !== undefined) { + return Promise.reject(result.error); + } + + return Promise.resolve({ type, result }); + } + + /** + * Handles the disconnect event of the port. Calls + * to the messenger to destroy and tear down the + * implemented page-script.js logic. + */ + function handlePortOnDisconnect() { + void messenger.destroy(); + } +})(globalThis); diff --git a/apps/browser/src/vault/fido2/content/messaging/messenger.spec.ts b/apps/browser/src/vault/fido2/content/messaging/messenger.spec.ts index 0c46ac39aa..5283c60882 100644 --- a/apps/browser/src/vault/fido2/content/messaging/messenger.spec.ts +++ b/apps/browser/src/vault/fido2/content/messaging/messenger.spec.ts @@ -68,7 +68,7 @@ describe("Messenger", () => { const abortController = new AbortController(); // 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 - messengerA.request(createRequest(), abortController); + messengerA.request(createRequest(), abortController.signal); abortController.abort(); const received = handlerB.receive(); diff --git a/apps/browser/src/vault/fido2/content/messaging/messenger.ts b/apps/browser/src/vault/fido2/content/messaging/messenger.ts index cc29282227..f05c138eab 100644 --- a/apps/browser/src/vault/fido2/content/messaging/messenger.ts +++ b/apps/browser/src/vault/fido2/content/messaging/messenger.ts @@ -47,7 +47,7 @@ export class Messenger { } /** - * The handler that will be called when a message is recieved. The handler should return + * The handler that will be called when a message is received. The handler should return * a promise that resolves to the response message. If the handler throws an error, the * error will be sent back to the sender. */ @@ -65,10 +65,10 @@ export class Messenger { * AbortController signals will be forwarded to the content script. * * @param request data to send to the content script - * @param abortController the abort controller that might be used to abort the request + * @param abortSignal the abort controller that might be used to abort the request * @returns the response from the content script */ - async request(request: Message, abortController?: AbortController): Promise<Message> { + async request(request: Message, abortSignal?: AbortSignal): Promise<Message> { const requestChannel = new MessageChannel(); const { port1: localPort, port2: remotePort } = requestChannel; @@ -82,7 +82,7 @@ export class Messenger { metadata: { SENDER }, type: MessageType.AbortRequest, }); - abortController?.signal.addEventListener("abort", abortListener); + abortSignal?.addEventListener("abort", abortListener); this.broadcastChannel.postMessage( { ...request, SENDER, senderId: this.messengerId }, @@ -90,7 +90,7 @@ export class Messenger { ); const response = await promise; - abortController?.signal.removeEventListener("abort", abortListener); + abortSignal?.removeEventListener("abort", abortListener); if (response.type === MessageType.ErrorResponse) { const error = new Error(); @@ -113,12 +113,7 @@ export class Messenger { const message = event.data; const port = event.ports?.[0]; - if ( - message?.SENDER !== SENDER || - message.senderId == this.messengerId || - message == null || - port == null - ) { + if (message?.SENDER !== SENDER || message.senderId == this.messengerId || port == null) { return; } @@ -167,10 +162,6 @@ export class Messenger { } } - async sendReconnectCommand() { - await this.request({ type: MessageType.ReconnectRequest }); - } - private async sendDisconnectCommand() { await this.request({ type: MessageType.DisconnectRequest }); } diff --git a/apps/browser/src/vault/fido2/content/page-script-append.mv2.spec.ts b/apps/browser/src/vault/fido2/content/page-script-append.mv2.spec.ts new file mode 100644 index 0000000000..d40a725a1f --- /dev/null +++ b/apps/browser/src/vault/fido2/content/page-script-append.mv2.spec.ts @@ -0,0 +1,69 @@ +import { Fido2ContentScript } from "../enums/fido2-content-script.enum"; + +describe("FIDO2 page-script for manifest v2", () => { + let createdScriptElement: HTMLScriptElement; + jest.spyOn(window.document, "createElement"); + + afterEach(() => { + Object.defineProperty(window.document, "contentType", { value: "text/html", writable: true }); + jest.clearAllMocks(); + jest.resetModules(); + }); + + it("skips appending the `page-script.js` file if the document contentType is not `text/html`", () => { + Object.defineProperty(window.document, "contentType", { value: "text/plain", writable: true }); + + require("./page-script-append.mv2"); + + expect(window.document.createElement).not.toHaveBeenCalled(); + }); + + it("appends the `page-script.js` file to the document head when the contentType is `text/html`", () => { + jest.spyOn(window.document.head, "insertBefore").mockImplementation((node) => { + createdScriptElement = node as HTMLScriptElement; + return node; + }); + + require("./page-script-append.mv2"); + + expect(window.document.createElement).toHaveBeenCalledWith("script"); + expect(chrome.runtime.getURL).toHaveBeenCalledWith(Fido2ContentScript.PageScript); + expect(window.document.head.insertBefore).toHaveBeenCalledWith( + expect.any(HTMLScriptElement), + window.document.head.firstChild, + ); + expect(createdScriptElement.src).toBe(`chrome-extension://id/${Fido2ContentScript.PageScript}`); + }); + + it("appends the `page-script.js` file to the document element if the head is not available", () => { + window.document.documentElement.removeChild(window.document.head); + jest.spyOn(window.document.documentElement, "insertBefore").mockImplementation((node) => { + createdScriptElement = node as HTMLScriptElement; + return node; + }); + + require("./page-script-append.mv2"); + + expect(window.document.createElement).toHaveBeenCalledWith("script"); + expect(chrome.runtime.getURL).toHaveBeenCalledWith(Fido2ContentScript.PageScript); + expect(window.document.documentElement.insertBefore).toHaveBeenCalledWith( + expect.any(HTMLScriptElement), + window.document.documentElement.firstChild, + ); + expect(createdScriptElement.src).toBe(`chrome-extension://id/${Fido2ContentScript.PageScript}`); + }); + + it("removes the appended `page-script.js` file after the script has triggered a load event", () => { + createdScriptElement = document.createElement("script"); + jest.spyOn(window.document, "createElement").mockImplementation((element) => { + return createdScriptElement; + }); + + require("./page-script-append.mv2"); + + jest.spyOn(createdScriptElement, "remove"); + createdScriptElement.dispatchEvent(new Event("load")); + + expect(createdScriptElement.remove).toHaveBeenCalled(); + }); +}); diff --git a/apps/browser/src/vault/fido2/content/page-script-append.mv2.ts b/apps/browser/src/vault/fido2/content/page-script-append.mv2.ts new file mode 100644 index 0000000000..4e806d2990 --- /dev/null +++ b/apps/browser/src/vault/fido2/content/page-script-append.mv2.ts @@ -0,0 +1,19 @@ +/** + * This script handles injection of the FIDO2 override page script into the document. + * This is required for manifest v2, but will be removed when we migrate fully to manifest v3. + */ +import { Fido2ContentScript } from "../enums/fido2-content-script.enum"; + +(function (globalContext) { + if (globalContext.document.contentType !== "text/html") { + return; + } + + const script = globalContext.document.createElement("script"); + script.src = chrome.runtime.getURL(Fido2ContentScript.PageScript); + script.addEventListener("load", () => script.remove()); + + const scriptInsertionPoint = + globalContext.document.head || globalContext.document.documentElement; + scriptInsertionPoint.insertBefore(script, scriptInsertionPoint.firstChild); +})(globalThis); diff --git a/apps/browser/src/vault/fido2/content/page-script.ts b/apps/browser/src/vault/fido2/content/page-script.ts index 9adea68307..1de0f3258a 100644 --- a/apps/browser/src/vault/fido2/content/page-script.ts +++ b/apps/browser/src/vault/fido2/content/page-script.ts @@ -5,212 +5,229 @@ import { WebauthnUtils } from "../webauthn-utils"; import { MessageType } from "./messaging/message"; import { Messenger } from "./messaging/messenger"; -const BrowserPublicKeyCredential = window.PublicKeyCredential; - -const browserNativeWebauthnSupport = window.PublicKeyCredential != undefined; -let browserNativeWebauthnPlatformAuthenticatorSupport = false; -if (!browserNativeWebauthnSupport) { - // Polyfill webauthn support - try { - // credentials is read-only if supported, use type-casting to force assignment - (navigator as any).credentials = { - async create() { - throw new Error("Webauthn not supported in this browser."); - }, - async get() { - throw new Error("Webauthn not supported in this browser."); - }, - }; - window.PublicKeyCredential = class PolyfillPublicKeyCredential { - static isUserVerifyingPlatformAuthenticatorAvailable() { - return Promise.resolve(true); - } - } as any; - window.AuthenticatorAttestationResponse = - class PolyfillAuthenticatorAttestationResponse {} as any; - } catch { - /* empty */ +(function (globalContext) { + if (globalContext.document.contentType !== "text/html") { + return; } -} + const BrowserPublicKeyCredential = globalContext.PublicKeyCredential; + const BrowserNavigatorCredentials = navigator.credentials; + const BrowserAuthenticatorAttestationResponse = globalContext.AuthenticatorAttestationResponse; -if (browserNativeWebauthnSupport) { - // 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 - BrowserPublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable().then((available) => { - browserNativeWebauthnPlatformAuthenticatorSupport = available; - - if (!available) { - // Polyfill platform authenticator support - window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable = () => - Promise.resolve(true); + const browserNativeWebauthnSupport = globalContext.PublicKeyCredential != undefined; + let browserNativeWebauthnPlatformAuthenticatorSupport = false; + if (!browserNativeWebauthnSupport) { + // Polyfill webauthn support + try { + // credentials are read-only if supported, use type-casting to force assignment + (navigator as any).credentials = { + async create() { + throw new Error("Webauthn not supported in this browser."); + }, + async get() { + throw new Error("Webauthn not supported in this browser."); + }, + }; + globalContext.PublicKeyCredential = class PolyfillPublicKeyCredential { + static isUserVerifyingPlatformAuthenticatorAvailable() { + return Promise.resolve(true); + } + } as any; + globalContext.AuthenticatorAttestationResponse = + class PolyfillAuthenticatorAttestationResponse {} as any; + } catch { + /* empty */ } - }); -} + } else { + void BrowserPublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable().then( + (available) => { + browserNativeWebauthnPlatformAuthenticatorSupport = available; -const browserCredentials = { - create: navigator.credentials.create.bind( - navigator.credentials, - ) as typeof navigator.credentials.create, - get: navigator.credentials.get.bind(navigator.credentials) as typeof navigator.credentials.get, -}; - -const messenger = ((window as any).messenger = Messenger.forDOMCommunication(window)); - -navigator.credentials.create = createWebAuthnCredential; -navigator.credentials.get = getWebAuthnCredential; - -/** - * Creates a new webauthn credential. - * - * @param options Options for creating new credentials. - * @param abortController Abort controller to abort the request if needed. - * @returns Promise that resolves to the new credential object. - */ -async function createWebAuthnCredential( - options?: CredentialCreationOptions, - abortController?: AbortController, -): Promise<Credential> { - if (!isWebauthnCall(options)) { - return await browserCredentials.create(options); - } - - const fallbackSupported = - (options?.publicKey?.authenticatorSelection?.authenticatorAttachment === "platform" && - browserNativeWebauthnPlatformAuthenticatorSupport) || - (options?.publicKey?.authenticatorSelection?.authenticatorAttachment !== "platform" && - browserNativeWebauthnSupport); - try { - const response = await messenger.request( - { - type: MessageType.CredentialCreationRequest, - data: WebauthnUtils.mapCredentialCreationOptions(options, fallbackSupported), + if (!available) { + // Polyfill platform authenticator support + globalContext.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable = () => + Promise.resolve(true); + } }, - abortController, ); + } - if (response.type !== MessageType.CredentialCreationResponse) { - throw new Error("Something went wrong."); - } + const browserCredentials = { + create: navigator.credentials.create.bind( + navigator.credentials, + ) as typeof navigator.credentials.create, + get: navigator.credentials.get.bind(navigator.credentials) as typeof navigator.credentials.get, + }; - return WebauthnUtils.mapCredentialRegistrationResult(response.result); - } catch (error) { - if (error && error.fallbackRequested && fallbackSupported) { - await waitForFocus(); + const messenger = Messenger.forDOMCommunication(window); + let waitForFocusTimeout: number | NodeJS.Timeout; + let focusListenerHandler: () => void; + + navigator.credentials.create = createWebAuthnCredential; + navigator.credentials.get = getWebAuthnCredential; + + /** + * Creates a new webauthn credential. + * + * @param options Options for creating new credentials. + * @returns Promise that resolves to the new credential object. + */ + async function createWebAuthnCredential( + options?: CredentialCreationOptions, + ): Promise<Credential> { + if (!isWebauthnCall(options)) { return await browserCredentials.create(options); } - throw error; - } -} + const authenticatorAttachmentIsPlatform = + options?.publicKey?.authenticatorSelection?.authenticatorAttachment === "platform"; -/** - * Retrieves a webauthn credential. - * - * @param options Options for creating new credentials. - * @param abortController Abort controller to abort the request if needed. - * @returns Promise that resolves to the new credential object. - */ -async function getWebAuthnCredential( - options?: CredentialRequestOptions, - abortController?: AbortController, -): Promise<Credential> { - if (!isWebauthnCall(options)) { - return await browserCredentials.get(options); + const fallbackSupported = + (authenticatorAttachmentIsPlatform && browserNativeWebauthnPlatformAuthenticatorSupport) || + (!authenticatorAttachmentIsPlatform && browserNativeWebauthnSupport); + try { + const response = await messenger.request( + { + type: MessageType.CredentialCreationRequest, + data: WebauthnUtils.mapCredentialCreationOptions(options, fallbackSupported), + }, + options?.signal, + ); + + if (response.type !== MessageType.CredentialCreationResponse) { + throw new Error("Something went wrong."); + } + + return WebauthnUtils.mapCredentialRegistrationResult(response.result); + } catch (error) { + if (error && error.fallbackRequested && fallbackSupported) { + await waitForFocus(); + return await browserCredentials.create(options); + } + + throw error; + } } - const fallbackSupported = browserNativeWebauthnSupport; - - try { - if (options?.mediation && options.mediation !== "optional") { - throw new FallbackRequestedError(); - } - - const response = await messenger.request( - { - type: MessageType.CredentialGetRequest, - data: WebauthnUtils.mapCredentialRequestOptions(options, fallbackSupported), - }, - abortController, - ); - - if (response.type !== MessageType.CredentialGetResponse) { - throw new Error("Something went wrong."); - } - - return WebauthnUtils.mapCredentialAssertResult(response.result); - } catch (error) { - if (error && error.fallbackRequested && fallbackSupported) { - await waitForFocus(); + /** + * Retrieves a webauthn credential. + * + * @param options Options for creating new credentials. + * @returns Promise that resolves to the new credential object. + */ + async function getWebAuthnCredential(options?: CredentialRequestOptions): Promise<Credential> { + if (!isWebauthnCall(options)) { return await browserCredentials.get(options); } - throw error; - } -} + const fallbackSupported = browserNativeWebauthnSupport; -function isWebauthnCall(options?: CredentialCreationOptions | CredentialRequestOptions) { - return options && "publicKey" in options; -} + try { + if (options?.mediation && options.mediation !== "optional") { + throw new FallbackRequestedError(); + } -/** - * Wait for window to be focused. - * Safari doesn't allow scripts to trigger webauthn when window is not focused. - * - * @param fallbackWait How long to wait when the script is not able to add event listeners to `window.top`. Defaults to 500ms. - * @param timeout Maximum time to wait for focus in milliseconds. Defaults to 5 minutes. - * @returns Promise that resolves when window is focused, or rejects if timeout is reached. - */ -async function waitForFocus(fallbackWait = 500, timeout = 5 * 60 * 1000) { - try { - if (window.top.document.hasFocus()) { - return; + const response = await messenger.request( + { + type: MessageType.CredentialGetRequest, + data: WebauthnUtils.mapCredentialRequestOptions(options, fallbackSupported), + }, + options?.signal, + ); + + if (response.type !== MessageType.CredentialGetResponse) { + throw new Error("Something went wrong."); + } + + return WebauthnUtils.mapCredentialAssertResult(response.result); + } catch (error) { + if (error && error.fallbackRequested && fallbackSupported) { + await waitForFocus(); + return await browserCredentials.get(options); + } + + throw error; } - } catch { - // Cannot access window.top due to cross-origin frame, fallback to waiting - return await new Promise((resolve) => window.setTimeout(resolve, fallbackWait)); } - let focusListener; - const focusPromise = new Promise<void>((resolve) => { - focusListener = () => resolve(); - window.top.addEventListener("focus", focusListener); - }); - - let timeoutId; - const timeoutPromise = new Promise<void>((_, reject) => { - timeoutId = window.setTimeout( - () => - reject( - new DOMException("The operation either timed out or was not allowed.", "AbortError"), - ), - timeout, - ); - }); - - try { - await Promise.race([focusPromise, timeoutPromise]); - } finally { - window.top.removeEventListener("focus", focusListener); - window.clearTimeout(timeoutId); - } -} - -/** - * Sets up a listener to handle cleanup or reconnection when the extension's - * context changes due to being reloaded or unloaded. - */ -messenger.handler = (message, abortController) => { - const type = message.type; - - // Handle cleanup for disconnect request - if (type === MessageType.DisconnectRequest && browserNativeWebauthnSupport) { - navigator.credentials.create = browserCredentials.create; - navigator.credentials.get = browserCredentials.get; + function isWebauthnCall(options?: CredentialCreationOptions | CredentialRequestOptions) { + return options && "publicKey" in options; } - // Handle reinitialization for reconnect request - if (type === MessageType.ReconnectRequest && browserNativeWebauthnSupport) { - navigator.credentials.create = createWebAuthnCredential; - navigator.credentials.get = getWebAuthnCredential; + /** + * Wait for window to be focused. + * Safari doesn't allow scripts to trigger webauthn when window is not focused. + * + * @param fallbackWait How long to wait when the script is not able to add event listeners to `window.top`. Defaults to 500ms. + * @param timeout Maximum time to wait for focus in milliseconds. Defaults to 5 minutes. + * @returns Promise that resolves when window is focused, or rejects if timeout is reached. + */ + async function waitForFocus(fallbackWait = 500, timeout = 5 * 60 * 1000) { + try { + if (globalContext.top.document.hasFocus()) { + return; + } + } catch { + // Cannot access window.top due to cross-origin frame, fallback to waiting + return await new Promise((resolve) => globalContext.setTimeout(resolve, fallbackWait)); + } + + const focusPromise = new Promise<void>((resolve) => { + focusListenerHandler = () => resolve(); + globalContext.top.addEventListener("focus", focusListenerHandler); + }); + + const timeoutPromise = new Promise<void>((_, reject) => { + waitForFocusTimeout = globalContext.setTimeout( + () => + reject( + new DOMException("The operation either timed out or was not allowed.", "AbortError"), + ), + timeout, + ); + }); + + try { + await Promise.race([focusPromise, timeoutPromise]); + } finally { + clearWaitForFocus(); + } } -}; + + function clearWaitForFocus() { + globalContext.top.removeEventListener("focus", focusListenerHandler); + if (waitForFocusTimeout) { + globalContext.clearTimeout(waitForFocusTimeout); + } + } + + function destroy() { + try { + if (browserNativeWebauthnSupport) { + navigator.credentials.create = browserCredentials.create; + navigator.credentials.get = browserCredentials.get; + } else { + (navigator as any).credentials = BrowserNavigatorCredentials; + globalContext.PublicKeyCredential = BrowserPublicKeyCredential; + globalContext.AuthenticatorAttestationResponse = BrowserAuthenticatorAttestationResponse; + } + + clearWaitForFocus(); + void messenger.destroy(); + } catch (e) { + /** empty */ + } + } + + /** + * Sets up a listener to handle cleanup or reconnection when the extension's + * context changes due to being reloaded or unloaded. + */ + messenger.handler = (message) => { + const type = message.type; + + // Handle cleanup for disconnect request + if (type === MessageType.DisconnectRequest) { + destroy(); + } + }; +})(globalThis); diff --git a/apps/browser/src/vault/fido2/content/page-script.webauthn-supported.spec.ts b/apps/browser/src/vault/fido2/content/page-script.webauthn-supported.spec.ts new file mode 100644 index 0000000000..211959c466 --- /dev/null +++ b/apps/browser/src/vault/fido2/content/page-script.webauthn-supported.spec.ts @@ -0,0 +1,121 @@ +import { + createAssertCredentialResultMock, + createCreateCredentialResultMock, + createCredentialCreationOptionsMock, + createCredentialRequestOptionsMock, + setupMockedWebAuthnSupport, +} from "../../../autofill/spec/fido2-testing-utils"; +import { WebauthnUtils } from "../webauthn-utils"; + +import { MessageType } from "./messaging/message"; +import { Messenger } from "./messaging/messenger"; + +let messenger: Messenger; +jest.mock("./messaging/messenger", () => { + return { + Messenger: class extends jest.requireActual("./messaging/messenger").Messenger { + static forDOMCommunication: any = jest.fn((window) => { + const windowOrigin = window.location.origin; + + messenger = new Messenger({ + postMessage: (message, port) => window.postMessage(message, windowOrigin, [port]), + addEventListener: (listener) => window.addEventListener("message", listener), + removeEventListener: (listener) => window.removeEventListener("message", listener), + }); + messenger.destroy = jest.fn(); + return messenger; + }); + }, + }; +}); +jest.mock("../webauthn-utils"); + +describe("Fido2 page script with native WebAuthn support", () => { + const mockCredentialCreationOptions = createCredentialCreationOptionsMock(); + const mockCreateCredentialsResult = createCreateCredentialResultMock(); + const mockCredentialRequestOptions = createCredentialRequestOptionsMock(); + const mockCredentialAssertResult = createAssertCredentialResultMock(); + setupMockedWebAuthnSupport(); + + require("./page-script"); + + afterAll(() => { + jest.clearAllMocks(); + jest.resetModules(); + }); + + describe("creating WebAuthn credentials", () => { + beforeEach(() => { + messenger.request = jest.fn().mockResolvedValue({ + type: MessageType.CredentialCreationResponse, + result: mockCreateCredentialsResult, + }); + }); + + it("falls back to the default browser credentials API if an error occurs", async () => { + window.top.document.hasFocus = jest.fn().mockReturnValue(true); + messenger.request = jest.fn().mockRejectedValue({ fallbackRequested: true }); + + try { + await navigator.credentials.create(mockCredentialCreationOptions); + expect("This will fail the test").toBe(true); + } catch { + expect(WebauthnUtils.mapCredentialRegistrationResult).not.toHaveBeenCalled(); + } + }); + + it("creates and returns a WebAuthn credential when the navigator API is called to create credentials", async () => { + await navigator.credentials.create(mockCredentialCreationOptions); + + expect(WebauthnUtils.mapCredentialCreationOptions).toHaveBeenCalledWith( + mockCredentialCreationOptions, + true, + ); + expect(WebauthnUtils.mapCredentialRegistrationResult).toHaveBeenCalledWith( + mockCreateCredentialsResult, + ); + }); + }); + + describe("get WebAuthn credentials", () => { + beforeEach(() => { + messenger.request = jest.fn().mockResolvedValue({ + type: MessageType.CredentialGetResponse, + result: mockCredentialAssertResult, + }); + }); + + it("falls back to the default browser credentials API when an error occurs", async () => { + window.top.document.hasFocus = jest.fn().mockReturnValue(true); + messenger.request = jest.fn().mockRejectedValue({ fallbackRequested: true }); + + const returnValue = await navigator.credentials.get(mockCredentialRequestOptions); + + expect(returnValue).toBeDefined(); + expect(WebauthnUtils.mapCredentialAssertResult).not.toHaveBeenCalled(); + }); + + it("gets and returns the WebAuthn credentials", async () => { + await navigator.credentials.get(mockCredentialRequestOptions); + + expect(WebauthnUtils.mapCredentialRequestOptions).toHaveBeenCalledWith( + mockCredentialRequestOptions, + true, + ); + expect(WebauthnUtils.mapCredentialAssertResult).toHaveBeenCalledWith( + mockCredentialAssertResult, + ); + }); + }); + + describe("destroy", () => { + it("should destroy the message listener when receiving a disconnect request", async () => { + jest.spyOn(globalThis.top, "removeEventListener"); + const SENDER = "bitwarden-webauthn"; + void messenger.handler({ type: MessageType.DisconnectRequest, SENDER, senderId: "1" }); + + expect(globalThis.top.removeEventListener).toHaveBeenCalledWith("focus", undefined); + expect(messenger.destroy).toHaveBeenCalled(); + }); + }); +}); diff --git a/apps/browser/src/vault/fido2/content/page-script.webauthn-unsupported.spec.ts b/apps/browser/src/vault/fido2/content/page-script.webauthn-unsupported.spec.ts new file mode 100644 index 0000000000..f3aee685e1 --- /dev/null +++ b/apps/browser/src/vault/fido2/content/page-script.webauthn-unsupported.spec.ts @@ -0,0 +1,96 @@ +import { + createAssertCredentialResultMock, + createCreateCredentialResultMock, + createCredentialCreationOptionsMock, + createCredentialRequestOptionsMock, +} from "../../../autofill/spec/fido2-testing-utils"; +import { WebauthnUtils } from "../webauthn-utils"; + +import { MessageType } from "./messaging/message"; +import { Messenger } from "./messaging/messenger"; + +let messenger: Messenger; +jest.mock("./messaging/messenger", () => { + return { + Messenger: class extends jest.requireActual("./messaging/messenger").Messenger { + static forDOMCommunication: any = jest.fn((window) => { + const windowOrigin = window.location.origin; + + messenger = new Messenger({ + postMessage: (message, port) => window.postMessage(message, windowOrigin, [port]), + addEventListener: (listener) => window.addEventListener("message", listener), + removeEventListener: (listener) => window.removeEventListener("message", listener), + }); + messenger.destroy = jest.fn(); + return messenger; + }); + }, + }; +}); +jest.mock("../webauthn-utils"); + +describe("Fido2 page script without native WebAuthn support", () => { + const mockCredentialCreationOptions = createCredentialCreationOptionsMock(); + const mockCreateCredentialsResult = createCreateCredentialResultMock(); + const mockCredentialRequestOptions = createCredentialRequestOptionsMock(); + const mockCredentialAssertResult = createAssertCredentialResultMock(); + require("./page-script"); + + afterAll(() => { + jest.clearAllMocks(); + jest.resetModules(); + }); + + describe("creating WebAuthn credentials", () => { + beforeEach(() => { + messenger.request = jest.fn().mockResolvedValue({ + type: MessageType.CredentialCreationResponse, + result: mockCreateCredentialsResult, + }); + }); + + it("creates and returns a WebAuthn credential", async () => { + await navigator.credentials.create(mockCredentialCreationOptions); + + expect(WebauthnUtils.mapCredentialCreationOptions).toHaveBeenCalledWith( + mockCredentialCreationOptions, + false, + ); + expect(WebauthnUtils.mapCredentialRegistrationResult).toHaveBeenCalledWith( + mockCreateCredentialsResult, + ); + }); + }); + + describe("get WebAuthn credentials", () => { + beforeEach(() => { + messenger.request = jest.fn().mockResolvedValue({ + type: MessageType.CredentialGetResponse, + result: mockCredentialAssertResult, + }); + }); + + it("gets and returns the WebAuthn credentials", async () => { + await navigator.credentials.get(mockCredentialRequestOptions); + + expect(WebauthnUtils.mapCredentialRequestOptions).toHaveBeenCalledWith( + mockCredentialRequestOptions, + false, + ); + expect(WebauthnUtils.mapCredentialAssertResult).toHaveBeenCalledWith( + mockCredentialAssertResult, + ); + }); + }); + + describe("destroy", () => { + it("should destroy the message listener when receiving a disconnect request", async () => { + jest.spyOn(globalThis.top, "removeEventListener"); + const SENDER = "bitwarden-webauthn"; + void messenger.handler({ type: MessageType.DisconnectRequest, SENDER, senderId: "1" }); + + expect(globalThis.top.removeEventListener).toHaveBeenCalledWith("focus", undefined); + expect(messenger.destroy).toHaveBeenCalled(); + }); + }); +}); diff --git a/apps/browser/src/vault/fido2/content/trigger-fido2-content-script-injection.spec.ts b/apps/browser/src/vault/fido2/content/trigger-fido2-content-script-injection.spec.ts deleted file mode 100644 index 8f4efe0330..0000000000 --- a/apps/browser/src/vault/fido2/content/trigger-fido2-content-script-injection.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -describe("TriggerFido2ContentScriptInjection", () => { - afterEach(() => { - jest.resetModules(); - jest.clearAllMocks(); - }); - - describe("init", () => { - it("sends a message to the extension background", () => { - require("../content/trigger-fido2-content-script-injection"); - - expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({ - command: "triggerFido2ContentScriptInjection", - }); - }); - }); -}); diff --git a/apps/browser/src/vault/fido2/content/trigger-fido2-content-script-injection.ts b/apps/browser/src/vault/fido2/content/trigger-fido2-content-script-injection.ts deleted file mode 100644 index 7ca6956729..0000000000 --- a/apps/browser/src/vault/fido2/content/trigger-fido2-content-script-injection.ts +++ /dev/null @@ -1,5 +0,0 @@ -(function () { - // 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({ command: "triggerFido2ContentScriptInjection" }); -})(); diff --git a/apps/browser/src/vault/fido2/enums/fido2-content-script.enum.ts b/apps/browser/src/vault/fido2/enums/fido2-content-script.enum.ts new file mode 100644 index 0000000000..287de6804b --- /dev/null +++ b/apps/browser/src/vault/fido2/enums/fido2-content-script.enum.ts @@ -0,0 +1,10 @@ +export const Fido2ContentScript = { + PageScript: "content/fido2/page-script.js", + PageScriptAppend: "content/fido2/page-script-append-mv2.js", + ContentScript: "content/fido2/content-script.js", +} as const; + +export const Fido2ContentScriptId = { + PageScript: "fido2-page-script-registration", + ContentScript: "fido2-content-script-registration", +} as const; diff --git a/apps/browser/src/vault/fido2/enums/fido2-port-name.enum.ts b/apps/browser/src/vault/fido2/enums/fido2-port-name.enum.ts new file mode 100644 index 0000000000..7836247425 --- /dev/null +++ b/apps/browser/src/vault/fido2/enums/fido2-port-name.enum.ts @@ -0,0 +1,3 @@ +export const Fido2PortName = { + InjectedScript: "fido2-injected-content-script-port", +} as const; diff --git a/apps/browser/src/vault/services/abstractions/fido2.service.ts b/apps/browser/src/vault/services/abstractions/fido2.service.ts deleted file mode 100644 index 138b538b15..0000000000 --- a/apps/browser/src/vault/services/abstractions/fido2.service.ts +++ /dev/null @@ -1,4 +0,0 @@ -export abstract class Fido2Service { - init: () => Promise<void>; - injectFido2ContentScripts: (sender: chrome.runtime.MessageSender) => Promise<void>; -} diff --git a/apps/browser/src/vault/services/fido2.service.spec.ts b/apps/browser/src/vault/services/fido2.service.spec.ts deleted file mode 100644 index 1db2bdfb77..0000000000 --- a/apps/browser/src/vault/services/fido2.service.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { BrowserApi } from "../../platform/browser/browser-api"; - -import Fido2Service from "./fido2.service"; - -describe("Fido2Service", () => { - let fido2Service: Fido2Service; - let tabMock: chrome.tabs.Tab; - let sender: chrome.runtime.MessageSender; - - beforeEach(() => { - fido2Service = new Fido2Service(); - tabMock = { id: 123, url: "https://bitwarden.com" } as chrome.tabs.Tab; - sender = { tab: tabMock }; - jest.spyOn(BrowserApi, "executeScriptInTab").mockImplementation(); - }); - - afterEach(() => { - jest.resetModules(); - jest.clearAllMocks(); - }); - - describe("injectFido2ContentScripts", () => { - const fido2ContentScript = "content/fido2/content-script.js"; - const defaultExecuteScriptOptions = { runAt: "document_start" }; - - it("accepts an extension message sender and injects the fido2 scripts into the tab of the sender", async () => { - await fido2Service.injectFido2ContentScripts(sender); - - expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith(tabMock.id, { - file: fido2ContentScript, - ...defaultExecuteScriptOptions, - }); - }); - }); -}); diff --git a/apps/browser/src/vault/services/fido2.service.ts b/apps/browser/src/vault/services/fido2.service.ts deleted file mode 100644 index 98b440b109..0000000000 --- a/apps/browser/src/vault/services/fido2.service.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { BrowserApi } from "../../platform/browser/browser-api"; - -import { Fido2Service as Fido2ServiceInterface } from "./abstractions/fido2.service"; - -export default class Fido2Service implements Fido2ServiceInterface { - async init() { - const tabs = await BrowserApi.tabsQuery({}); - tabs.forEach((tab) => { - if (tab.url?.startsWith("https")) { - // 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.injectFido2ContentScripts({ tab } as chrome.runtime.MessageSender); - } - }); - - BrowserApi.addListener(chrome.runtime.onConnect, (port) => { - if (port.name === "fido2ContentScriptReady") { - port.postMessage({ command: "fido2ContentScriptInit" }); - } - }); - } - - /** - * Injects the FIDO2 content script into the current tab. - * @param {chrome.runtime.MessageSender} sender - * @returns {Promise<void>} - */ - async injectFido2ContentScripts(sender: chrome.runtime.MessageSender): Promise<void> { - await BrowserApi.executeScriptInTab(sender.tab.id, { - file: "content/fido2/content-script.js", - frameId: sender.frameId, - runAt: "document_start", - }); - } -} diff --git a/apps/browser/test.setup.ts b/apps/browser/test.setup.ts index 1031268186..4800b4c17f 100644 --- a/apps/browser/test.setup.ts +++ b/apps/browser/test.setup.ts @@ -68,6 +68,8 @@ const tabs = { const scripting = { executeScript: jest.fn(), + registerContentScripts: jest.fn(), + unregisterContentScripts: jest.fn(), }; const windows = { @@ -124,6 +126,19 @@ const offscreen = { }, }; +const permissions = { + contains: jest.fn((permissions, callback) => { + callback(true); + }), +}; + +const webNavigation = { + onCommitted: { + addListener: jest.fn(), + removeListener: jest.fn(), + }, +}; + // set chrome global.chrome = { i18n, @@ -137,4 +152,6 @@ global.chrome = { privacy, extension, offscreen, + permissions, + webNavigation, } as any; diff --git a/apps/browser/tsconfig.json b/apps/browser/tsconfig.json index 694246f59a..505f1533ae 100644 --- a/apps/browser/tsconfig.json +++ b/apps/browser/tsconfig.json @@ -9,6 +9,7 @@ "allowJs": true, "sourceMap": true, "baseUrl": ".", + "lib": ["ES2021.String"], "paths": { "@bitwarden/admin-console": ["../../libs/admin-console/src"], "@bitwarden/angular/*": ["../../libs/angular/src/*"], diff --git a/apps/browser/webpack.config.js b/apps/browser/webpack.config.js index 3b5724b198..2756ab4395 100644 --- a/apps/browser/webpack.config.js +++ b/apps/browser/webpack.config.js @@ -166,8 +166,6 @@ const mainConfig = { "content/notificationBar": "./src/autofill/content/notification-bar.ts", "content/contextMenuHandler": "./src/autofill/content/context-menu-handler.ts", "content/content-message-handler": "./src/autofill/content/content-message-handler.ts", - "content/fido2/trigger-fido2-content-script-injection": - "./src/vault/fido2/content/trigger-fido2-content-script-injection.ts", "content/fido2/content-script": "./src/vault/fido2/content/content-script.ts", "content/fido2/page-script": "./src/vault/fido2/content/page-script.ts", "notification/bar": "./src/autofill/notification/bar.ts", @@ -277,6 +275,8 @@ if (manifestVersion == 2) { mainConfig.entry.background = "./src/platform/background.ts"; mainConfig.entry["content/lp-suppress-import-download-script-append-mv2"] = "./src/tools/content/lp-suppress-import-download-script-append.mv2.ts"; + mainConfig.entry["content/fido2/page-script-append-mv2"] = + "./src/vault/fido2/content/page-script-append.mv2.ts"; configs.push(mainConfig); } else { 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 bfc8cbe915..4e0aab017a 100644 --- a/libs/common/src/vault/services/fido2/fido2-client.service.ts +++ b/libs/common/src/vault/services/fido2/fido2-client.service.ts @@ -48,20 +48,26 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction { ) {} async isFido2FeatureEnabled(hostname: string, origin: string): Promise<boolean> { - const userEnabledPasskeys = await firstValueFrom(this.vaultSettingsService.enablePasskeys$); const isUserLoggedIn = (await this.authService.getAuthStatus()) !== AuthenticationStatus.LoggedOut; + if (!isUserLoggedIn) { + return false; + } const neverDomains = await firstValueFrom(this.domainSettingsService.neverDomains$); const isExcludedDomain = neverDomains != null && hostname in neverDomains; + if (isExcludedDomain) { + return false; + } const serverConfig = await firstValueFrom(this.configService.serverConfig$); const isOriginEqualBitwardenVault = origin === serverConfig.environment?.vault; + if (isOriginEqualBitwardenVault) { + return false; + } - return ( - userEnabledPasskeys && isUserLoggedIn && !isExcludedDomain && !isOriginEqualBitwardenVault - ); + return await firstValueFrom(this.vaultSettingsService.enablePasskeys$); } async createCredential( @@ -70,6 +76,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction { abortController = new AbortController(), ): Promise<CreateCredentialResult> { const parsedOrigin = parse(params.origin, { allowPrivateDomains: true }); + const enableFido2VaultCredentials = await this.isFido2FeatureEnabled( parsedOrigin.hostname, params.origin, @@ -346,7 +353,7 @@ function setAbortTimeout( ); } - return window.setTimeout(() => abortController.abort(), clampedTimeout); + return self.setTimeout(() => abortController.abort(), clampedTimeout); } /** diff --git a/libs/common/src/vault/services/fido2/fido2-utils.spec.ts b/libs/common/src/vault/services/fido2/fido2-utils.spec.ts new file mode 100644 index 0000000000..a05eab5230 --- /dev/null +++ b/libs/common/src/vault/services/fido2/fido2-utils.spec.ts @@ -0,0 +1,40 @@ +import { Fido2Utils } from "./fido2-utils"; + +describe("Fido2 Utils", () => { + const asciiHelloWorldArray = [104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]; + const b64HelloWorldString = "aGVsbG8gd29ybGQ="; + + describe("fromBufferToB64(...)", () => { + it("should convert an ArrayBuffer to a b64 string", () => { + const buffer = new Uint8Array(asciiHelloWorldArray).buffer; + const b64String = Fido2Utils.fromBufferToB64(buffer); + expect(b64String).toBe(b64HelloWorldString); + }); + + it("should return an empty string when given an empty ArrayBuffer", () => { + const buffer = new Uint8Array([]).buffer; + const b64String = Fido2Utils.fromBufferToB64(buffer); + expect(b64String).toBe(""); + }); + + it("should return null when given null input", () => { + const b64String = Fido2Utils.fromBufferToB64(null); + expect(b64String).toBeNull(); + }); + }); + + describe("fromB64ToArray(...)", () => { + it("should convert a b64 string to an Uint8Array", () => { + const expectedArray = new Uint8Array(asciiHelloWorldArray); + + const resultArray = Fido2Utils.fromB64ToArray(b64HelloWorldString); + + expect(resultArray).toEqual(expectedArray); + }); + + it("should return null when given null input", () => { + const expectedArray = Fido2Utils.fromB64ToArray(null); + expect(expectedArray).toBeNull(); + }); + }); +}); diff --git a/libs/common/src/vault/services/fido2/fido2-utils.ts b/libs/common/src/vault/services/fido2/fido2-utils.ts index a2de137550..13c9762135 100644 --- a/libs/common/src/vault/services/fido2/fido2-utils.ts +++ b/libs/common/src/vault/services/fido2/fido2-utils.ts @@ -1,14 +1,20 @@ -import { Utils } from "../../../platform/misc/utils"; - export class Fido2Utils { static bufferToString(bufferSource: BufferSource): string { - const buffer = Fido2Utils.bufferSourceToUint8Array(bufferSource); + let buffer: Uint8Array; + if (bufferSource instanceof ArrayBuffer || bufferSource.buffer === undefined) { + buffer = new Uint8Array(bufferSource as ArrayBuffer); + } else { + buffer = new Uint8Array(bufferSource.buffer); + } - return Utils.fromBufferToUrlB64(buffer); + return Fido2Utils.fromBufferToB64(buffer) + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=/g, ""); } static stringToBuffer(str: string): Uint8Array { - return Utils.fromUrlB64ToArray(str); + return Fido2Utils.fromB64ToArray(Fido2Utils.fromUrlB64ToB64(str)); } static bufferSourceToUint8Array(bufferSource: BufferSource) { @@ -23,4 +29,52 @@ export class Fido2Utils { private static isArrayBuffer(bufferSource: BufferSource): bufferSource is ArrayBuffer { return bufferSource instanceof ArrayBuffer || bufferSource.buffer === undefined; } + + static fromB64toUrlB64(b64Str: string) { + return b64Str.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); + } + + static fromBufferToB64(buffer: ArrayBuffer): string { + if (buffer == null) { + return null; + } + + let binary = ""; + const bytes = new Uint8Array(buffer); + for (let i = 0; i < bytes.byteLength; i++) { + binary += String.fromCharCode(bytes[i]); + } + return globalThis.btoa(binary); + } + + static fromB64ToArray(str: string): Uint8Array { + if (str == null) { + return null; + } + + const binaryString = globalThis.atob(str); + const bytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + return bytes; + } + + static fromUrlB64ToB64(urlB64Str: string): string { + let output = urlB64Str.replace(/-/g, "+").replace(/_/g, "/"); + switch (output.length % 4) { + case 0: + break; + case 2: + output += "=="; + break; + case 3: + output += "="; + break; + default: + throw new Error("Illegal base64url string!"); + } + + return output; + } } From d5f503a0d6b855442ce55b9c042357393d7394bf Mon Sep 17 00:00:00 2001 From: Will Martin <contact@willmartian.com> Date: Thu, 18 Apr 2024 13:23:35 -0400 Subject: [PATCH 219/351] [CL-18] toast component and service (#6490) Update toast styles and new service to CL. --- apps/browser/src/_locales/en/messages.json | 3 + .../foreground-platform-utils.service.ts | 23 +--- apps/browser/src/popup/app.component.ts | 17 ++- apps/browser/src/popup/app.module.ts | 5 +- apps/browser/src/popup/scss/plugins.scss | 98 -------------- apps/browser/src/popup/scss/popup.scss | 1 - .../src/popup/services/services.module.ts | 16 +-- apps/desktop/src/app/app.component.ts | 38 +----- apps/desktop/src/locales/en/messages.json | 3 + apps/desktop/src/scss/plugins.scss | 95 -------------- apps/desktop/src/scss/styles.scss | 1 - apps/web/src/app/app.component.ts | 39 +----- apps/web/src/app/shared/shared.module.ts | 3 - apps/web/src/locales/en/messages.json | 3 + apps/web/src/scss/styles.scss | 3 - apps/web/src/scss/toasts.scss | 117 ----------------- .../src/components/toastr.component.ts | 98 -------------- libs/angular/src/jslib.module.ts | 7 +- .../abstractions/platform-utils.service.ts | 5 + libs/components/src/index.ts | 1 + libs/components/src/toast/index.ts | 2 + .../components/src/toast/toast.component.html | 24 ++++ libs/components/src/toast/toast.component.ts | 66 ++++++++++ libs/components/src/toast/toast.module.ts | 39 ++++++ libs/components/src/toast/toast.service.ts | 57 ++++++++ libs/components/src/toast/toast.spec.ts | 16 +++ libs/components/src/toast/toast.stories.ts | 124 ++++++++++++++++++ libs/components/src/toast/toast.tokens.css | 4 + libs/components/src/toast/toastr.component.ts | 26 ++++ libs/components/src/toast/toastr.css | 23 ++++ libs/components/src/toast/utils.ts | 14 ++ libs/components/src/tw-theme.css | 3 + 32 files changed, 440 insertions(+), 534 deletions(-) delete mode 100644 apps/browser/src/popup/scss/plugins.scss delete mode 100644 apps/desktop/src/scss/plugins.scss delete mode 100644 apps/web/src/scss/toasts.scss delete mode 100644 libs/angular/src/components/toastr.component.ts create mode 100644 libs/components/src/toast/index.ts create mode 100644 libs/components/src/toast/toast.component.html create mode 100644 libs/components/src/toast/toast.component.ts create mode 100644 libs/components/src/toast/toast.module.ts create mode 100644 libs/components/src/toast/toast.service.ts create mode 100644 libs/components/src/toast/toast.spec.ts create mode 100644 libs/components/src/toast/toast.stories.ts create mode 100644 libs/components/src/toast/toast.tokens.css create mode 100644 libs/components/src/toast/toastr.component.ts create mode 100644 libs/components/src/toast/toastr.css create mode 100644 libs/components/src/toast/utils.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 36e3ce65a8..8c81088fc5 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -3000,6 +3000,9 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, diff --git a/apps/browser/src/platform/services/platform-utils/foreground-platform-utils.service.ts b/apps/browser/src/platform/services/platform-utils/foreground-platform-utils.service.ts index 8cf1a8d3e4..24aa45d5c3 100644 --- a/apps/browser/src/platform/services/platform-utils/foreground-platform-utils.service.ts +++ b/apps/browser/src/platform/services/platform-utils/foreground-platform-utils.service.ts @@ -1,13 +1,10 @@ -import { SecurityContext } from "@angular/core"; -import { DomSanitizer } from "@angular/platform-browser"; -import { ToastrService } from "ngx-toastr"; +import { ToastService } from "@bitwarden/components"; import { BrowserPlatformUtilsService } from "./browser-platform-utils.service"; export class ForegroundPlatformUtilsService extends BrowserPlatformUtilsService { constructor( - private sanitizer: DomSanitizer, - private toastrService: ToastrService, + private toastService: ToastService, clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void, biometricCallback: () => Promise<boolean>, win: Window & typeof globalThis, @@ -21,20 +18,6 @@ export class ForegroundPlatformUtilsService extends BrowserPlatformUtilsService text: string | string[], options?: any, ): void { - if (typeof text === "string") { - // Already in the correct format - } else if (text.length === 1) { - text = text[0]; - } else { - let message = ""; - text.forEach( - (t: string) => - (message += "<p>" + this.sanitizer.sanitize(SecurityContext.HTML, t) + "</p>"), - ); - text = message; - options.enableHtml = true; - } - this.toastrService.show(text, title, options, "toast-" + type); - // noop + this.toastService._showToast({ type, title, text, options }); } } diff --git a/apps/browser/src/popup/app.component.ts b/apps/browser/src/popup/app.component.ts index c224e652f6..2aba93ac95 100644 --- a/apps/browser/src/popup/app.component.ts +++ b/apps/browser/src/popup/app.component.ts @@ -1,19 +1,18 @@ import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core"; import { NavigationEnd, Router, RouterOutlet } from "@angular/router"; -import { ToastrService } from "ngx-toastr"; import { filter, concatMap, Subject, takeUntil, firstValueFrom, map } from "rxjs"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { DialogService, SimpleDialogOptions } from "@bitwarden/components"; +import { DialogService, SimpleDialogOptions, ToastService } from "@bitwarden/components"; import { BrowserApi } from "../platform/browser/browser-api"; import { ZonedMessageListenerService } from "../platform/browser/zoned-message-listener.service"; import { BrowserStateService } from "../platform/services/abstractions/browser-state.service"; -import { ForegroundPlatformUtilsService } from "../platform/services/platform-utils/foreground-platform-utils.service"; import { BrowserSendStateService } from "../tools/popup/services/browser-send-state.service"; import { VaultBrowserStateService } from "../vault/services/vault-browser-state.service"; @@ -35,7 +34,6 @@ export class AppComponent implements OnInit, OnDestroy { private destroy$ = new Subject<void>(); constructor( - private toastrService: ToastrService, private broadcasterService: BroadcasterService, private authService: AuthService, private i18nService: I18nService, @@ -46,9 +44,10 @@ export class AppComponent implements OnInit, OnDestroy { private cipherService: CipherService, private changeDetectorRef: ChangeDetectorRef, private ngZone: NgZone, - private platformUtilsService: ForegroundPlatformUtilsService, + private platformUtilsService: PlatformUtilsService, private dialogService: DialogService, private browserMessagingApi: ZonedMessageListenerService, + private toastService: ToastService, ) {} async ngOnInit() { @@ -83,10 +82,10 @@ export class AppComponent implements OnInit, OnDestroy { if (msg.command === "doneLoggingOut") { this.authService.logOut(async () => { if (msg.expired) { - this.showToast({ - type: "warning", + this.toastService.showToast({ + variant: "warning", title: this.i18nService.t("loggedOut"), - text: this.i18nService.t("loginExpired"), + message: this.i18nService.t("loginExpired"), }); } @@ -116,7 +115,7 @@ export class AppComponent implements OnInit, OnDestroy { // eslint-disable-next-line @typescript-eslint/no-floating-promises this.showNativeMessagingFingerprintDialog(msg); } else if (msg.command === "showToast") { - this.showToast(msg); + this.toastService._showToast(msg); } else if (msg.command === "reloadProcess") { const forceWindowReload = this.platformUtilsService.isSafari() || diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index d179868448..5718542b01 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -11,11 +11,10 @@ import { BrowserModule } from "@angular/platform-browser"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components/environment-selector.component"; -import { BitwardenToastModule } from "@bitwarden/angular/components/toastr.component"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { ColorPasswordCountPipe } from "@bitwarden/angular/pipes/color-password-count.pipe"; import { ColorPasswordPipe } from "@bitwarden/angular/pipes/color-password.pipe"; -import { AvatarModule, ButtonModule } from "@bitwarden/components"; +import { AvatarModule, ButtonModule, ToastModule } from "@bitwarden/components"; import { ExportScopeCalloutComponent } from "@bitwarden/vault-export-ui"; import { AccountSwitcherComponent } from "../auth/popup/account-switching/account-switcher.component"; @@ -87,7 +86,7 @@ import "../platform/popup/locales"; imports: [ A11yModule, AppRoutingModule, - BitwardenToastModule.forRoot({ + ToastModule.forRoot({ maxOpened: 2, autoDismiss: true, closeButton: true, diff --git a/apps/browser/src/popup/scss/plugins.scss b/apps/browser/src/popup/scss/plugins.scss deleted file mode 100644 index e1e386d62d..0000000000 --- a/apps/browser/src/popup/scss/plugins.scss +++ /dev/null @@ -1,98 +0,0 @@ -@import "~ngx-toastr/toastr"; - -@import "variables.scss"; -@import "buttons.scss"; - -// Toaster - -.toast-container { - .toast-close-button { - @include themify($themes) { - color: themed("toastTextColor"); - } - font-size: 18px; - margin-right: 4px; - } - - .ngx-toastr { - @include themify($themes) { - color: themed("toastTextColor"); - } - align-items: center; - background-image: none !important; - border-radius: $border-radius; - box-shadow: 0 0 8px rgba(0, 0, 0, 0.35); - display: flex; - padding: 15px; - - .toast-close-button { - position: absolute; - right: 5px; - top: 0; - } - - &:hover { - box-shadow: 0 0 10px rgba(0, 0, 0, 0.6); - } - - .icon i::before { - float: left; - font-style: normal; - font-family: $icomoon-font-family; - font-size: 25px; - line-height: 20px; - padding-right: 15px; - } - - .toast-message { - p { - margin-bottom: 0.5rem; - - &:last-child { - margin-bottom: 0; - } - } - } - - &.toast-danger, - &.toast-error { - @include themify($themes) { - background-color: themed("dangerColor"); - } - - .icon i::before { - content: map_get($icons, "error"); - } - } - - &.toast-warning { - @include themify($themes) { - background-color: themed("warningColor"); - } - - .icon i::before { - content: map_get($icons, "exclamation-triangle"); - } - } - - &.toast-info { - @include themify($themes) { - background-color: themed("infoColor"); - } - - .icon i:before { - content: map_get($icons, "info-circle"); - } - } - - &.toast-success { - @include themify($themes) { - background-color: themed("successColor"); - } - - .icon i:before { - content: map_get($icons, "check"); - } - } - } -} diff --git a/apps/browser/src/popup/scss/popup.scss b/apps/browser/src/popup/scss/popup.scss index 0d7e428138..850ef96c64 100644 --- a/apps/browser/src/popup/scss/popup.scss +++ b/apps/browser/src/popup/scss/popup.scss @@ -8,7 +8,6 @@ @import "buttons.scss"; @import "misc.scss"; @import "modal.scss"; -@import "plugins.scss"; @import "environment.scss"; @import "pages.scss"; @import "@angular/cdk/overlay-prebuilt.css"; diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 4906198047..f3be8490c1 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -1,7 +1,5 @@ import { APP_INITIALIZER, NgModule, NgZone } from "@angular/core"; -import { DomSanitizer } from "@angular/platform-browser"; import { Router } from "@angular/router"; -import { ToastrService } from "ngx-toastr"; import { UnauthGuard as BaseUnauthGuardService } from "@bitwarden/angular/auth/guards"; import { AngularThemingService } from "@bitwarden/angular/platform/services/theming/angular-theming.service"; @@ -83,7 +81,7 @@ import { FolderService as FolderServiceAbstraction } from "@bitwarden/common/vau import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service"; import { TotpService } from "@bitwarden/common/vault/services/totp.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { UnauthGuardService } from "../../auth/popup/services"; import { AutofillService as AutofillServiceAbstraction } from "../../autofill/services/abstractions/autofill.service"; @@ -259,15 +257,9 @@ const safeProviders: SafeProvider[] = [ }), safeProvider({ provide: PlatformUtilsService, - useExisting: ForegroundPlatformUtilsService, - }), - safeProvider({ - provide: ForegroundPlatformUtilsService, - useClass: ForegroundPlatformUtilsService, - useFactory: (sanitizer: DomSanitizer, toastrService: ToastrService) => { + useFactory: (toastService: ToastService) => { return new ForegroundPlatformUtilsService( - sanitizer, - toastrService, + toastService, (clipboardValue: string, clearMs: number) => { void BrowserApi.sendMessage("clearClipboard", { clipboardValue, clearMs }); }, @@ -284,7 +276,7 @@ const safeProviders: SafeProvider[] = [ window, ); }, - deps: [DomSanitizer, ToastrService], + deps: [ToastService], }), safeProvider({ provide: PasswordGenerationServiceAbstraction, diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index b2b44e6b21..ad99a3a447 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -3,14 +3,11 @@ import { NgZone, OnDestroy, OnInit, - SecurityContext, Type, ViewChild, ViewContainerRef, } from "@angular/core"; -import { DomSanitizer } from "@angular/platform-browser"; import { Router } from "@angular/router"; -import { IndividualConfig, ToastrService } from "ngx-toastr"; import { firstValueFrom, Subject, takeUntil } from "rxjs"; import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref"; @@ -49,7 +46,7 @@ import { CollectionService } from "@bitwarden/common/vault/abstractions/collecti import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { CipherType } from "@bitwarden/common/vault/enums"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { DeleteAccountComponent } from "../auth/delete-account.component"; import { LoginApprovalComponent } from "../auth/login/login-approval.component"; @@ -129,9 +126,8 @@ export class AppComponent implements OnInit, OnDestroy { private cipherService: CipherService, private authService: AuthService, private router: Router, - private toastrService: ToastrService, + private toastService: ToastService, private i18nService: I18nService, - private sanitizer: DomSanitizer, private ngZone: NgZone, private vaultTimeoutService: VaultTimeoutService, private vaultTimeoutSettingsService: VaultTimeoutSettingsService, @@ -294,7 +290,7 @@ export class AppComponent implements OnInit, OnDestroy { ); break; case "showToast": - this.showToast(message); + this.toastService._showToast(message); break; case "copiedToClipboard": if (!message.clearing) { @@ -674,34 +670,6 @@ export class AppComponent implements OnInit, OnDestroy { }); } - private showToast(msg: any) { - let message = ""; - - const options: Partial<IndividualConfig> = {}; - - if (typeof msg.text === "string") { - message = msg.text; - } else if (msg.text.length === 1) { - message = msg.text[0]; - } else { - msg.text.forEach( - (t: string) => - (message += "<p>" + this.sanitizer.sanitize(SecurityContext.HTML, t) + "</p>"), - ); - options.enableHtml = true; - } - if (msg.options != null) { - if (msg.options.trustedHtml === true) { - options.enableHtml = true; - } - if (msg.options.timeout != null && msg.options.timeout > 0) { - options.timeOut = msg.options.timeout; - } - } - - this.toastrService.show(message, msg.title, options, "toast-" + msg.type); - } - private routeToVault(action: string, cipherType: CipherType) { if (!this.router.url.includes("vault")) { // 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/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 3d2b40ac62..ff9cbc97cc 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/scss/plugins.scss b/apps/desktop/src/scss/plugins.scss deleted file mode 100644 index c156456809..0000000000 --- a/apps/desktop/src/scss/plugins.scss +++ /dev/null @@ -1,95 +0,0 @@ -@import "~ngx-toastr/toastr"; - -@import "variables.scss"; - -.toast-container { - .toast-close-button { - @include themify($themes) { - color: themed("toastTextColor"); - } - font-size: 18px; - margin-right: 4px; - } - - .ngx-toastr { - @include themify($themes) { - color: themed("toastTextColor"); - } - align-items: center; - background-image: none !important; - border-radius: $border-radius; - box-shadow: 0 0 8px rgba(0, 0, 0, 0.35); - display: flex; - padding: 15px; - - .toast-close-button { - position: absolute; - right: 5px; - top: 0; - } - - &:hover { - box-shadow: 0 0 10px rgba(0, 0, 0, 0.6); - } - - .icon i::before { - float: left; - font-style: normal; - font-family: $icomoon-font-family; - font-size: 25px; - line-height: 20px; - padding-right: 15px; - } - - .toast-message { - p { - margin-bottom: 0.5rem; - - &:last-child { - margin-bottom: 0; - } - } - } - - &.toast-danger, - &.toast-error { - @include themify($themes) { - background-color: themed("dangerColor"); - } - - .icon i::before { - content: map_get($icons, "error"); - } - } - - &.toast-warning { - @include themify($themes) { - background-color: themed("warningColor"); - } - - .icon i::before { - content: map_get($icons, "exclamation-triangle"); - } - } - - &.toast-info { - @include themify($themes) { - background-color: themed("infoColor"); - } - - .icon i:before { - content: map_get($icons, "info-circle"); - } - } - - &.toast-success { - @include themify($themes) { - background-color: themed("successColor"); - } - - .icon i:before { - content: map_get($icons, "check"); - } - } - } -} diff --git a/apps/desktop/src/scss/styles.scss b/apps/desktop/src/scss/styles.scss index 033a0f8b67..54c1385dcf 100644 --- a/apps/desktop/src/scss/styles.scss +++ b/apps/desktop/src/scss/styles.scss @@ -11,7 +11,6 @@ @import "buttons.scss"; @import "misc.scss"; @import "modal.scss"; -@import "plugins.scss"; @import "environment.scss"; @import "header.scss"; @import "left-nav.scss"; diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts index 7a3b34969a..1da2d94c15 100644 --- a/apps/web/src/app/app.component.ts +++ b/apps/web/src/app/app.component.ts @@ -1,9 +1,7 @@ import { DOCUMENT } from "@angular/common"; -import { Component, Inject, NgZone, OnDestroy, OnInit, SecurityContext } from "@angular/core"; -import { DomSanitizer } from "@angular/platform-browser"; +import { Component, Inject, NgZone, OnDestroy, OnInit } from "@angular/core"; import { NavigationEnd, Router } from "@angular/router"; import * as jq from "jquery"; -import { IndividualConfig, ToastrService } from "ngx-toastr"; import { Subject, switchMap, takeUntil, timer } from "rxjs"; import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service"; @@ -29,7 +27,7 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { PolicyListService } from "./admin-console/core/policy-list.service"; import { @@ -68,14 +66,13 @@ export class AppComponent implements OnDestroy, OnInit { private cipherService: CipherService, private authService: AuthService, private router: Router, - private toastrService: ToastrService, + private toastService: ToastService, private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, private ngZone: NgZone, private vaultTimeoutService: VaultTimeoutService, private cryptoService: CryptoService, private collectionService: CollectionService, - private sanitizer: DomSanitizer, private searchService: SearchService, private notificationsService: NotificationsService, private stateService: StateService, @@ -209,7 +206,7 @@ export class AppComponent implements OnDestroy, OnInit { break; } case "showToast": - this.showToast(message); + this.toastService._showToast(message); break; case "convertAccountToKeyConnector": // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. @@ -327,34 +324,6 @@ export class AppComponent implements OnDestroy, OnInit { }, IdleTimeout); } - private showToast(msg: any) { - let message = ""; - - const options: Partial<IndividualConfig> = {}; - - if (typeof msg.text === "string") { - message = msg.text; - } else if (msg.text.length === 1) { - message = msg.text[0]; - } else { - msg.text.forEach( - (t: string) => - (message += "<p>" + this.sanitizer.sanitize(SecurityContext.HTML, t) + "</p>"), - ); - options.enableHtml = true; - } - if (msg.options != null) { - if (msg.options.trustedHtml === true) { - options.enableHtml = true; - } - if (msg.options.timeout != null && msg.options.timeout > 0) { - options.timeOut = msg.options.timeout; - } - } - - this.toastrService.show(message, msg.title, options, "toast-" + msg.type); - } - private idleStateChanged() { if (this.isIdle) { // 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/web/src/app/shared/shared.module.ts b/apps/web/src/app/shared/shared.module.ts index bc775f07e2..1b04583a39 100644 --- a/apps/web/src/app/shared/shared.module.ts +++ b/apps/web/src/app/shared/shared.module.ts @@ -4,7 +4,6 @@ import { NgModule } from "@angular/core"; import { FormsModule, ReactiveFormsModule } from "@angular/forms"; import { RouterModule } from "@angular/router"; import { InfiniteScrollModule } from "ngx-infinite-scroll"; -import { ToastrModule } from "ngx-toastr"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { @@ -52,7 +51,6 @@ import "./locales"; ReactiveFormsModule, InfiniteScrollModule, RouterModule, - ToastrModule, JslibModule, // Component library modules @@ -90,7 +88,6 @@ import "./locales"; ReactiveFormsModule, InfiniteScrollModule, RouterModule, - ToastrModule, JslibModule, // Component library diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 7632392c23..f14574508c 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, diff --git a/apps/web/src/scss/styles.scss b/apps/web/src/scss/styles.scss index 98b3512ba5..8fbea200a9 100644 --- a/apps/web/src/scss/styles.scss +++ b/apps/web/src/scss/styles.scss @@ -43,8 +43,6 @@ @import "~bootstrap/scss/_utilities"; @import "~bootstrap/scss/_print"; -@import "~ngx-toastr/toastr"; - @import "./base"; @import "./buttons"; @import "./callouts"; @@ -54,5 +52,4 @@ @import "./pages"; @import "./plugins"; @import "./tables"; -@import "./toasts"; @import "./vault-filters"; diff --git a/apps/web/src/scss/toasts.scss b/apps/web/src/scss/toasts.scss deleted file mode 100644 index 6685de6449..0000000000 --- a/apps/web/src/scss/toasts.scss +++ /dev/null @@ -1,117 +0,0 @@ -.toast-container { - .toast-close-button { - font-size: 18px; - margin-right: 4px; - } - - .ngx-toastr { - align-items: center; - background-image: none !important; - border-radius: $border-radius; - box-shadow: 0 0 8px rgba(0, 0, 0, 0.35); - display: flex; - padding: 15px; - - .toast-close-button { - position: absolute; - right: 5px; - top: 0; - } - - &:hover { - box-shadow: 0 0 10px rgba(0, 0, 0, 0.6); - } - - .icon i::before { - float: left; - font-style: normal; - font-family: $icomoon-font-family; - font-size: 25px; - line-height: 20px; - padding-right: 15px; - } - - .toast-message { - p { - margin-bottom: 0.5rem; - - &:last-child { - margin-bottom: 0; - } - } - } - - &.toast-danger, - &.toast-error { - @include themify($themes) { - background-color: themed("danger"); - } - - &, - &:before, - & .toast-close-button { - @include themify($themes) { - color: themed("textDangerColor") !important; - } - } - - .icon i::before { - content: map_get($icons, "error"); - } - } - - &.toast-warning { - @include themify($themes) { - background-color: themed("warning"); - } - - &, - &:before, - & .toast-close-button { - @include themify($themes) { - color: themed("textWarningColor") !important; - } - } - - .icon i::before { - content: map_get($icons, "exclamation-triangle"); - } - } - - &.toast-info { - @include themify($themes) { - background-color: themed("info"); - } - - &, - &:before, - & .toast-close-button { - @include themify($themes) { - color: themed("textInfoColor") !important; - } - } - - .icon i:before { - content: map_get($icons, "info-circle"); - } - } - - &.toast-success { - @include themify($themes) { - background-color: themed("success"); - } - - &, - &:before, - & .toast-close-button { - @include themify($themes) { - color: themed("textSuccessColor") !important; - } - } - - .icon i:before { - content: map_get($icons, "check"); - } - } - } -} diff --git a/libs/angular/src/components/toastr.component.ts b/libs/angular/src/components/toastr.component.ts deleted file mode 100644 index bfe20ed866..0000000000 --- a/libs/angular/src/components/toastr.component.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { animate, state, style, transition, trigger } from "@angular/animations"; -import { CommonModule } from "@angular/common"; -import { Component, ModuleWithProviders, NgModule } from "@angular/core"; -import { - DefaultNoComponentGlobalConfig, - GlobalConfig, - Toast as BaseToast, - ToastPackage, - ToastrService, - TOAST_CONFIG, -} from "ngx-toastr"; - -@Component({ - selector: "[toast-component2]", - template: ` - <button - *ngIf="options.closeButton" - (click)="remove()" - type="button" - class="toast-close-button" - aria-label="Close" - > - <span aria-hidden="true">&times;</span> - </button> - <div class="icon"> - <i></i> - </div> - <div> - <div *ngIf="title" [class]="options.titleClass" [attr.aria-label]="title"> - {{ title }} <ng-container *ngIf="duplicatesCount">[{{ duplicatesCount + 1 }}]</ng-container> - </div> - <div - *ngIf="message && options.enableHtml" - role="alertdialog" - aria-live="polite" - [class]="options.messageClass" - [innerHTML]="message" - ></div> - <div - *ngIf="message && !options.enableHtml" - role="alertdialog" - aria-live="polite" - [class]="options.messageClass" - [attr.aria-label]="message" - > - {{ message }} - </div> - </div> - <div *ngIf="options.progressBar"> - <div class="toast-progress" [style.width]="width + '%'"></div> - </div> - `, - animations: [ - trigger("flyInOut", [ - state("inactive", style({ opacity: 0 })), - state("active", style({ opacity: 1 })), - state("removed", style({ opacity: 0 })), - transition("inactive => active", animate("{{ easeTime }}ms {{ easing }}")), - transition("active => removed", animate("{{ easeTime }}ms {{ easing }}")), - ]), - ], - preserveWhitespaces: false, -}) -export class BitwardenToast extends BaseToast { - constructor( - protected toastrService: ToastrService, - public toastPackage: ToastPackage, - ) { - super(toastrService, toastPackage); - } -} - -export const BitwardenToastGlobalConfig: GlobalConfig = { - ...DefaultNoComponentGlobalConfig, - toastComponent: BitwardenToast, -}; - -@NgModule({ - imports: [CommonModule], - declarations: [BitwardenToast], - exports: [BitwardenToast], -}) -export class BitwardenToastModule { - static forRoot(config: Partial<GlobalConfig> = {}): ModuleWithProviders<BitwardenToastModule> { - return { - ngModule: BitwardenToastModule, - providers: [ - { - provide: TOAST_CONFIG, - useValue: { - default: BitwardenToastGlobalConfig, - config: config, - }, - }, - ], - }; - } -} diff --git a/libs/angular/src/jslib.module.ts b/libs/angular/src/jslib.module.ts index 64fb44e3b8..5f1bf796aa 100644 --- a/libs/angular/src/jslib.module.ts +++ b/libs/angular/src/jslib.module.ts @@ -2,10 +2,9 @@ import { CommonModule, DatePipe } from "@angular/common"; import { NgModule } from "@angular/core"; import { FormsModule, ReactiveFormsModule } from "@angular/forms"; -import { AutofocusDirective } from "@bitwarden/components"; +import { AutofocusDirective, ToastModule } from "@bitwarden/components"; import { CalloutComponent } from "./components/callout.component"; -import { BitwardenToastModule } from "./components/toastr.component"; import { A11yInvalidDirective } from "./directives/a11y-invalid.directive"; import { A11yTitleDirective } from "./directives/a11y-title.directive"; import { ApiActionDirective } from "./directives/api-action.directive"; @@ -34,7 +33,7 @@ import { IconComponent } from "./vault/components/icon.component"; @NgModule({ imports: [ - BitwardenToastModule.forRoot({ + ToastModule.forRoot({ maxOpened: 5, autoDismiss: true, closeButton: true, @@ -77,7 +76,7 @@ import { IconComponent } from "./vault/components/icon.component"; A11yTitleDirective, ApiActionDirective, AutofocusDirective, - BitwardenToastModule, + ToastModule, BoxRowDirective, CalloutComponent, CopyTextDirective, diff --git a/libs/common/src/platform/abstractions/platform-utils.service.ts b/libs/common/src/platform/abstractions/platform-utils.service.ts index d518a17f7b..f2dff46c78 100644 --- a/libs/common/src/platform/abstractions/platform-utils.service.ts +++ b/libs/common/src/platform/abstractions/platform-utils.service.ts @@ -28,6 +28,11 @@ export abstract class PlatformUtilsService { abstract getApplicationVersionNumber(): Promise<string>; abstract supportsWebAuthn(win: Window): boolean; abstract supportsDuo(): boolean; + /** + * @deprecated use `@bitwarden/components/ToastService.showToast` instead + * + * Jira: [CL-213](https://bitwarden.atlassian.net/browse/CL-213) + */ abstract showToast( type: "error" | "success" | "warning" | "info", title: string, diff --git a/libs/components/src/index.ts b/libs/components/src/index.ts index 139e69ebb6..527d5f3615 100644 --- a/libs/components/src/index.ts +++ b/libs/components/src/index.ts @@ -29,6 +29,7 @@ export * from "./section"; export * from "./select"; export * from "./table"; export * from "./tabs"; +export * from "./toast"; export * from "./toggle-group"; export * from "./typography"; export * from "./utils/i18n-mock.service"; diff --git a/libs/components/src/toast/index.ts b/libs/components/src/toast/index.ts new file mode 100644 index 0000000000..f0b5540219 --- /dev/null +++ b/libs/components/src/toast/index.ts @@ -0,0 +1,2 @@ +export * from "./toast.module"; +export * from "./toast.service"; diff --git a/libs/components/src/toast/toast.component.html b/libs/components/src/toast/toast.component.html new file mode 100644 index 0000000000..f301995d0a --- /dev/null +++ b/libs/components/src/toast/toast.component.html @@ -0,0 +1,24 @@ +<div + class="tw-mb-1 tw-min-w-[--bit-toast-width] tw-text-contrast tw-flex tw-flex-col tw-justify-between tw-rounded-md tw-pointer-events-auto tw-cursor-default {{ + bgColor + }}" +> + <div class="tw-flex tw-items-center tw-gap-4 tw-px-2 tw-pb-1 tw-pt-2"> + <i aria-hidden="true" class="bwi tw-text-xl tw-py-1.5 tw-px-2.5 {{ iconClass }}"></i> + <div> + <span class="tw-sr-only">{{ variant | i18n }}</span> + <p *ngIf="title" data-testid="toast-title" class="tw-font-semibold tw-mb-0">{{ title }}</p> + <p *ngFor="let m of messageArray" data-testid="toast-message" class="tw-mb-2 last:tw-mb-0"> + {{ m }} + </p> + </div> + <button + class="tw-ml-auto" + bitIconButton="bwi-close" + buttonType="contrast" + type="button" + (click)="this.onClose.emit()" + ></button> + </div> + <div class="tw-h-1 tw-w-full tw-bg-text-contrast/70" [style.width]="progressWidth + '%'"></div> +</div> diff --git a/libs/components/src/toast/toast.component.ts b/libs/components/src/toast/toast.component.ts new file mode 100644 index 0000000000..4a31e00586 --- /dev/null +++ b/libs/components/src/toast/toast.component.ts @@ -0,0 +1,66 @@ +import { Component, EventEmitter, Input, Output } from "@angular/core"; + +import { IconButtonModule } from "../icon-button"; +import { SharedModule } from "../shared"; + +export type ToastVariant = "success" | "error" | "info" | "warning"; + +const variants: Record<ToastVariant, { icon: string; bgColor: string }> = { + success: { + icon: "bwi-check", + bgColor: "tw-bg-success-600", + }, + error: { + icon: "bwi-error", + bgColor: "tw-bg-danger-600", + }, + info: { + icon: "bwi-info-circle", + bgColor: "tw-bg-info-600", + }, + warning: { + icon: "bwi-exclamation-triangle", + bgColor: "tw-bg-warning-600", + }, +}; + +@Component({ + selector: "bit-toast", + templateUrl: "toast.component.html", + standalone: true, + imports: [SharedModule, IconButtonModule], +}) +export class ToastComponent { + @Input() variant: ToastVariant = "info"; + + /** + * The message to display + * + * Pass an array to render multiple paragraphs. + **/ + @Input({ required: true }) + message: string | string[]; + + /** An optional title to display over the message. */ + @Input() title: string; + + /** + * The percent width of the progress bar, from 0-100 + **/ + @Input() progressWidth = 0; + + /** Emits when the user presses the close button */ + @Output() onClose = new EventEmitter<void>(); + + protected get iconClass(): string { + return variants[this.variant].icon; + } + + protected get bgColor(): string { + return variants[this.variant].bgColor; + } + + protected get messageArray(): string[] { + return Array.isArray(this.message) ? this.message : [this.message]; + } +} diff --git a/libs/components/src/toast/toast.module.ts b/libs/components/src/toast/toast.module.ts new file mode 100644 index 0000000000..bf39a0be9a --- /dev/null +++ b/libs/components/src/toast/toast.module.ts @@ -0,0 +1,39 @@ +import { CommonModule } from "@angular/common"; +import { ModuleWithProviders, NgModule } from "@angular/core"; +import { DefaultNoComponentGlobalConfig, GlobalConfig, TOAST_CONFIG } from "ngx-toastr"; + +import { ToastComponent } from "./toast.component"; +import { BitwardenToastrComponent } from "./toastr.component"; + +@NgModule({ + imports: [CommonModule, ToastComponent], + declarations: [BitwardenToastrComponent], + exports: [BitwardenToastrComponent], +}) +export class ToastModule { + static forRoot(config: Partial<GlobalConfig> = {}): ModuleWithProviders<ToastModule> { + return { + ngModule: ToastModule, + providers: [ + { + provide: TOAST_CONFIG, + useValue: { + default: BitwardenToastrGlobalConfig, + config: config, + }, + }, + ], + }; + } +} + +export const BitwardenToastrGlobalConfig: GlobalConfig = { + ...DefaultNoComponentGlobalConfig, + toastComponent: BitwardenToastrComponent, + tapToDismiss: false, + timeOut: 5000, + extendedTimeOut: 2000, + maxOpened: 5, + autoDismiss: true, + progressBar: true, +}; diff --git a/libs/components/src/toast/toast.service.ts b/libs/components/src/toast/toast.service.ts new file mode 100644 index 0000000000..8bbff02c41 --- /dev/null +++ b/libs/components/src/toast/toast.service.ts @@ -0,0 +1,57 @@ +import { Injectable } from "@angular/core"; +import { IndividualConfig, ToastrService } from "ngx-toastr"; + +import type { ToastComponent } from "./toast.component"; +import { calculateToastTimeout } from "./utils"; + +export type ToastOptions = { + /** + * The duration the toast will persist in milliseconds + **/ + timeout?: number; +} & Pick<ToastComponent, "message" | "variant" | "title">; + +/** + * Presents toast notifications + **/ +@Injectable({ providedIn: "root" }) +export class ToastService { + constructor(private toastrService: ToastrService) {} + + showToast(options: ToastOptions) { + const toastrConfig: Partial<IndividualConfig> = { + payload: { + message: options.message, + variant: options.variant, + title: options.title, + }, + timeOut: + options.timeout != null && options.timeout > 0 + ? options.timeout + : calculateToastTimeout(options.message), + }; + + this.toastrService.show(null, options.title, toastrConfig); + } + + /** + * @deprecated use `showToast` instead + * + * Converts options object from PlatformUtilsService + **/ + _showToast(options: { + type: "error" | "success" | "warning" | "info"; + title: string; + text: string | string[]; + options?: { + timeout?: number; + }; + }) { + this.showToast({ + message: options.text, + variant: options.type, + title: options.title, + timeout: options.options?.timeout, + }); + } +} diff --git a/libs/components/src/toast/toast.spec.ts b/libs/components/src/toast/toast.spec.ts new file mode 100644 index 0000000000..92d8071dc5 --- /dev/null +++ b/libs/components/src/toast/toast.spec.ts @@ -0,0 +1,16 @@ +import { calculateToastTimeout } from "./utils"; + +describe("Toast default timer", () => { + it("should have a minimum of 5000ms", () => { + expect(calculateToastTimeout("")).toBe(5000); + expect(calculateToastTimeout([""])).toBe(5000); + expect(calculateToastTimeout(" ")).toBe(5000); + }); + + it("should return an extra second for each 120 words", () => { + expect(calculateToastTimeout("foo ".repeat(119))).toBe(5000); + expect(calculateToastTimeout("foo ".repeat(120))).toBe(6000); + expect(calculateToastTimeout("foo ".repeat(240))).toBe(7000); + expect(calculateToastTimeout(["foo ".repeat(120), " \n foo ".repeat(120)])).toBe(7000); + }); +}); diff --git a/libs/components/src/toast/toast.stories.ts b/libs/components/src/toast/toast.stories.ts new file mode 100644 index 0000000000..d209453d85 --- /dev/null +++ b/libs/components/src/toast/toast.stories.ts @@ -0,0 +1,124 @@ +import { CommonModule } from "@angular/common"; +import { Component, Input } from "@angular/core"; +import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; +import { action } from "@storybook/addon-actions"; +import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; + +import { ButtonModule } from "../button"; +import { I18nMockService } from "../utils/i18n-mock.service"; + +import { ToastComponent } from "./toast.component"; +import { BitwardenToastrGlobalConfig, ToastModule } from "./toast.module"; +import { ToastOptions, ToastService } from "./toast.service"; + +const toastServiceExampleTemplate = ` + <button bitButton type="button" (click)="toastService.showToast(toastOptions)">Show Toast</button> +`; +@Component({ + selector: "toast-service-example", + template: toastServiceExampleTemplate, +}) +export class ToastServiceExampleComponent { + @Input() + toastOptions: ToastOptions; + + constructor(protected toastService: ToastService) {} +} + +export default { + title: "Component Library/Toast", + component: ToastComponent, + + decorators: [ + moduleMetadata({ + imports: [CommonModule, BrowserAnimationsModule, ButtonModule], + declarations: [ToastServiceExampleComponent], + }), + applicationConfig({ + providers: [ + ToastModule.forRoot().providers, + { + provide: I18nService, + useFactory: () => { + return new I18nMockService({ + close: "Close", + success: "Success", + error: "Error", + warning: "Warning", + }); + }, + }, + ], + }), + ], + args: { + onClose: action("emit onClose"), + variant: "info", + progressWidth: 50, + title: "", + message: "Hello Bitwarden!", + }, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library", + }, + }, +} as Meta; + +type Story = StoryObj<ToastComponent>; + +export const Default: Story = { + render: (args) => ({ + props: args, + template: ` + <div class="tw-flex tw-flex-col tw-min-w tw-max-w-[--bit-toast-width]"> + <bit-toast [title]="title" [message]="message" [progressWidth]="progressWidth" (onClose)="onClose()" variant="success"></bit-toast> + <bit-toast [title]="title" [message]="message" [progressWidth]="progressWidth" (onClose)="onClose()" variant="info"></bit-toast> + <bit-toast [title]="title" [message]="message" [progressWidth]="progressWidth" (onClose)="onClose()" variant="warning"></bit-toast> + <bit-toast [title]="title" [message]="message" [progressWidth]="progressWidth" (onClose)="onClose()" variant="error"></bit-toast> + </div> + `, + }), +}; + +/** + * Avoid using long messages in toasts. + */ +export const LongContent: Story = { + ...Default, + args: { + title: "Foo", + message: [ + "Lorem ipsum dolor sit amet, consectetur adipisci", + "Lorem ipsum dolor sit amet, consectetur adipisci", + ], + }, +}; + +export const Service: Story = { + render: (args) => ({ + props: { + toastOptions: args, + }, + template: ` + <toast-service-example [toastOptions]="toastOptions"></toast-service-example> + `, + }), + args: { + title: "", + message: "Hello Bitwarden!", + variant: "error", + timeout: BitwardenToastrGlobalConfig.timeOut, + } as ToastOptions, + parameters: { + chromatic: { disableSnapshot: true }, + docs: { + source: { + code: toastServiceExampleTemplate, + }, + }, + }, +}; diff --git a/libs/components/src/toast/toast.tokens.css b/libs/components/src/toast/toast.tokens.css new file mode 100644 index 0000000000..2ff9e99ae5 --- /dev/null +++ b/libs/components/src/toast/toast.tokens.css @@ -0,0 +1,4 @@ +:root { + --bit-toast-width: 19rem; + --bit-toast-width-full: 96%; +} diff --git a/libs/components/src/toast/toastr.component.ts b/libs/components/src/toast/toastr.component.ts new file mode 100644 index 0000000000..70085dfc47 --- /dev/null +++ b/libs/components/src/toast/toastr.component.ts @@ -0,0 +1,26 @@ +import { animate, state, style, transition, trigger } from "@angular/animations"; +import { Component } from "@angular/core"; +import { Toast as BaseToastrComponent } from "ngx-toastr"; + +@Component({ + template: ` + <bit-toast + [title]="options?.payload?.title" + [variant]="options?.payload?.variant" + [message]="options?.payload?.message" + [progressWidth]="width" + (onClose)="remove()" + ></bit-toast> + `, + animations: [ + trigger("flyInOut", [ + state("inactive", style({ opacity: 0 })), + state("active", style({ opacity: 1 })), + state("removed", style({ opacity: 0 })), + transition("inactive => active", animate("{{ easeTime }}ms {{ easing }}")), + transition("active => removed", animate("{{ easeTime }}ms {{ easing }}")), + ]), + ], + preserveWhitespaces: false, +}) +export class BitwardenToastrComponent extends BaseToastrComponent {} diff --git a/libs/components/src/toast/toastr.css b/libs/components/src/toast/toastr.css new file mode 100644 index 0000000000..fabf8caf10 --- /dev/null +++ b/libs/components/src/toast/toastr.css @@ -0,0 +1,23 @@ +@import "~ngx-toastr/toastr"; +@import "./toast.tokens.css"; + +/* Override all default styles from `ngx-toaster` */ +.toast-container .ngx-toastr { + all: unset; + display: block; + width: var(--bit-toast-width); + + /* Needed to make hover states work in Electron, since the toast appears in the draggable region. */ + -webkit-app-region: no-drag; +} + +/* Disable hover styles */ +.toast-container .ngx-toastr:hover { + box-shadow: none; +} + +.toast-container.toast-bottom-full-width .ngx-toastr { + width: var(--bit-toast-width-full); + margin-left: auto; + margin-right: auto; +} diff --git a/libs/components/src/toast/utils.ts b/libs/components/src/toast/utils.ts new file mode 100644 index 0000000000..4c8323f396 --- /dev/null +++ b/libs/components/src/toast/utils.ts @@ -0,0 +1,14 @@ +/** + * Given a toast message, calculate the ideal timeout length following: + * a minimum of 5 seconds + 1 extra second per 120 additional words + * + * @param message the toast message to be displayed + * @returns the timeout length in milliseconds + */ +export const calculateToastTimeout = (message: string | string[]): number => { + const paragraphs = Array.isArray(message) ? message : [message]; + const numWords = paragraphs + .map((paragraph) => paragraph.split(/\s+/).filter((word) => word !== "")) + .flat().length; + return 5000 + Math.floor(numWords / 120) * 1000; +}; diff --git a/libs/components/src/tw-theme.css b/libs/components/src/tw-theme.css index 0087af28ae..72e8e1e5e8 100644 --- a/libs/components/src/tw-theme.css +++ b/libs/components/src/tw-theme.css @@ -171,6 +171,9 @@ @import "./popover/popover.component.css"; @import "./search/search.component.css"; +@import "./toast/toast.tokens.css"; +@import "./toast/toastr.css"; + /** * tw-break-words does not work with table cells: * https://github.com/tailwindlabs/tailwindcss/issues/835 From ce75f7b5659aeafe036edf4e27ee8ba1c7608913 Mon Sep 17 00:00:00 2001 From: Matt Gibson <mgibson@bitwarden.com> Date: Thu, 18 Apr 2024 14:06:31 -0400 Subject: [PATCH 220/351] Vault/pm-7580/resolve-cipher-update-race (#8806) * Resolve updated values from updates Uses the now returned updated values from cipher service to guarantee-return the updated cipher for CLI edits * Use updated cipher for creation * Use updated cipher for editing collections * Await async methods Cipher data more closely approximates server responses. TODO: this should really use actual response types --- apps/cli/src/commands/edit.command.ts | 6 +-- apps/cli/src/vault/create.command.ts | 6 +-- .../src/vault/abstractions/cipher.service.ts | 37 ++++++++++++++-- .../src/vault/services/cipher.service.spec.ts | 43 +++++++------------ .../src/vault/services/cipher.service.ts | 31 ++++++++----- 5 files changed, 73 insertions(+), 50 deletions(-) diff --git a/apps/cli/src/commands/edit.command.ts b/apps/cli/src/commands/edit.command.ts index 3d4f9529ad..e64ff8b551 100644 --- a/apps/cli/src/commands/edit.command.ts +++ b/apps/cli/src/commands/edit.command.ts @@ -86,8 +86,7 @@ export class EditCommand { cipherView = CipherExport.toView(req, cipherView); const encCipher = await this.cipherService.encrypt(cipherView); try { - await this.cipherService.updateWithServer(encCipher); - const updatedCipher = await this.cipherService.get(cipher.id); + const updatedCipher = await this.cipherService.updateWithServer(encCipher); const decCipher = await updatedCipher.decrypt( await this.cipherService.getKeyForCipherKeyDecryption(updatedCipher), ); @@ -111,8 +110,7 @@ export class EditCommand { cipher.collectionIds = req; try { - await this.cipherService.saveCollectionsWithServer(cipher); - const updatedCipher = await this.cipherService.get(cipher.id); + const updatedCipher = await this.cipherService.saveCollectionsWithServer(cipher); const decCipher = await updatedCipher.decrypt( await this.cipherService.getKeyForCipherKeyDecryption(updatedCipher), ); diff --git a/apps/cli/src/vault/create.command.ts b/apps/cli/src/vault/create.command.ts index b813227109..78ee04e73c 100644 --- a/apps/cli/src/vault/create.command.ts +++ b/apps/cli/src/vault/create.command.ts @@ -80,8 +80,7 @@ export class CreateCommand { private async createCipher(req: CipherExport) { const cipher = await this.cipherService.encrypt(CipherExport.toView(req)); try { - await this.cipherService.createWithServer(cipher); - const newCipher = await this.cipherService.get(cipher.id); + const newCipher = await this.cipherService.createWithServer(cipher); const decCipher = await newCipher.decrypt( await this.cipherService.getKeyForCipherKeyDecryption(newCipher), ); @@ -142,12 +141,11 @@ export class CreateCommand { } try { - await this.cipherService.saveAttachmentRawWithServer( + const updatedCipher = await this.cipherService.saveAttachmentRawWithServer( cipher, fileName, new Uint8Array(fileBuf).buffer, ); - const updatedCipher = await this.cipherService.get(cipher.id); const decCipher = await updatedCipher.decrypt( await this.cipherService.getKeyForCipherKeyDecryption(updatedCipher), ); diff --git a/libs/common/src/vault/abstractions/cipher.service.ts b/libs/common/src/vault/abstractions/cipher.service.ts index 501fd87665..22e2c54a59 100644 --- a/libs/common/src/vault/abstractions/cipher.service.ts +++ b/libs/common/src/vault/abstractions/cipher.service.ts @@ -47,8 +47,24 @@ export abstract class CipherService { updateLastUsedDate: (id: string) => Promise<void>; updateLastLaunchedDate: (id: string) => Promise<void>; saveNeverDomain: (domain: string) => Promise<void>; - createWithServer: (cipher: Cipher, orgAdmin?: boolean) => Promise<any>; - updateWithServer: (cipher: Cipher, orgAdmin?: boolean, isNotClone?: boolean) => Promise<any>; + /** + * Create a cipher with the server + * + * @param cipher The cipher to create + * @param orgAdmin If true, the request is submitted as an organization admin request + * + * @returns A promise that resolves to the created cipher + */ + createWithServer: (cipher: Cipher, orgAdmin?: boolean) => Promise<Cipher>; + /** + * Update a cipher with the server + * @param cipher The cipher to update + * @param orgAdmin If true, the request is submitted as an organization admin request + * @param isNotClone If true, the cipher is not a clone and should be treated as a new cipher + * + * @returns A promise that resolves to the updated cipher + */ + updateWithServer: (cipher: Cipher, orgAdmin?: boolean, isNotClone?: boolean) => Promise<Cipher>; shareWithServer: ( cipher: CipherView, organizationId: string, @@ -70,7 +86,14 @@ export abstract class CipherService { data: ArrayBuffer, admin?: boolean, ) => Promise<Cipher>; - saveCollectionsWithServer: (cipher: Cipher) => Promise<any>; + /** + * Save the collections for a cipher with the server + * + * @param cipher The cipher to save collections for + * + * @returns A promise that resolves when the collections have been saved + */ + saveCollectionsWithServer: (cipher: Cipher) => Promise<Cipher>; /** * Bulk update collections for many ciphers with the server * @param orgId @@ -84,7 +107,13 @@ export abstract class CipherService { collectionIds: CollectionId[], removeCollections: boolean, ) => Promise<void>; - upsert: (cipher: CipherData | CipherData[]) => Promise<any>; + /** + * Update the local store of CipherData with the provided data. Values are upserted into the existing store. + * + * @param cipher The cipher data to upsert. Can be a single CipherData object or an array of CipherData objects. + * @returns A promise that resolves to a record of updated cipher store, keyed by their cipher ID. Returns all ciphers, not just those updated + */ + upsert: (cipher: CipherData | CipherData[]) => Promise<Record<CipherId, CipherData>>; replace: (ciphers: { [id: string]: CipherData }) => Promise<any>; clear: (userId: string) => Promise<any>; moveManyWithServer: (ids: string[], folderId: string) => Promise<any>; diff --git a/libs/common/src/vault/services/cipher.service.spec.ts b/libs/common/src/vault/services/cipher.service.spec.ts index 28c4bfc653..9b03753118 100644 --- a/libs/common/src/vault/services/cipher.service.spec.ts +++ b/libs/common/src/vault/services/cipher.service.spec.ts @@ -174,23 +174,20 @@ describe("Cipher Service", () => { it("should call apiService.postCipherAdmin when orgAdmin param is true and the cipher orgId != null", async () => { const spy = jest .spyOn(apiService, "postCipherAdmin") - .mockImplementation(() => Promise.resolve<any>(cipherObj)); - // 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 - cipherService.createWithServer(cipherObj, true); + .mockImplementation(() => Promise.resolve<any>(cipherObj.toCipherData())); + await cipherService.createWithServer(cipherObj, true); const expectedObj = new CipherCreateRequest(cipherObj); expect(spy).toHaveBeenCalled(); expect(spy).toHaveBeenCalledWith(expectedObj); }); + it("should call apiService.postCipher when orgAdmin param is true and the cipher orgId is null", async () => { cipherObj.organizationId = null; const spy = jest .spyOn(apiService, "postCipher") - .mockImplementation(() => Promise.resolve<any>(cipherObj)); - // 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 - cipherService.createWithServer(cipherObj, true); + .mockImplementation(() => Promise.resolve<any>(cipherObj.toCipherData())); + await cipherService.createWithServer(cipherObj, true); const expectedObj = new CipherRequest(cipherObj); expect(spy).toHaveBeenCalled(); @@ -201,10 +198,8 @@ describe("Cipher Service", () => { cipherObj.collectionIds = ["123"]; const spy = jest .spyOn(apiService, "postCipherCreate") - .mockImplementation(() => Promise.resolve<any>(cipherObj)); - // 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 - cipherService.createWithServer(cipherObj); + .mockImplementation(() => Promise.resolve<any>(cipherObj.toCipherData())); + await cipherService.createWithServer(cipherObj); const expectedObj = new CipherCreateRequest(cipherObj); expect(spy).toHaveBeenCalled(); @@ -214,10 +209,8 @@ describe("Cipher Service", () => { it("should call apiService.postCipher when orgAdmin and collectionIds logic is false", async () => { const spy = jest .spyOn(apiService, "postCipher") - .mockImplementation(() => Promise.resolve<any>(cipherObj)); - // 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 - cipherService.createWithServer(cipherObj); + .mockImplementation(() => Promise.resolve<any>(cipherObj.toCipherData())); + await cipherService.createWithServer(cipherObj); const expectedObj = new CipherRequest(cipherObj); expect(spy).toHaveBeenCalled(); @@ -229,10 +222,8 @@ describe("Cipher Service", () => { it("should call apiService.putCipherAdmin when orgAdmin and isNotClone params are true", async () => { const spy = jest .spyOn(apiService, "putCipherAdmin") - .mockImplementation(() => Promise.resolve<any>(cipherObj)); - // 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 - cipherService.updateWithServer(cipherObj, true, true); + .mockImplementation(() => Promise.resolve<any>(cipherObj.toCipherData())); + await cipherService.updateWithServer(cipherObj, true, true); const expectedObj = new CipherRequest(cipherObj); expect(spy).toHaveBeenCalled(); @@ -243,10 +234,8 @@ describe("Cipher Service", () => { cipherObj.edit = true; const spy = jest .spyOn(apiService, "putCipher") - .mockImplementation(() => Promise.resolve<any>(cipherObj)); - // 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 - cipherService.updateWithServer(cipherObj); + .mockImplementation(() => Promise.resolve<any>(cipherObj.toCipherData())); + await cipherService.updateWithServer(cipherObj); const expectedObj = new CipherRequest(cipherObj); expect(spy).toHaveBeenCalled(); @@ -257,10 +246,8 @@ describe("Cipher Service", () => { cipherObj.edit = false; const spy = jest .spyOn(apiService, "putPartialCipher") - .mockImplementation(() => Promise.resolve<any>(cipherObj)); - // 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 - cipherService.updateWithServer(cipherObj); + .mockImplementation(() => Promise.resolve<any>(cipherObj.toCipherData())); + await cipherService.updateWithServer(cipherObj); const expectedObj = new CipherPartialRequest(cipherObj); expect(spy).toHaveBeenCalled(); diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 7d06b3185f..4a13196c9c 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -573,7 +573,7 @@ export class CipherService implements CipherServiceAbstraction { await this.domainSettingsService.setNeverDomains(domains); } - async createWithServer(cipher: Cipher, orgAdmin?: boolean): Promise<any> { + async createWithServer(cipher: Cipher, orgAdmin?: boolean): Promise<Cipher> { let response: CipherResponse; if (orgAdmin && cipher.organizationId != null) { const request = new CipherCreateRequest(cipher); @@ -588,10 +588,16 @@ export class CipherService implements CipherServiceAbstraction { cipher.id = response.id; const data = new CipherData(response, cipher.collectionIds); - await this.upsert(data); + const updated = await this.upsert(data); + // No local data for new ciphers + return new Cipher(updated[cipher.id as CipherId]); } - async updateWithServer(cipher: Cipher, orgAdmin?: boolean, isNotClone?: boolean): Promise<any> { + async updateWithServer( + cipher: Cipher, + orgAdmin?: boolean, + isNotClone?: boolean, + ): Promise<Cipher> { let response: CipherResponse; if (orgAdmin && isNotClone) { const request = new CipherRequest(cipher); @@ -605,7 +611,9 @@ export class CipherService implements CipherServiceAbstraction { } const data = new CipherData(response, cipher.collectionIds); - await this.upsert(data); + const updated = await this.upsert(data); + // updating with server does not change local data + return new Cipher(updated[cipher.id as CipherId], cipher.localData); } async shareWithServer( @@ -732,11 +740,13 @@ export class CipherService implements CipherServiceAbstraction { return new Cipher(cData); } - async saveCollectionsWithServer(cipher: Cipher): Promise<any> { + async saveCollectionsWithServer(cipher: Cipher): Promise<Cipher> { const request = new CipherCollectionsRequest(cipher.collectionIds); const response = await this.apiService.putCipherCollections(cipher.id, request); const data = new CipherData(response); - await this.upsert(data); + const updated = await this.upsert(data); + // Collection updates don't change local data + return new Cipher(updated[cipher.id as CipherId], cipher.localData); } /** @@ -782,9 +792,9 @@ export class CipherService implements CipherServiceAbstraction { await this.encryptedCiphersState.update(() => ciphers); } - async upsert(cipher: CipherData | CipherData[]): Promise<any> { + async upsert(cipher: CipherData | CipherData[]): Promise<Record<CipherId, CipherData>> { const ciphers = cipher instanceof CipherData ? [cipher] : cipher; - await this.updateEncryptedCipherState((current) => { + return await this.updateEncryptedCipherState((current) => { ciphers.forEach((c) => (current[c.id as CipherId] = c)); return current; }); @@ -796,12 +806,13 @@ export class CipherService implements CipherServiceAbstraction { private async updateEncryptedCipherState( update: (current: Record<CipherId, CipherData>) => Record<CipherId, CipherData>, - ) { + ): Promise<Record<CipherId, CipherData>> { await this.clearDecryptedCiphersState(); - await this.encryptedCiphersState.update((current) => { + const [, updatedCiphers] = await this.encryptedCiphersState.update((current) => { const result = update(current ?? {}); return result; }); + return updatedCiphers; } async clear(userId?: string): Promise<any> { From 40ba15c07e1a5f719727c62bad2dbc5a4b272101 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Thu, 18 Apr 2024 14:24:16 -0500 Subject: [PATCH 221/351] [SM-956] Secret Manager: Integrations Page (#8701) * add navigation item for integrations and SDKs page * Initial routing to Integrations & SDKs page * Initial add of integrations component * Initial add of SDKs component * add secret manage integration images * remove integration & sdk components in favor of a single component * add integration & integration grid components * add integrations & sdks * rename page & components to integration after design discussion * add external rel attribute for SDK links * remove ts extension * refactor: use pseudo element to cover as a link * refactor: change secondaryText to linkText to align with usage * update icon for integrations * add new badge option for integration cards * hardcode integration/sdk names * add dark mode images for integrations and sdks * update integration/sdk card with dark mode image when applicable * refactor integration types to be an enum * fix enum typings in integration grid test --------- Co-authored-by: Robyn MacCallum <robyntmaccallum@gmail.com> --- .../secrets-manager/integrations/ansible.svg | 4 + .../integrations/github-white.svg | 10 + .../secrets-manager/integrations/github.svg | 3 + .../integrations/gitlab-white.svg | 3 + .../secrets-manager/integrations/gitlab.svg | 6 + .../secrets-manager/sdks/c-plus-plus.png | Bin 0 -> 12445 bytes .../images/secrets-manager/sdks/c-sharp.svg | 7 + .../src/images/secrets-manager/sdks/go.svg | 7 + .../secrets-manager/sdks/java-white.svg | 15 ++ .../src/images/secrets-manager/sdks/java.svg | 15 ++ .../src/images/secrets-manager/sdks/php.svg | 4 + .../images/secrets-manager/sdks/python.svg | 19 ++ .../src/images/secrets-manager/sdks/ruby.png | Bin 0 -> 19253 bytes .../src/images/secrets-manager/sdks/wasm.svg | 11 ++ apps/web/src/locales/en/messages.json | 52 ++++++ .../integration-card.component.html | 29 +++ .../integration-card.component.spec.ts | 174 ++++++++++++++++++ .../integration-card.component.ts | 93 ++++++++++ .../integration-grid.component.html | 15 ++ .../integration-grid.component.spec.ts | 81 ++++++++ .../integration-grid.component.ts | 15 ++ .../integrations-routing.module.ts | 17 ++ .../integrations/integrations.component.html | 16 ++ .../integrations.component.spec.ts | 77 ++++++++ .../integrations/integrations.component.ts | 113 ++++++++++++ .../integrations/integrations.module.ts | 15 ++ .../integrations/models/integration.ts | 21 +++ .../layout/navigation.component.html | 6 + .../app/secrets-manager/sm-routing.module.ts | 8 + libs/common/src/enums/index.ts | 1 + .../common/src/enums/integration-type.enum.ts | 4 + 31 files changed, 841 insertions(+) create mode 100644 apps/web/src/images/secrets-manager/integrations/ansible.svg create mode 100644 apps/web/src/images/secrets-manager/integrations/github-white.svg create mode 100644 apps/web/src/images/secrets-manager/integrations/github.svg create mode 100644 apps/web/src/images/secrets-manager/integrations/gitlab-white.svg create mode 100644 apps/web/src/images/secrets-manager/integrations/gitlab.svg create mode 100644 apps/web/src/images/secrets-manager/sdks/c-plus-plus.png create mode 100644 apps/web/src/images/secrets-manager/sdks/c-sharp.svg create mode 100644 apps/web/src/images/secrets-manager/sdks/go.svg create mode 100644 apps/web/src/images/secrets-manager/sdks/java-white.svg create mode 100644 apps/web/src/images/secrets-manager/sdks/java.svg create mode 100644 apps/web/src/images/secrets-manager/sdks/php.svg create mode 100644 apps/web/src/images/secrets-manager/sdks/python.svg create mode 100644 apps/web/src/images/secrets-manager/sdks/ruby.png create mode 100644 apps/web/src/images/secrets-manager/sdks/wasm.svg create mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-card/integration-card.component.html create mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-card/integration-card.component.spec.ts create mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-card/integration-card.component.ts create mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-grid/integration-grid.component.html create mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-grid/integration-grid.component.spec.ts create mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-grid/integration-grid.component.ts create mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations-routing.module.ts create mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.html create mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.spec.ts create mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.ts create mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.module.ts create mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/integrations/models/integration.ts create mode 100644 libs/common/src/enums/integration-type.enum.ts diff --git a/apps/web/src/images/secrets-manager/integrations/ansible.svg b/apps/web/src/images/secrets-manager/integrations/ansible.svg new file mode 100644 index 0000000000..7a32617ab2 --- /dev/null +++ b/apps/web/src/images/secrets-manager/integrations/ansible.svg @@ -0,0 +1,4 @@ +<svg width="112" height="111" viewBox="0 0 112 111" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path d="M96.7901 56.5804C96.7901 64.3954 94.4726 72.0349 90.1309 78.5328C85.7891 85.0308 79.6179 90.0953 72.3978 93.086C65.1777 96.0767 57.2329 96.8592 49.568 95.3345C41.9032 93.8099 34.8626 90.0466 29.3365 84.5206C23.8105 78.9945 20.0472 71.9539 18.5226 64.2891C16.9979 56.6242 17.7804 48.6794 20.7711 41.4593C23.7618 34.2391 28.8263 28.068 35.3242 23.7262C41.8222 19.3844 49.4617 17.067 57.2767 17.067C62.4657 17.0667 67.604 18.0886 72.3981 20.0742C77.1922 22.0599 81.5483 24.9704 85.2175 28.6396C88.8867 32.3088 91.7972 36.6648 93.7829 41.4589C95.7685 46.253 96.7903 51.3913 96.7901 56.5804Z" fill="#FF5850"/> + <path d="M57.9424 41.7704L68.1686 67.0076L52.7229 54.8419L57.9424 41.7704ZM76.1078 72.8215L60.379 34.9703C60.1981 34.4723 59.8658 34.0436 59.4288 33.744C58.9919 33.4445 58.4721 33.2893 57.9424 33.3002C57.4042 33.288 56.8752 33.4417 56.4274 33.7405C55.9795 34.0392 55.6345 34.4686 55.4391 34.9703L38.1761 76.4888H44.0818L50.9147 59.3696L71.3083 75.8457C72.1292 76.5092 72.7202 76.8092 73.4901 76.8092C73.8662 76.8181 74.2403 76.7516 74.5903 76.6137C74.9404 76.4757 75.2593 76.2692 75.5283 76.0062C75.7974 75.7432 76.0111 75.429 76.1568 75.0822C76.3026 74.7353 76.3776 74.3628 76.3772 73.9866C76.3505 73.5863 76.2595 73.1929 76.1078 72.8215Z" fill="white"/> +</svg> diff --git a/apps/web/src/images/secrets-manager/integrations/github-white.svg b/apps/web/src/images/secrets-manager/integrations/github-white.svg new file mode 100644 index 0000000000..030c7c6723 --- /dev/null +++ b/apps/web/src/images/secrets-manager/integrations/github-white.svg @@ -0,0 +1,10 @@ +<svg width="115" height="111" viewBox="0 0 115 111" fill="none" xmlns="http://www.w3.org/2000/svg"> + <g clip-path="url(#clip0_212_1335)"> + <path fill-rule="evenodd" clip-rule="evenodd" d="M57.3312 0.0012207C26.0958 0.0012207 0.845001 25.4382 0.845001 56.9071C0.845001 82.0619 17.024 103.355 39.4687 110.891C42.2748 111.458 43.3027 109.667 43.3027 108.16C43.3027 106.841 43.2102 102.319 43.2102 97.6074C27.4971 101 24.225 90.8238 24.225 90.8238C21.6998 84.2287 17.9583 82.5337 17.9583 82.5337C12.8154 79.0477 18.3329 79.0477 18.3329 79.0477C24.0377 79.4246 27.0312 84.8889 27.0312 84.8889C32.0804 93.556 40.2168 91.1071 43.49 89.5994C43.9571 85.9249 45.4545 83.3812 47.0443 81.9683C34.5119 80.649 21.3264 75.7501 21.3264 53.8917C21.3264 47.6735 23.5694 42.5861 27.1237 38.6295C26.5629 37.2166 24.5985 31.3742 27.6856 23.5547C27.6856 23.5547 32.455 22.047 43.2091 29.3959C47.8133 28.1503 52.5615 27.5166 57.3312 27.5113C62.1006 27.5113 66.9625 28.1715 71.4522 29.3959C82.2074 22.047 86.9768 23.5547 86.9768 23.5547C90.0639 31.3742 88.0983 37.2166 87.5376 38.6295C91.1854 42.5861 93.336 47.6735 93.336 53.8917C93.336 75.7501 80.1504 80.5542 67.5245 81.9683C69.5825 83.7581 71.3585 87.1493 71.3585 92.52C71.3585 100.151 71.266 106.276 71.266 108.159C71.266 109.667 72.2951 111.458 75.1001 110.892C97.5447 103.354 113.724 82.0619 113.724 56.9071C113.816 25.4382 88.4729 0.0012207 57.3312 0.0012207Z" fill="white"/> + </g> + <defs> + <clipPath id="clip0_212_1335"> + <rect width="113.31" height="110.998" fill="white" transform="translate(0.845001 0.0012207)"/> + </clipPath> + </defs> +</svg> diff --git a/apps/web/src/images/secrets-manager/integrations/github.svg b/apps/web/src/images/secrets-manager/integrations/github.svg new file mode 100644 index 0000000000..99e3ffbbe2 --- /dev/null +++ b/apps/web/src/images/secrets-manager/integrations/github.svg @@ -0,0 +1,3 @@ +<svg width="114" height="111" viewBox="0 0 114 111" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path fill-rule="evenodd" clip-rule="evenodd" d="M57.3312 0C26.0951 0 0.84375 25.4375 0.84375 56.9072C0.84375 82.0625 17.0232 103.356 39.4683 110.892C42.2745 111.459 43.3024 109.668 43.3024 108.161C43.3024 106.842 43.2099 102.32 43.2099 97.6083C27.4965 101.001 24.2243 90.8246 24.2243 90.8246C21.699 84.2293 17.9574 82.5343 17.9574 82.5343C12.8144 79.0482 18.332 79.0482 18.332 79.0482C24.037 79.4251 27.0305 84.8895 27.0305 84.8895C32.0798 93.5568 40.2164 91.1079 43.4897 89.6001C43.9568 85.9256 45.4542 83.3818 47.044 81.9689C34.5114 80.6496 21.3256 75.7506 21.3256 53.8917C21.3256 47.6733 23.5687 42.5858 27.123 38.6292C26.5622 37.2162 24.5978 31.3737 27.6849 23.554C27.6849 23.554 32.4545 22.0462 43.2087 29.3953C47.813 28.1497 52.5614 27.516 57.3312 27.5107C62.1007 27.5107 66.9628 28.1709 71.4525 29.3953C82.2079 22.0462 86.9774 23.554 86.9774 23.554C90.0646 31.3737 88.099 37.2162 87.5382 38.6292C91.1862 42.5858 93.3368 47.6733 93.3368 53.8917C93.3368 75.7506 80.1509 80.5548 67.5247 81.9689C69.5828 83.7587 71.3588 87.15 71.3588 92.5208C71.3588 100.152 71.2663 106.277 71.2663 108.16C71.2663 109.668 72.2954 111.459 75.1004 110.894C97.5456 103.355 113.725 82.0625 113.725 56.9072C113.817 25.4375 88.4736 0 57.3312 0Z" fill="#24292F"/> +</svg> diff --git a/apps/web/src/images/secrets-manager/integrations/gitlab-white.svg b/apps/web/src/images/secrets-manager/integrations/gitlab-white.svg new file mode 100644 index 0000000000..7f7bf006ee --- /dev/null +++ b/apps/web/src/images/secrets-manager/integrations/gitlab-white.svg @@ -0,0 +1,3 @@ +<svg width="57" height="55" viewBox="0 0 57 55" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path d="M55.9495 21.8712L55.8706 21.6696L48.235 1.7422C48.0796 1.35164 47.8045 1.02032 47.4492 0.795784C47.0937 0.575065 46.6789 0.46876 46.261 0.491221C45.8431 0.513683 45.4422 0.66383 45.1124 0.92139C44.7862 1.18634 44.5495 1.54537 44.4347 1.9496L39.279 17.7233H18.4023L13.2466 1.9496C13.1348 1.54316 12.8976 1.18234 12.5689 0.918468C12.2391 0.660908 11.8381 0.510761 11.4202 0.4883C11.0024 0.465838 10.5876 0.572144 10.2321 0.792863C9.87754 1.0183 9.6027 1.34933 9.44631 1.73928L1.79608 21.6579L1.72013 21.8595C0.620944 24.7315 0.485268 27.883 1.33356 30.8388C2.18184 33.7947 3.96811 36.3946 6.42302 38.2466L6.44931 38.267L6.51942 38.3167L18.151 47.0273L23.9055 51.3826L27.4108 54.029C27.8208 54.3404 28.3215 54.5089 28.8363 54.5089C29.3511 54.5089 29.8517 54.3404 30.2617 54.029L33.767 51.3826L39.5215 47.0273L51.2232 38.2641L51.2524 38.2408C53.7018 36.3884 55.4839 33.7912 56.3309 30.8393C57.1779 27.8875 57.0441 24.7404 55.9495 21.8712Z" fill="white"/> +</svg> diff --git a/apps/web/src/images/secrets-manager/integrations/gitlab.svg b/apps/web/src/images/secrets-manager/integrations/gitlab.svg new file mode 100644 index 0000000000..9bbc28c37a --- /dev/null +++ b/apps/web/src/images/secrets-manager/integrations/gitlab.svg @@ -0,0 +1,6 @@ +<svg width="112" height="111" viewBox="0 0 112 111" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path d="M82.9495 49.8712L82.8706 49.6696L75.235 29.7422C75.0796 29.3516 74.8045 29.0203 74.4492 28.7958C74.0937 28.5751 73.6789 28.4688 73.261 28.4912C72.8431 28.5137 72.4422 28.6638 72.1124 28.9214C71.7862 29.1863 71.5495 29.5454 71.4347 29.9496L66.279 45.7233H45.4023L40.2466 29.9496C40.1348 29.5432 39.8976 29.1823 39.5689 28.9185C39.2391 28.6609 38.8381 28.5108 38.4202 28.4883C38.0024 28.4658 37.5876 28.5721 37.2321 28.7929C36.8775 29.0183 36.6027 29.3493 36.4463 29.7393L28.7961 49.6579L28.7201 49.8595C27.6209 52.7315 27.4853 55.883 28.3336 58.8388C29.1818 61.7947 30.9681 64.3946 33.423 66.2466L33.4493 66.267L33.5194 66.3167L45.151 75.0273L50.9055 79.3826L54.4108 82.029C54.8208 82.3404 55.3215 82.5089 55.8363 82.5089C56.3511 82.5089 56.8517 82.3404 57.2617 82.029L60.767 79.3826L66.5215 75.0273L78.2232 66.2641L78.2524 66.2408C80.7018 64.3884 82.4839 61.7912 83.3309 58.8393C84.1779 55.8875 84.0441 52.7404 82.9495 49.8712Z" fill="#E24329"/> + <path d="M82.9495 49.8712L82.8706 49.6697C79.15 50.4334 75.6441 52.0093 72.6031 54.2849L55.8333 66.9652C61.544 71.2855 66.5156 75.039 66.5156 75.039L78.2174 66.2759L78.2466 66.2525C80.6995 64.4002 82.4844 61.8013 83.3325 58.8469C84.1807 55.8925 84.0463 52.7426 82.9495 49.8712Z" fill="#FC6D26"/> + <path d="M45.151 75.039L50.9055 79.3943L54.4108 82.0408C54.8208 82.3521 55.3215 82.5206 55.8363 82.5206C56.3511 82.5206 56.8517 82.3521 57.2617 82.0408L60.767 79.3943L66.5215 75.039C66.5215 75.039 61.544 71.2738 55.8333 66.9652C50.1227 71.2738 45.151 75.039 45.151 75.039Z" fill="#FCA326"/> + <path d="M39.0607 54.2849C36.0222 52.0046 32.5169 50.4245 28.7961 49.658L28.7201 49.8595C27.6209 52.7315 27.4853 55.883 28.3336 58.8388C29.1818 61.7947 30.9681 64.3946 33.423 66.2466L33.4493 66.2671L33.5194 66.3167L45.151 75.0273C45.151 75.0273 50.1168 71.2738 55.8333 66.9535L39.0607 54.2849Z" fill="#FC6D26"/> +</svg> diff --git a/apps/web/src/images/secrets-manager/sdks/c-plus-plus.png b/apps/web/src/images/secrets-manager/sdks/c-plus-plus.png new file mode 100644 index 0000000000000000000000000000000000000000..bac17e2ae270435654cf2c206938ce51ae4b97a9 GIT binary patch literal 12445 zcmV;OFk;V%P)<h;3K|Lk000e1NJLTq007<q007<y1^@s6QfI!m00048X+uL$Nkc;* zP;zf(X>4Tx04R}lkijd1aTv!xV_I0DmMCclDK%QNgX@em?XYHRxai%x88!3k-6S{p z1Dqr`H@P_PB5~lX6mmiiJ5#%`UeCK>8+q#K`{~>BJiq7nK#NmTDr*PUas?wgGwGgR zSadr|*lEX!i+)Ydt3V_YmJ+_TIm)-#EWH`EX2nkW#&@osjo{_$%i&Ou`di-=9jSC) z7yXd*>&hsA%_|()EGUb&g<Xa$CA?>GrpCt>Cu@9Kt%*g0!q>vx`ihnkeiZg38~jWI zuVpv%uN|!Io#|YBPP9*QfG{(-iHpXF5+Ff{DW?DF@mG6-tfSb%V3iCj6l6j`97UG2 zaEKf_V|avpcqRSTt$*LJ?x|jwIFP5F>fX{l6bGSex9$y><U6x)ai%mw`?;*GY|k%` z`)b@-gtmRtd`p011S)r?`Cc^5$`Q0az<K!-yUt|pga<790000uWmrjOO-%qQ00008 z00D<-00aO40096102%-Q00002paK8{000010002qpaTE|000010002q00000=T=kr z001tyNkl<ZcmeHw2b>f|_W$e28)3;J5|*4%Ks-bgNuq~}3g?|aImy98j0b|kk~uM) z{0+c@qH>~#hi3xGNRGQ~1U9od$H}4l|7u~*Wtmwf^vrbkRPAR!GgDphTlMWquij&T zIzc1AaU3&mTgq^jvpobHGlT<fATUfT;2`vR!8s!UQ^s&y9LI183M;e!)A<9>2)a_V zaKKOugo(hkjd88zYWaMb45lLYqTMMa!!dab!+mUHnC)9<^)GQJE)DA+1Ss+RgR{%I z#BiLfAU1gcj%+d5IL%#-=pZ1+K{3c=t81F2ZrU|vip@a~>J=aaDDeUWyrXPqZ%!Bn zjBIoLbkb1(5B(Tm(rqmJ(zdw+fAug48jWWJDDij()RW}qd=}S-wJBEMy6qWH;=39L z3HS}mm{)F{HTbftacDT-AV7)d8(dvV_2tjwo9ir!zXNa!F~W__5|^TS6G+Q3@R7<a z|L~I+`xbkX5RL0O0+e{3W9M<q*|RSwVX*QqoQzqEqg;EB6Y?S=2h24b0oTON?z<It zML92$qES6SfD+FG(7cH<YfI8_+;Loo8y_RRNr$hv3<L2TXPXl{`|k66rAgDdJpz<? zx5tQ=5Yzu(Tzk1j@d1u-k7I%wUJ~Ui5e`@+;C7i(xoXq2-Z^|FOVe?V041JtY<PLH zU`KkS$!M6%v9JOY6c*ubtziv%ZMst#5fd}53m(i<=PLv#@qC1AObjQ>ZJ+RroRcj_ zF@-)p%Em{@GS_h3_gd`ie*f`N20@=JC7utuz}&3~{a6+@;s(YjA0+R$Byr2-m|Vsz z_;gnPM89PkBpFfS@z~JQx8XXDRjve%o55qU0Z$4$$!9XPdfBGG_ACf^VI)!jN<1DK zvR=$<Tk-;6*|%_t){2KDg_uMgik~fxo!RdbAr?+-WunCMvc=|XO`gcI>^qoDFE5i4 zM|2J{@pi-;v9tSsFOC8Wvy_y09=6MzO(}g?nQa3`f7HVy#2t-+Ju<ocjm^{hri!~F zfhj#Do}0M8_{HUxW|R4CoKnuls3C4fmsD^R=C+~q%SI+#y>`oAdX`J7az3sMC7z1_ zlMAV{W0R+0XWM|FwQw<7Pz1xn_dFRVTeXRbpSj%Llz1*cHGNysIJuQwiEEo-E(8ze zAZaYiEu`0vZr=qc@tVNs#jUBG%&hgF7~~23r&u+C)&CwbC=|!RE=I*H+44gF%>Vh* zFE=4TJJU^YT>mk9^Mz(0Q@?`K%U97S!a)$~bsGdYhN+Nq%*R$4^WnCceJgJhJm?-% z;x%Y;BIZ@tA?11C*kyQoz%4x|4U(Z>Z$=;+Ckw0b_~Emgy@UG~CEktppS2}<C@}0s zoF?5D++D(FwNFAVXtTG@q8iW$!a+~GX&cj9D>Mdj0R932ggPH0z=0KaK)1=1w)LB) z-JRpZoCKeA&=T+UFN?#f4JGqsAbShn#F2t;DM=NOt3m<nPsc=ET{b01wVwqc@i=B? zZ1NOT0J{$Rau-SUXVF!LTt8wL$Ic%3jp&N*Wr2f=cyl(zcjx5tjkt00h?lt0=)5Ap zoRBF)r*EFtHJ#UF1)Nw=5O4m+DebH(b{WfYbFsofQD*`7IcG&fg-hIKr6O$YC)2y- zISV8ee@S=3>tbF>ub@2Pay-?g>o`&LTCN<I0c+c<ntdu|^%IgM-ptsPhZ)ZLDGqQw zMA6@9A#Xwe$#<2NF|Tf&HDJ$84<zzKa>SeSd1`moV!@3Q<|%9=$<<FH{mz|g;JrU3 zZou?Tb0?C}!%C)OqL^1J7F0_a8O*>#a4O~{^q%Kd`50n~Y@@|y`E>g{X`KIwo_NoH zxi3s(Z2KpMk*&eq%64wGCk-qr1hBK_a3I@o=d8Y)Vi<-MRpA>IR@B6s8=Lfyjl=6Y zz&y~XUi7Dg5x`EH1e|>SmN|V-h_#xcB;Ndw&$Y9tG$>ceJdIn>GO>bB%k2#UWW-~? zRWq>kqgew{5L;YMRK%OU?L>sa+H5geyID*LC$5g>!_w+yz<j9J6t4Md>Uf<G^WimV zQ4o)u$IaRt|1=|$zl+n#PP`&O6O#x6OfJ5Rm&DE<@Gmh`Nbtm)yY<rDHkR3lgc~6S zKxr8RivZ4sPAjbJi<{;RiVti_n$BIY#3O1KOg76~sOMw`7E-X~Y#LtrG=K<fw<?%r zpHJ^ENbxhlb?BJSvo!^#+E+lvyn>C^I)DvFvlcx9oF3oVE4R$*{|<vj358T0K@o5I zmZV8?VE=(&br%vyv@D`WAQNe~Y|EShUkjxsf*{_^*rWjn3~KY@MhOKNS`O(UfJ5FX zU@SAY&Ki6<(6tLF@rX7rR;FHu8sTQ6n3p`zV9@-70s$iN@`>EWuE!0SfLT8Z81ZIr zO`L;PQr9C0&4Z%3X(fe*Kq1bs)-aqUcFUY07JnDc@5IAt<yeLz)^(Uc{;n*|k0Q_* z0i1qj$YtECn`aLEsqr&k{`MR3=6n{{hqWr!AmJXTQYX%v6nEbsB18dUY8kW)S3!hE z0nNh{5EZ6`P_+V7at7q&AHxxcx9Y*ha$vD>pf%c{qRw3ZuQk~~XR<<t)(m=+sO5>B zgT?QYA6S|EbyU8a!EdsTQ}a3TUj970xz48jI~r}AkECnnxER6dOSscKR0$nhg+M3# z@6b{McQn^Pvrq+Sl>CUGnyf68RGXpJXo1XPJ!F;Yp}5)vRXV@Vu7uyn9Ms~8@kZ1- zw;A<+=KHe1J|o_OJ?Uy=q47D!#w|r@f@oh>LQH8#;t1CFt_bMaJ`B3F358Y>N>IxA zA(^W<SaEdBuQWnol@XG1>)>jk4zxHrQujXua<I>?i=Ej&)>r3$J|W&*RJ&l~tZ1~6 zx#xdX3F0r!a8$d$XB6~Cvh{2qE;uBtI91Im(Zkt{YDmeegGxFYUT-4~5^w}ui1MGm z@!e>=5^uqd^hl%L_yMlrrt@7;v85NPlEHvZkua=V6!h&NJhC~%ldKmn7i!_u)hbBH zsRaxE4t6JFm`zHVa@9vOdf`qeKaMxz&5XU+o8gqZu~@-j3)~6*?V73Jf!-})1deba zDsdl8l67EwP7NGPEQfM!aIXnbz$3@P$shZCE<dKHc_AJlos3hR31$^~(QQNEk%4Vs z5RP(EbYvR3=Ta#YRhgt%J=f}ln=dHrF>1@4fpM-ya30EQ;?0ar=z@2FkK-+ZJDq1E z-6x$|hrp9V+TwIFIP@#UW!J(F7mI_LfMtwEK4i<(zPaA?YOjcg8k)ncN;VEhx}Xzk zZWW<|iG$n1echV}<(;LIX;tvkg_0oMh~W|~%E)2cr*%Q?XP!BQXVI?5vZ`$B@%%QZ zNJrLf69>0}2Ya;urNY0*0N%6}-*NBn5e37#HiLb*{yUIVj)(L%zOxW;ddvWYxfXBH zz(P+8>G6n%qF=pnzKhzPf=9Ty_nqM|?ZFNxNuu-=Z%M`@(lR?v6hcaVoh0k$s8VK& z&2}NL_zx-TC!BZm{C0jGk9aZ;HsD6HxVP<Yht7@Xp*YGtJG_1Uw9>^WK^3eePP-RQ z=nTiNRKo6a#VD#K>Cul(&Mz^=Wfz2)EY{W7(BnO6Y>!5~8K0iNUnW<d@FYXA#p#8k z+;lq1x!i1I^xJ+czg`-~<wzn4SXfnSKAn+ksnqJjZWI~cFw8@+bB5bo@My%7D>MsG z^Os2A@~oJ6Xj;FPn=q&~YN7Dx{p4B3B9BEzzgHgZ0>8wU!JhcidI4^cR|#qiM*YdO zYpR0E8Wq-s{M#MQ__%5K!)@ZZ-J@RqJifWkB1=K0x3}jmwa;*k9A-V-5y+a3I?oYE zDbT_fzvV-<NJY@hRx2dt7MqgtisUF(=WLKF1IFAFzMfy@4pMG!%NeYSp;9JY`zSRm zn%I?+&YfQ5By3-NaV?q{jW;gTPC^LPo3o}Cmgv7dn`DW<R;X@*bff~Ufl;|Flk7I} zIF`LvsBn2(u6}rHVDS@Op{?jTinlfQU3@b2d=oJx*owiG)M|~pFQl?3uU-o^nk}wv zU$fkO?$*uO@aZ%iPR0zyBLvAgaxV4!19yT<=JxDca#-+PAr0;d|80C{*pAcAGwE*4 z$oMXb)AWoMtLfy`94@P@QsXp`vj>d4zq3cq{M{xVcFS<7@oxOU*6{Sb)WVmuPQr&j zd7n+i7h5FWuHT<n#z&I8q+zkz*pz}2OI%I?>OXS2j%zwEj&0F&fLousO+2Zc-A%lw zZT;@$t<X>(z7bHM%8F-(wX0WMu@`qd1MSj^%Z(Sa^Ho}-+1o{^Yg5t&6?U6=4dROa zD>*74;$PJH7lBDARBl0)6bDh%ED)~RVA3DEoXM8dXalxw9kj$7*}DZy8cL^^f#~=o z&nfra0o8g7T*$2PTM-Oqi}gZwK3pj*3-w#3PBIEg;tl8&0ngnp*<(m2O)Wg{S))3l z4y;^=%l0w(IAAYbD=;PIm4uk^Y)%L+=!i$o>1JYs6E%9&1r`C)1k)bs0OcrVmRoM{ zNnr}AYK=!PXEJ(|CB!Egc$lc$=Xf6GBZz2WYB?+z--S2Td4i~v$P1yyt5x&HbmaZU zN44H)`86@kxaZOp4U*2=%@f{H4jST-5pKcwPS6t7=}{*v1jr%#OL)$wQg}ZLG25)R z<5#l{yU!;vc@@>}pUL?NnxL6#P8!-4qTAYq%Kd~c&BkQ}h%(;^cejBb&X>3xr(r;= zm1WaP%8VDX3p99LM^MwshUK(>4+`S-?-+rKh^UQAd!q~W^RYo~pahjEok7J)?zp1r zI`({KuDPUE7x3LmSIaGV;x$Kg=+8gU!BsqHxBw&YBp&gk<Y}SK1<lqvvqgLSN|q|G zqW-#$V5gOiAeH<Y@0nqDpnU?#zh4}+ttkCkgezg%sHR_(X4FA{F)QEl^~w0~8>ACm zT_xQkhoUl`d%IEddt&RA<R-(tU7N$X%*InrhRrB0*PqGERpVi}sCUUEOT2J24l)@> zJL<%Vz_Y{esJG*wGhf%=Qc<Tjo=D4)m(~VO%!^MXB|Ft5Ixnd<Fh0AAf72o_&ZkkJ zy3T0gj$F<(?7on!LDGqOlxv8fBuBjVc#-v?-cb$fDcbK9wPwghsg9Bw6V#w=W3AqX zIzTL7wxSwg{38d@k!X!53fHm3pe0eX+r3>FkhPMSoz7@sk6p>*cArnS;hIkH#{=$S zElJ`%g;Pwiii8nMuEa`DGU}*IL%Vf#M$cpmLs3en{|!5QL!z6DN?PaJ)7hp)JI|}G zmDiCOnIu(6N{)EM{@egOw-cK?hi6wrEqYp!ju*{bM6)w7G(!HrJXQdG&{WvSUM<n6 zN`zo<*~jNpaIbuI&idQsLcxi6@!g*!M?4~-A{Iw9BpymGhrO4|JRaFxt!rw&7RZ0= zNEM75&<cikj}nXu`m$P+<+UAW*c~S_RXBYX_Z->929Xr;`k+>;Xf$su21l&!>^xOi zzi#tqdCPFk_%Bqqeju>|o*2>whT^t;pd8W7`oZ2*%c}3=l+`+Opo$TYd?iJ^$54%f z&|OKj3BE?cT}GXgfpn#{CWt+pha+wU%y{@NXpNgP{&Fl5?$=8N)`ef4wPh4nOMY7E z2v^AvuNw+LcW)=`(N2iA1+q%@u;Irn7=zQ#as694>La}RCFE9F7wtR;`x5d=-;+%L za~CAZRQ$m{ZYz3n7b}m$<IZ@!F^{9E6&|PI%ZNk;!*^$j@TkH9kKqjgUzm_4Q^}Rz zU$Vp=zN$dwLf+s1@g*RBNLv!biwKd!K!KV-)}!Rb$NR5AI__2q-R(zPcX<^?nDNkE zcs9sS*D#oE>=(x}xR~8>TuHS)C^k-9X=i@WbtNYsh9Po&8l~`raEQ68_kYR8&g>ht z2EKrltUWjUoP}n$y5M2FcRV?mRA_zu>$8f4Yn8qzs;JWxUT^gP&fz_TH_ap?-FqS< zogI22CG*d{S*Qifv%~wcS|dAU;}O%?b$co4?4UOK6g~0oY@va>@b<Eh966u+XkQNG zi)f^4j9+Pu8DbCR0$%KH{K)BVz1hmH+8xieoA))d>r7Tq?p8Vlr_lq^6K|LRul?*e zUH}=z`bJQT?`L|k4!%OE7gyYl;~CcOulyU9eHX{zaY4{;p4<*I(Xa6aq6n0b+~Jf8 zIDS<+Yns#P)zG132#j@d)R3HCZJqY%Z_I`4a?$rCfA%9$gVjDtgSOX%GtDeTsSvU) zFE#g{v&AqF<zL%0`+fF!ty~N1e@w7#K6F)%5-4<pbF<M!J=GMN#n$ad^KeZhHCsvm zMABpnO1Yq69kAF~cGdR@?0`kzbDIucp`>$@aQ5J$9`T~v2<MvC$+SwyC=vg<&fYN^ ze-cg4AMejFe6TlOfuo&w=M0S#55nJ~Cf)#n9GV*~Hu(NrsbsrPrPjcn&&spL9;p`2 zn@;3ti<WrAt~R-SEd;kep&V-QR=Ct08u&1uP0ur;qS$bpZTe4&T53Q#sE=rg*9)%z z3Begq4)fR*>Fi<>Ol{~=3QKL5a*9+)K1w(rbTrWtuWf@JHXq>Pa+2{`H8|RNCgI_7 zCjJVD!&*Ugo#kvsuDL?1r~2-Fh`VTs7u{AE%Z)>LZYKp-YcLwmX6C}RvT8N%%hJsg zA2+IKiPr(o@B-}$DvhENfOdqJ84WeY<rM0Y^NK@}Z~;4y`*rbn=+Um3qawnlLs#IP zpAsED^Tcz?eO+MvvqK$5t<aia#EPH6gd1kQbfP3)B&xAf$OFHzas{=3iPdo|YfUey zV9#e4*z_iIz^9d+TZKcHHjbr`B2dRBpDrp?<uosi%0EiY_^lD2=roZQG3g`(7nkE> z6T&{KWND37cQQREq`Xcq%J%mWEPhQ^l)$$^O<93<TGV$bw%Z3<)XID|GtZb+S{d*W zF3{~4==?=Vya-L;*ZxIl$IW1N{mM)r8`{RP=J@PF?ow`{5;rb|u`3lwKm?GtD2W%U z4*dGA1O=kS;IhjqOs6yQ<T|5Slx-~rj6Er%C>tXT)sqL>k-KU_cICA?^T~{B9Ioen zzfvIN0tZ>yL`l4G+|>-UtJVoqL`-KgThR`<CB3*zjnm4&PYrG~RG3-;izW_m+H9>N zoNt&t*!vDx`OG~I<5ug<@cy3UTb}*jh^|hLC+_LuT#X@F#w>ek5K8YjzGbrc@Kq?N z^!}wA_#S)<WViPS^V61ZQzbE3g|@!qtnoQT*2KIbHCopZT)PtDz5UePO+o7P*#o`W z0r_|2a>}$0iTCF}bcILnzROXRCjG*ZcuSrb<TT7r@p-{Zybx93U*yIc1{0mOkiQ;q zoHeJs+IlWCm#s0FgssunUzms>AHlsDUBOH)3q0}A47V#q^=X(&t=@P#{hGS4x=xhs z=u>b6o;YnV4;zTw$_|t>n=S0c)GPYb{1T202~&eC@S9D6!a}V1ijsJ&0MskU8UK&E zM9k|-_BHeQ#MDre#d7^#lu6G*NVpOIiBth)ph%b~O5zy>u(6?1`d6N~pt!_*`ce{G zRja*u9cN=@psfu7gPw!12qUONoNo_`q*De%F-1u{1FA+3v<v51aMxLV^ag|R#Ki=- zR#@CLp=?$LYO6G$G}J;wWGKiLf@)hdp#T@N%bisddfm|+!kl1QUTcJF6^_lC<&@Pq zOf#dT)@gn%aCYuYAe$tHmTBu1JUL3(VCLuUrJx$eQAHj3)p}?b+)-Md`bCENR&32| zu|QH<rY`>Sl@N|~<2=q{jD#wDGigFu2o1O4`p>robrC>fEojKnADo81^6t~nv%O;@ znVSw>hPQXcIgRV+!NM=kIP$LdoAYo_*fev5!{<fSdicZgpXjw9N9gE{fgkZY;SGti z>5fY0tsiH*oSDPLr(Ok}!RVPVGsnrnfQP(Bw7V4^X#q``Ajsz}GE#~!$`MZ|fHO3r zOqXw0SyQ7uayCv;SzW_V;vA=jI=s51Lou6(W+qUng`ud1Z{S3jo+yc@6@ZQtVk|`| zXHdbb_s0sjj$_XyrP|VS@_a)K)~bY>3MFp5kP%Rh(@+0z$%>F1zAUmRiC3Wwyqsh+ zG~C!0RSf2%AxBRgsy??|Ou1}Lyqcl3*;wCC2TcYgnDDKo2{S`@gc=x`f462m1xkYP zL`giN>qH2S*c0%Sc-i@dx)T=?G)A+@-}mDbVALtVf*UZQNI(s4@qrtgH$nhj0%Z^- z@hWl2LTnT$0{p`6cUL6*5?|(4H>j+sHJ^%4v=x<>d$w`nR&GrMwpr!4{=@S>oe?6U zl%P=3!}liO;`@;(iDyI7mDie}U4ShGb#5I}Z?3q!?)Fn(z0qhqmzc_B=H@F|mJPh_ z3VTnoS~XCGFAsG{9fU=OgB%yG0dNnm*$Xnpan`j4elH#GJe>u<T*`M^OJ8dkBN>a5 zc%<MtWrlzgPmUw*y<MYV&n4$}6UccS9uF82E~kZ{m=`I!FrCQ|0cMj4Lc&yd{$~b; z@#_WJ!Ycn6d?D)g1Fdpjw7Xk*UTkXc+4IMs!A2)<YFl1WiTS4^Cv@i$Q-n=AJ0KjZ z27MiaBcKY*<RFkbUm+mc5ic7@JR!JnjT}bwih^U8f484dtJUdF#wDooi%ZFrQaIB} zR{<m9XEjw}pwww00=I7E3g7Rzx>`1ggcB|Cu9fNQ4J8UevF#H>+QNw|RbVum;X=|C zQ%Xi=U^h<q>YNsHI8>FgD1K(f>8Ao@0x{3TSN#H*j%cTv<ls7^*njJSrrL;TIv(0l zVf*Hn12!a`U)M%Wm5Y~WL~n~huY$^Q+t9lrj4~PHy~F@71rv87q9q=I{c4eLmLX3M z?=0_nM>EN8$+vGAraZX(aZTEL&oGY<?#PdbzPP#LRp~`dyfgt3uX(rz-kCMptHuy= z<d0r_1R_H<(6wzO{P5aX#ep}+o1)uB3QgmakRiOOY@#L}(QQdosSCwDHmp0mH0fTU z6o<ERy!iM~m^7l-P3ev4-$9-E!DQJd(}uGx!_|SmE^<@pg#Sa-#3S%tK*Nwia_`R_ z2Lqzp2&qV3m8EybR`9{Ru};&NH@3G7r=Q#lBfHbl&nZf`J`g?e60bSFfqZLK{o_Lp z)qh<1EOc(`*w&kWO2JPqUD`##k$0zpQqlC%wFT}jeLi=jEaQX8+=PK01S}=M4=#M9 zB6{K_=hs1vUO3ZD(kPvfbpKuXCy2szpIBWh)E@Ek>OVo-DA!L$aQ(;ZSu|ex@0Uhd zyR?p=yPr)ozUYZZ>h@cz@XR3(jBbmg#q**hU#rLv_<qF{=+min6J5voqsjM0D^fN* z#jc)wk2ON0q6BmRl_ZE4ms1Nwa9U_?VApnV<o&78*@=l^p_P}EC;E;kIJn_?7}3vt z_2&)i6QWYcmrlA{acTV%%#4vegf%5;SkM4{mjv+&tBi0}pk|Q=b|E9*ROr{)v(dw! z{o<bP?cmUc{{u2w^W~!3M8ej&4=Cc-J!u=<xh38GyaB8vh)3#jFbS`=2-J0M+Z>L* z_g8ph@*f0QLL%fnZ{lD$^v?6RDdbqwm$wBU)TyQX?CQtmo2Cypb#Cq0*15NNh$(*4 z+f@!~GWhX%FQ9j42af{kMK)Llb&iAx0hsV|1VpG>31bIGLyrzE;bM9r)aXo(g8ALA zMMMaE`S-`+jVZ%Hqw=ljE+P1k=+=rU_jR#hUd-9_5*8)@r10|3f#e^*aHsL4Q0e;m z60=zclbkC|bq5q*hW*RK{UCnRY<TUN`+yuh1j<E(s9?bpLm_9!OYro_K7K7m`>0U) znkj>s^be-Mq#?m6ekK{>k)CrgyB3P7g`tn7sh%7a9s=*q84FiFn-6~;)z_~CBxIjD zb^x5-I1M)b?GcCw4cx7zj;+GwyI&ovIJ{(nwOiZBpxS_u%v6(fbYeQojQ@i=MK;y% z1@o*0ZreZoKyM(UAaQx6wV>r$&4InLQD|{>;iO^k+3OQw#)P|}O>@`J=j>_t>J!ze z%jS*g#k2@hv6rq@fX;;X0mbFqo@%;X!CA41M}UKego+D!qc55m4V~NEY}-Sq<!x9l zRQCsa&Sk)!vl;N+iOW!0<=yRzyIM!WgrQww%7cC2fdQQWclaA-C&=%pU&Z?Oug|la z4_yh|Q0zW3M`7p8QSL;O9Pvoyd$tdQ*B*;@2M0lh%_**iL-E;=QCJBnIc1Q9|CM!y z`cctts*Jp7$aJw~L@4y`)Czjv)e8FJ(Zz^<9ieOcC_&cCl|rQz)vzyqcG7Y%rAQ3- z8{H<JZ?~~rL73+-b$SE0>A!JCyqFp?vVTXOQ}=Ijo!d39pB6gG)yA?wY-7q%DYV;f zPe%y~%&*5C&17#s-dBC_bP6ipZKjf%@lDBWu2`)$)0s<2mhbi+v}NSvhWz!NA5ay8 zbl2;|95qpUAw|-WcYwuegJ~On0x%g>P*Gw7ZFL=RHpv{oi=TM-+Rw_%uig2}J_VY! zQd(`c`ca%r(^l*_Cb0K7Esm=Qto!C9WEEA}gQN8$eFRjMGxZjbK>X?_L`}Sk>RR@v zBgf5$PG5*Ln#{NC9K65l3|vSnv^O686bS-p1(mS!D+e!4n@t9F)k>%;F@nYH*ixlP z>L^O$8H`55(Q_A#yZ0YqipwjV?tYnXOjq<qdPPhm{juH>>!ju1HZAmSwT3`-MHpJZ zH32ICyWAdtD=OmQI*z@Va)teN??E-1w8G17+-;XQ$vLI49;YqTi35RU+mAx(HCH5% zu<cC-H7eh=g09YnJ59ouoM_MlqS)2M%{#83sMv5SJ{fAXTDK4AR{iTZ+|#2SJURSk zy&(;7pkD=m0698Y^Ysb$O2Tp~)P7B@-^$STpAl_9h~oCK;E9LUb;xd|yr87aZNfEd z!G$01he6Tp@NS%Mr0*N%MZdd`KyGObOvRmOj~#1caO$Z6g;s~!uN7$hNYLkhf}Lub zEN0t@3zu~N*>_ltr1Sa(K^k`Kq8(6CD-6}|9(OPaMzr8EZQ|SCKw&u#`g5$7Fr0oW z&;*$YY$!h{P$xLz+0b3d$}yr=X6xmw99{(qGYYF<!AE-q+7!WCtf~L_FC=D_@(F+( zjc=-<x{L!|Es?tuoCY+nM-o2K$}cW4p$$1@MRkqO=IJ|+q|~c7SUqhNpGBu>d5FM@ zuYQAXj;44R`!>;7Rt9=)7#_l-^h+cMDve;KpMoG>ola*vej&kJP*Um_v&3WpPqb$( zcv5=GXK&LL1@)Mm)x~^y%=cxoSyfPj)|?bdEks6X(E3rp`+YzQfMP+W3rQ(lN_w`M z1I{m`v$yg~AMb-OjS8keGRW>lKmCZnd*7Xb6^;(ceOU&pB^)YCIkfw3!V!=`<C{*; zfPGmEl6Iho$ED}w8qX%AC=4c3z-+J0`rv0!qv`MGM)&tM&NTf72yFWKA}syvaQ$I< z18;wQMX@tn|C#aPV_1X@NqCFIlRpaLHu3Pp+N-Uil8Q>hsklU0c~!MvM>t1dlB+dS z|M3G5o7Z&YbA%B8e((PKG`zL_NImBKr|w3I#14HOs{hw(jS!AUOe(eK`w9%+P<7L} zO+1uIu5~ky`rulv-g+)6)smT4fEB#G#EB<)coK)j3e9})C#W-+>ThU!k_L_A8Um|! z9LF`?Z-E-=92e4N(?AU>L{@365P=&oG7qfZa^SY1Mz@KFq{##~E~8+_HeXCmWl}P- z<Q9u9;G>))^vUb$<xloQRh<!HratH>2=((4f#qNP1}lYpTf}TsK!u4|zSTik81_@V zcjdaf>Uz-YAw3|T>=cskw)wC&)>=?jW;k{(PN_GVJRinyn-hNl$YP%yw7)lJ91tBP z)On9UHO}N-{$w9~aUjwA#C)DYz4)05j0O(E!g2i<N@Rg=Pe2Y1yIVB3iD$7GPOFq5 zwYUrERHIN)S-JH>%4Jz`X@%%Cmwvu40gfl<!GZP9QSmZ&dsCzE`DL~6)QVkj{%T>P zCj$S;qOzsBYP<ueHQ@S>1Es1daYa0!H@W>*xvg}+BZy{gzO)Y**|^5fOlFJuY+|xL zE$3P!$GY&QXq<q6eiMz#n_hkt#t-Q#pi+saC<iWP!*lDthq4;)6gCN>#_CX<eujtQ z`QNR4VVHyP^$h0*1f5jCqpj4Tf1F>5+au4}(<EYEiB~c>ypXCWt*8vandq$rv<DW+ zXSL1<JC7y<hdZCcdUZe>akrmwh@@&hEvC_8g=JqHg?S(D0UZjANW$4HWcrDEL~x0v zP+9T(?|0s?%-<m8#|(Gt=@CIDzke#Ed3fkS++-P^S6HGy8J{St)9EF<DC&#ms)*UD z?sv9u7vG=?pO{?=&#yzd%j^>A)>|%TAvA);^&eTLB|y*odmtvpZCOx{h(}O;`rT9G zPhO1wxwx#t?RBCUTZQPPT>6(0@YZu9aKDR=ehu5uV6wu7U8i6*-g_iB4T8eSiTGLA zv#{%>Up%R=$HW7Ok--?|Hyrgwc#^SL<8-?-3bwuWIEY<!AYb?&NyvePD1MiOqg`-0 z=2tlQ_5@Gs>;>^K%h79F0%N^`-&%QE0I|k<_MtwoZpP@K-T6$*uY}jP9Dturr-`)* zy(xDASd}B-=$kp7CgL6ONUmd7jYEAG`#ddx*yGKcIHdm2y-o9`cAUjt%iqhNUtSAq z{&fPj?ui4d0Il%;UQ!=@V;B?eu{?sK-BE9H@Sb=i_i?MA1D4(4O+myQm#9&n`^O=$ z<j?m*`&Obmx^xM8awfNS=Lz`i*94Ta#0|dSb+5p|W&iZH`uq@&lw!=PdAQyB4t@*w zwoqb?PmUb^Z_EIgiPm_A_vs+kYTQ)rqY2mQN4K4LS=DZ@;idq=^8;l@m|1Y>?QMKi znLpx@(u|Ag1+4tPNWh+a6i&=(4C!$P%zpG9cxGg86hA}JKM}dI8a?d#EfvU}xdz&t zMN|X6i^`y6iunaNxGJ8nl=&qdDb?i3sO!Gp8z5s|$6t8gBDftfZ~WM>ZuKMK!-G0Q zWT;@Y58Tu+22}d{?_;U(Esku*l5+9zn<@cDfQV+SWe}bJA#B;gD<J$6&)%HlR^Nql zlO=fEFcU|>pg8NZSKZksx$*wsfQ~R}L@yZ7r40=1+OGa0uCwsI^d|-|ujW_4*~<m> zWV@JNM9KC$;6^mh@GZ!=S7Bca>Wtvu`Hpz@vXbd1C*O=apCj#F^z%9bo!T~o-W^-P zxWUmdAi8b6J-beAnxRAzzs-UQxPk0OUd}CtxQrq=ol*c7(+i=vQUY?6ymuz%DE8GQ zaA?_S-ji;SAipi#AmLlSj$8RGE>72CrMlemjC#}~KsIMOwP}t!qv6mlD!ksnWFI7E zbF`Trf>YKAoTh42a{O1I^vw09BdQ|_u?%KQ{fI@Z$kw2oX;yI!q~VB`Q(Oi4Wpz+g zr3EwIw5QG)0!$_f<}OA33jcwlF+Sh6^&jy_eIJTxg?APfqLIM`IQ?wq3<B!kGy-Hq zR3P!ltw<Z5r4bX()On0RHTK1i!OFdhgv0OtasDNqy-ttDv;(v3ANX^s-HU!w1OkA- zmna3W3JI6t@4N#;JbNuiulxh<etv?l>_K)f`biP+0|FO8E}IYg-a6-pyc}i`SmM<e zi3nTEaOaa*hC}|H4ogWrDFVD8z~NMqS%i`&TX;#%i$nxKyc?x|;LX;c33(ZREyl$Q zy?5h9qtd925kUD$U^hZIw-)~Ezja*WQZ)Q69OBtaH72GbfP4dvepBpT^phgsF#;TX z4|3);*#CC6$7%Q=qVR}kujIHD6M$tt!k^K0FZxLla07u1JP=(72j1S}MjXEd6)y4Y z5R6%Y!q;T|2kq=$^phgs903L;P1qQS;x@ys7_;+mfqf!;;t_ntZD<3m;cX-!xdHLJ zqM5*AM{^ez0xYW7`X{LLtKpaRRl+LTt)&qM@$BUuAJYwNGO{Bz%I-x!DFXO1K7(?V zvtWNrvRKd%7xC<<j#)Vc-)=Fu``O#>ML&ZO0fs^Si?AGTgYOnw?Zr85%R;lR-3D(5 zPK`#hnZ5|RG_QS&tvC6vyhOBX1?a+O!m-6k{>zrXoWwceHLU-mt2=>(dlRK!W}~<G z>(j7q^m`)&7;-0W2gnUeVc*)KMh`{xQ#u`W^qK)6V`Gtk_ll|oXklGLfPs@}Hed<t zf9sfQ5riE|I>f8T85PTqU4z$;xHY)o?jmgHY3W=<pa8Fvy^Oj}zH>3WD1u3hcsDSg z5EBVj`9fTHzlj745k*VTLN-Exsli*|o58{^$J+zOMh`{*Q<}uHH_LVLvv-kvlkHyg zQw#{mz6H*@5)Q?rilMZglu_Emvm-Ko)flh=k^iLEkL;KT@)NarvC0K-c(EkbaZN!S z9K^FDFlNOxl;2y4Q&4Fry|mX${GVvKl6ecy<31OERa~oqIM4B1tJ|%ivW{MYjt|D- z<y&ISmhOJuijbdu7*qnoeTZroo`b{7Px(3XfMyq5BVI$OA70fJ<f!?YgQpsL)9-#l zfPuZhu&<(+7cqJhbS`*_XK(7!s~$l)zju*<L;{97Un9Uk3R;+Y1NN`n>+7t9oIWL9 zee3*N4x_KUh;lFF`cX?EH<)P4#sP}=!CRmYkA>5(5)I8k?I`gYHvb=2-w7rTuOD%L z#Su`lrllKJ*$>}qaZUX#Ft)|M(#q_II`EzaC7vTl$FJ)KtYs6DZ?vPw)X!xEjsPoL z42J`!&CBJI`Z*vaUQ>XNTlEZzpRGdzc55maeRc~1sNaM`J95i^#3xkS?k(`3Rv`y| z)XR=Q=8;rrKWqmmRXSY%4McA$erCtU;U^LELN)!N>K9<|QZ#KvoihaJi02HM#{Oet z+T$TF9=<a(8b6}HfklOk+aWCEP5AzeQi{jz5un7oJw`W&cqpbHDC?~s4Zrz8(Ed7G zZx3Vtva>;pfCm+)#Pa|&n2g5{Rad8@^!yTB!F2W{3Gv5aaAf1&K&{MQioY)YuK^{V zH<&;9L37a7qV{WMDJG-vCMglc)gs|O1%qZ4oLofC=BV=k0ZKd%K)WT%MAUw5VAdl6 z|9s0c$#{^>N!d<N*p|SrF*%Z{R)8u(iN`1K53e4M>L-YVeGfj96+l{PC|373*uVUM z0LmhXB2eP--;4~5UNsv#Dp~)b9S9LS2JpHLtU@*Dn{hO!=W#qXB_$q@VBVC}=(R0T zOp2`i=Hi-2Z0CQ3WbZ*ZyGAJ1b#B5|0zWA6eBFqnV|t)=toJaRKl(Z=Ueafn!(g$^ z351nYUdk_$L@4q6)lj3?K8YjXDjfa#`72Ae@?qd23J0teqgH0O>M4-Glz9GZhzT3j zV6FKZ%74C!*`g$jAY3(ydBxx;w+%-*JSw2h9|%z5`2*#~c|45TuVvh8=sh1tzyQh1 zGAMt_egmvxDI8i_(zuB9w{H=k#Pcn_O{YG7eP3WrpWqa4WYYn8e|iMZ<d(s~w@&b$ zGELAW1Y|CSqJeyaz`n&Pa6dN+WgW@EdzNo9xhd6R9ATe<5$qUBx|?wE`wu$e`5kg6 z8BTsb1gdo};n^R#0UzchlxN;V?E=HS4<oph5EH|CmI#f-Cjyjse4@!mT15LbBVUah zFwgUl9G=H9y8vt`%p8-;LwYm`o)DnK;|WOKlNq~u2(auDyns8&gS0t}_bbY6uYm(g zPI?fZM)5HMlz2V{*z4pc#B>0&j99=Oi)oQNfJ7OuBUgc*@i@Zm!3~=q;qdBAM}erH bFcA3v??;y!V7JTB00000NkvXXu0mjfP1!2= literal 0 HcmV?d00001 diff --git a/apps/web/src/images/secrets-manager/sdks/c-sharp.svg b/apps/web/src/images/secrets-manager/sdks/c-sharp.svg new file mode 100644 index 0000000000..f0da2234f1 --- /dev/null +++ b/apps/web/src/images/secrets-manager/sdks/c-sharp.svg @@ -0,0 +1,7 @@ +<svg width="99" height="111" viewBox="0 0 99 111" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path d="M98.6672 32.5494C98.6665 30.6878 98.2683 29.0428 97.4632 27.6311C96.6723 26.2428 95.4879 25.0792 93.8992 24.1592C80.7854 16.5977 67.6589 9.05936 54.5493 1.49017C51.0151 -0.55023 47.5883 -0.475845 44.0803 1.59384C38.8606 4.67217 12.7274 19.6444 4.94006 24.155C1.73301 26.0115 0.172453 28.8528 0.171682 32.5459C0.166672 47.7525 0.171682 62.9587 0.166672 78.1657C0.166672 79.9864 0.54862 81.5994 1.3183 82.99C2.10956 84.4203 3.31052 85.6158 4.93582 86.5566C12.7235 91.0672 38.8602 106.038 44.0787 109.117C47.5883 111.188 51.0151 111.262 54.5505 109.221C67.6605 101.651 80.7877 94.1135 93.9035 86.552C95.5288 85.6116 96.7297 84.4153 97.521 82.9861C98.2895 81.5955 98.6722 79.9826 98.6722 78.1615C98.6722 78.1615 98.6722 47.7564 98.6672 32.5494Z" fill="#A179DC"/> + <path d="M49.5702 55.2075L1.3183 82.9899C2.10956 84.4202 3.31052 85.6157 4.93582 86.5565C12.7235 91.0671 38.8602 106.038 44.0787 109.117C47.5883 111.188 51.0151 111.262 54.5505 109.221C67.6605 101.651 80.7878 94.1134 93.9035 86.5519C95.5288 85.6115 96.7297 84.4152 97.521 82.986L49.5702 55.2075Z" fill="#280068"/> + <path d="M98.6672 32.5494C98.6665 30.6878 98.2683 29.0429 97.4632 27.6311L49.5702 55.2077L97.521 82.9862C98.2895 81.5956 98.6715 79.9826 98.6722 78.1615C98.6722 78.1615 98.6722 47.7564 98.6672 32.5494Z" fill="#390091"/> + <path d="M77.9793 44.8215V50.0145H83.1723V44.8215H85.7688V50.0145H90.9618V52.611H85.7688V57.804H90.9618V60.4005H85.7688V65.5935H83.1723V60.4005H77.9793V65.5935H75.3828V60.4005H70.1898V57.804H75.3828V52.611H70.1898V50.0145H75.3828V44.8215H77.9793ZM83.1723 52.611H77.9793V57.804H83.1723V52.611Z" fill="white"/> + <path d="M49.676 18.7412C63.221 18.7412 75.0469 26.0974 81.3811 37.0316L81.3193 36.9263L65.3829 46.1024C62.243 40.7861 56.4869 37.1985 49.884 37.1246L49.676 37.1235C39.6068 37.1235 31.4435 45.2863 31.4435 55.3556C31.4435 58.6484 32.3214 61.7351 33.8481 64.4011C36.9908 69.8876 42.8988 73.5881 49.676 73.5881C56.4951 73.5881 62.4368 69.8406 65.5635 64.2954L65.4875 64.4285L81.4 73.6469C75.1353 84.4885 63.4715 91.822 50.0839 91.9682L49.676 91.9704C36.0883 91.9704 24.2288 84.5689 17.9106 73.5769C14.8261 68.2108 13.0612 61.9896 13.0612 55.3556C13.0612 35.1343 29.4539 18.7412 49.676 18.7412Z" fill="white"/> +</svg> diff --git a/apps/web/src/images/secrets-manager/sdks/go.svg b/apps/web/src/images/secrets-manager/sdks/go.svg new file mode 100644 index 0000000000..d335346e71 --- /dev/null +++ b/apps/web/src/images/secrets-manager/sdks/go.svg @@ -0,0 +1,7 @@ +<svg width="207" height="77" viewBox="0 0 207 77" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path d="M16.3 23.3499C15.9 23.3499 15.8 23.1499 16 22.8499L18.1 20.1499C18.3 19.8499 18.8 19.6499 19.2 19.6499H54.9C55.3 19.6499 55.4 19.9499 55.2 20.2499L53.5 22.8499C53.3 23.1499 52.8 23.4499 52.5 23.4499L16.3 23.3499Z" fill="#00ACD7"/> + <path d="M1.2 32.5499C0.800003 32.5499 0.700003 32.3499 0.900003 32.0499L3 29.3499C3.2 29.0499 3.7 28.8499 4.1 28.8499H49.7C50.1 28.8499 50.3 29.1499 50.2 29.4499L49.4 31.8499C49.3 32.2499 48.9 32.4499 48.5 32.4499L1.2 32.5499Z" fill="#00ACD7"/> + <path d="M25.4 41.7498C25 41.7498 24.9 41.4498 25.1 41.1498L26.5 38.6498C26.7 38.3498 27.1 38.0498 27.5 38.0498H47.5C47.9 38.0498 48.1 38.3498 48.1 38.7498L47.9 41.1498C47.9 41.5498 47.5 41.8498 47.2 41.8498L25.4 41.7498Z" fill="#00ACD7"/> + <path d="M129.2 21.5498C122.9 23.1498 118.6 24.3498 112.4 25.9498C110.9 26.3498 110.8 26.4498 109.5 24.9498C108 23.2498 106.9 22.1498 104.8 21.1498C98.5 18.0498 92.4 18.9498 86.7 22.6498C79.9 27.0498 76.4 33.5498 76.5 41.6498C76.6 49.6498 82.1 56.2498 90 57.3498C96.8 58.2498 102.5 55.8498 107 50.7498C107.9 49.6498 108.7 48.4498 109.7 47.0498C106.1 47.0498 101.6 47.0498 90.4 47.0498C88.3 47.0498 87.8 45.7498 88.5 44.0498C89.8 40.9498 92.2 35.7498 93.6 33.1498C93.9 32.5498 94.6 31.5498 96.1 31.5498C101.2 31.5498 120 31.5498 132.5 31.5498C132.3 34.2498 132.3 36.9498 131.9 39.6498C130.8 46.8498 128.1 53.4498 123.7 59.2498C116.5 68.7498 107.1 74.6498 95.2 76.2498C85.4 77.5498 76.3 75.6498 68.3 69.6498C60.9 64.0498 56.7 56.6498 55.6 47.4498C54.3 36.5498 57.5 26.7498 64.1 18.1498C71.2 8.8498 80.6 2.94981 92.1 0.849805C101.5 -0.850195 110.5 0.249805 118.6 5.7498C123.9 9.24981 127.7 14.0498 130.2 19.8498C130.8 20.7498 130.4 21.2498 129.2 21.5498Z" fill="#00ACD7"/> + <path d="M162.3 76.8498C153.2 76.6498 144.9 74.0499 137.9 68.0499C132 62.9499 128.3 56.4498 127.1 48.7498C125.3 37.4498 128.4 27.4498 135.2 18.5498C142.5 8.94985 151.3 3.94985 163.2 1.84985C173.4 0.0498471 183 1.04985 191.7 6.94985C199.6 12.3498 204.5 19.6498 205.8 29.2498C207.5 42.7498 203.6 53.7498 194.3 63.1498C187.7 69.8498 179.6 74.0499 170.3 75.9499C167.6 76.4499 164.9 76.5498 162.3 76.8498ZM186.1 36.4498C186 35.1498 186 34.1498 185.8 33.1498C184 23.2498 174.9 17.6498 165.4 19.8498C156.1 21.9498 150.1 27.8498 147.9 37.2498C146.1 45.0498 149.9 52.9498 157.1 56.1498C162.6 58.5498 168.1 58.2498 173.4 55.5498C181.3 51.4498 185.6 45.0498 186.1 36.4498Z" fill="#00ACD7"/> +</svg> diff --git a/apps/web/src/images/secrets-manager/sdks/java-white.svg b/apps/web/src/images/secrets-manager/sdks/java-white.svg new file mode 100644 index 0000000000..082897c081 --- /dev/null +++ b/apps/web/src/images/secrets-manager/sdks/java-white.svg @@ -0,0 +1,15 @@ +<svg width="61" height="111" viewBox="0 0 61 111" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path fill-rule="evenodd" clip-rule="evenodd" d="M20.3909 58.8794C20.3909 58.8794 17.4831 60.5719 22.4627 61.143C28.4963 61.8318 31.5794 61.7326 38.2264 60.4762C38.2264 60.4762 39.9775 61.5716 42.4188 62.5204C27.5132 68.9065 8.68435 62.1503 20.3909 58.8794Z" fill="white"/> + <path fill-rule="evenodd" clip-rule="evenodd" d="M18.5686 50.5444C18.5686 50.5444 15.3067 52.9595 20.29 53.4745C26.7358 54.1399 31.8241 54.1945 40.6342 52.499C40.6342 52.499 41.8499 53.7339 43.7643 54.4087C25.7454 59.6794 5.67598 54.8234 18.5686 50.5444Z" fill="white"/> + <path fill-rule="evenodd" clip-rule="evenodd" d="M33.9225 36.4037C37.5972 40.6336 32.959 44.437 32.959 44.437C32.959 44.437 42.2841 39.6239 38.0024 33.5939C34.002 27.9731 30.935 25.181 47.539 15.5518C47.539 15.5518 21.4749 22.0598 33.9225 36.4037Z" fill="white"/> + <path fill-rule="evenodd" clip-rule="evenodd" d="M53.6361 65.0451C53.6361 65.0451 55.7887 66.8196 51.2652 68.1919C42.6637 70.7972 15.4578 71.5831 7.90133 68.2962C5.18686 67.1138 10.2796 65.4751 11.882 65.129C13.5528 64.7675 14.5067 64.8336 14.5067 64.8336C11.4857 62.7062 -5.0194 69.0116 6.12397 70.82C36.5124 75.7457 61.5174 68.6013 53.6361 65.0451Z" fill="white"/> + <path fill-rule="evenodd" clip-rule="evenodd" d="M21.7904 41.9075C21.7904 41.9075 7.95357 45.1949 16.8906 46.3897C20.6653 46.8948 28.1855 46.7786 35.1956 46.1905C40.923 45.7098 46.6708 44.6817 46.6708 44.6817C46.6708 44.6817 44.6528 45.5474 43.1917 46.5445C29.1378 50.2409 1.99342 48.5194 9.80908 44.7404C16.4158 41.5442 21.7904 41.9075 21.7904 41.9075Z" fill="white"/> + <path fill-rule="evenodd" clip-rule="evenodd" d="M46.6135 55.7815C60.8988 48.3603 54.2931 41.2277 49.6827 42.1884C48.5553 42.4237 48.0495 42.6275 48.0495 42.6275C48.0495 42.6275 48.4696 41.9695 49.2694 41.6864C58.3903 38.4813 65.4033 51.1426 46.3281 56.1571C46.3281 56.1571 46.5474 55.9584 46.6135 55.7815Z" fill="white"/> + <path fill-rule="evenodd" clip-rule="evenodd" d="M38.0026 0C38.0026 0 45.9122 7.9142 30.4982 20.0806C18.1371 29.8439 27.6798 35.4086 30.4938 41.7697C23.2777 35.2597 17.9848 29.5281 21.535 24.1945C26.7484 16.3663 41.1932 12.5697 38.0026 0Z" fill="white"/> + <path fill-rule="evenodd" clip-rule="evenodd" d="M23.1948 75.8976C36.904 76.7741 57.9617 75.4098 58.4601 68.9224C58.4601 68.9224 57.5021 71.3817 47.1293 73.333C35.4271 75.5362 20.9905 75.2797 12.4314 73.8664C12.4314 73.8664 14.1849 75.3177 23.1948 75.8976Z" fill="white"/> + <path fill-rule="evenodd" clip-rule="evenodd" d="M57.8271 87.5323H57.4096V87.299H58.534V87.5323H58.1182V88.6991H57.827V87.5323H57.8271ZM60.0722 87.5914H60.0668L59.6523 88.6989H59.4617L59.0497 87.5914H59.0455V88.6989H58.7696V87.2988H59.1742L59.5555 88.29L59.938 87.2988H60.3402V88.6989H60.0724L60.0722 87.5914Z" fill="white"/> + <path fill-rule="evenodd" clip-rule="evenodd" d="M20.9484 100.686C19.6556 101.806 18.2885 102.436 17.0614 102.436C15.3135 102.436 14.3654 101.386 14.3654 99.7053C14.3654 97.8859 15.3805 96.5541 19.4456 96.5541H20.9484V100.686ZM24.5171 104.711V92.2487C24.5171 89.063 22.7003 86.9609 18.3222 86.9609C15.7675 86.9609 13.5277 87.5925 11.7069 88.3967L12.2308 90.6041C13.6649 90.0774 15.5194 89.5883 17.3403 89.5883C19.8629 89.5883 20.9484 90.6041 20.9484 92.705V94.2808H19.6877C13.56 94.2808 10.7953 96.6572 10.7953 100.232C10.7953 103.312 12.6175 105.063 16.0476 105.063C18.2519 105.063 19.8995 104.153 21.4373 102.82L21.7174 104.711H24.5171Z" fill="white"/> + <path fill-rule="evenodd" clip-rule="evenodd" d="M36.4267 104.711H31.9764L26.6194 87.2793H30.5061L33.8313 97.991L34.5708 101.209C36.2491 96.5543 37.4394 91.8281 38.0346 87.2793H41.8148C40.804 93.0192 38.9801 99.3202 36.4267 104.711Z" fill="white"/> + <path fill-rule="evenodd" clip-rule="evenodd" d="M53.5036 100.686C52.2065 101.806 50.8391 102.436 49.6154 102.436C47.8668 102.436 46.9187 101.386 46.9187 99.7053C46.9187 97.8859 47.9358 96.5541 51.9974 96.5541H53.5034V100.686H53.5036ZM57.0735 104.711V92.2487C57.0735 89.063 55.2515 86.9609 50.8773 86.9609C48.3193 86.9609 46.08 87.5925 44.2592 88.3967L44.784 90.6041C46.2188 90.0774 48.0758 89.5883 49.8965 89.5883C52.4161 89.5883 53.5034 90.6041 53.5034 92.705V94.2808H52.2428C46.1137 94.2808 43.3499 96.6572 43.3499 100.232C43.3499 103.312 45.1695 105.063 48.5994 105.063C50.8056 105.063 52.4499 104.153 53.992 102.82L54.2739 104.711H57.0735Z" fill="white"/> + <path fill-rule="evenodd" clip-rule="evenodd" d="M6.88172 107.674C5.86495 109.16 4.22063 110.337 2.42204 111L0.660004 108.924C2.03012 108.221 3.2037 107.087 3.74972 106.031C4.22063 105.089 4.41668 103.879 4.41668 100.983V81.0786H8.2094V100.708C8.2094 104.582 7.90017 106.149 6.88172 107.674Z" fill="white"/> +</svg> diff --git a/apps/web/src/images/secrets-manager/sdks/java.svg b/apps/web/src/images/secrets-manager/sdks/java.svg new file mode 100644 index 0000000000..085cedd07c --- /dev/null +++ b/apps/web/src/images/secrets-manager/sdks/java.svg @@ -0,0 +1,15 @@ +<svg width="61" height="111" viewBox="0 0 61 111" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path fill-rule="evenodd" clip-rule="evenodd" d="M20.3909 58.8794C20.3909 58.8794 17.4831 60.5719 22.4627 61.143C28.4963 61.8318 31.5794 61.7326 38.2264 60.4762C38.2264 60.4762 39.9775 61.5716 42.4188 62.5204C27.5132 68.9065 8.68435 62.1503 20.3909 58.8794Z" fill="#0074BD"/> + <path fill-rule="evenodd" clip-rule="evenodd" d="M18.5686 50.5444C18.5686 50.5444 15.3067 52.9595 20.29 53.4745C26.7358 54.1399 31.8241 54.1945 40.6342 52.499C40.6342 52.499 41.8499 53.7339 43.7643 54.4087C25.7454 59.6794 5.67598 54.8234 18.5686 50.5444Z" fill="#0074BD"/> + <path fill-rule="evenodd" clip-rule="evenodd" d="M33.9225 36.4037C37.5972 40.6336 32.959 44.437 32.959 44.437C32.959 44.437 42.2841 39.6239 38.0024 33.5939C34.002 27.9731 30.935 25.181 47.539 15.5518C47.539 15.5518 21.4749 22.0598 33.9225 36.4037Z" fill="#EA2D2E"/> + <path fill-rule="evenodd" clip-rule="evenodd" d="M53.6361 65.0453C53.6361 65.0453 55.7887 66.8198 51.2652 68.1921C42.6637 70.7975 15.4578 71.5833 7.90133 68.2964C5.18686 67.1141 10.2796 65.4753 11.882 65.1292C13.5528 64.7678 14.5067 64.8338 14.5067 64.8338C11.4857 62.7064 -5.0194 69.0119 6.12397 70.8202C36.5124 75.7459 61.5174 68.6016 53.6361 65.0453Z" fill="#0074BD"/> + <path fill-rule="evenodd" clip-rule="evenodd" d="M21.7904 41.9072C21.7904 41.9072 7.95357 45.1947 16.8906 46.3895C20.6653 46.8946 28.1855 46.7784 35.1956 46.1902C40.923 45.7096 46.6708 44.6815 46.6708 44.6815C46.6708 44.6815 44.6528 45.5472 43.1917 46.5443C29.1378 50.2406 1.99342 48.5192 9.80908 44.7401C16.4158 41.5439 21.7904 41.9072 21.7904 41.9072Z" fill="#0074BD"/> + <path fill-rule="evenodd" clip-rule="evenodd" d="M46.6135 55.7813C60.8988 48.36 54.2931 41.2275 49.6827 42.1882C48.5553 42.4235 48.0495 42.6273 48.0495 42.6273C48.0495 42.6273 48.4696 41.9692 49.2694 41.6861C58.3903 38.481 65.4033 51.1424 46.3281 56.1569C46.3281 56.1569 46.5474 55.9581 46.6135 55.7813Z" fill="#0074BD"/> + <path fill-rule="evenodd" clip-rule="evenodd" d="M38.0026 0C38.0026 0 45.9122 7.9142 30.4982 20.0806C18.1371 29.8439 27.6798 35.4086 30.4938 41.7697C23.2777 35.2597 17.9848 29.5281 21.535 24.1945C26.7484 16.3663 41.1932 12.5697 38.0026 0Z" fill="#EA2D2E"/> + <path fill-rule="evenodd" clip-rule="evenodd" d="M23.1948 75.8976C36.904 76.7741 57.9617 75.4098 58.4601 68.9224C58.4601 68.9224 57.5021 71.3817 47.1293 73.333C35.4271 75.5362 20.9905 75.2797 12.4314 73.8664C12.4314 73.8664 14.1849 75.3177 23.1948 75.8976Z" fill="#0074BD"/> + <path fill-rule="evenodd" clip-rule="evenodd" d="M57.8271 87.532H57.4096V87.2987H58.534V87.532H58.1182V88.6988H57.827V87.532H57.8271ZM60.0722 87.5911H60.0668L59.6523 88.6987H59.4617L59.0497 87.5911H59.0455V88.6987H58.7696V87.2986H59.1742L59.5555 88.2898L59.938 87.2986H60.3402V88.6987H60.0724L60.0722 87.5911Z" fill="#EA2D2E"/> + <path fill-rule="evenodd" clip-rule="evenodd" d="M20.9484 100.686C19.6556 101.806 18.2885 102.436 17.0614 102.436C15.3135 102.436 14.3654 101.386 14.3654 99.7053C14.3654 97.8859 15.3805 96.5541 19.4456 96.5541H20.9484V100.686ZM24.5171 104.711V92.2487C24.5171 89.063 22.7003 86.9609 18.3222 86.9609C15.7675 86.9609 13.5277 87.5925 11.7069 88.3967L12.2308 90.6041C13.6649 90.0774 15.5194 89.5883 17.3403 89.5883C19.8629 89.5883 20.9484 90.6041 20.9484 92.705V94.2808H19.6877C13.56 94.2808 10.7953 96.6572 10.7953 100.232C10.7953 103.312 12.6175 105.063 16.0476 105.063C18.2519 105.063 19.8995 104.153 21.4373 102.82L21.7174 104.711H24.5171Z" fill="#EA2D2E"/> + <path fill-rule="evenodd" clip-rule="evenodd" d="M36.4267 104.711H31.9764L26.6194 87.2791H30.5061L33.8313 97.9908L34.5708 101.209C36.2491 96.5541 37.4394 91.8278 38.0346 87.2791H41.8148C40.804 93.0189 38.9801 99.32 36.4267 104.711Z" fill="#EA2D2E"/> + <path fill-rule="evenodd" clip-rule="evenodd" d="M53.5036 100.686C52.2065 101.806 50.8391 102.436 49.6154 102.436C47.8668 102.436 46.9187 101.386 46.9187 99.7053C46.9187 97.8859 47.9358 96.5541 51.9974 96.5541H53.5034V100.686H53.5036ZM57.0735 104.711V92.2487C57.0735 89.063 55.2515 86.9609 50.8773 86.9609C48.3193 86.9609 46.08 87.5925 44.2592 88.3967L44.784 90.6041C46.2188 90.0774 48.0758 89.5883 49.8965 89.5883C52.4161 89.5883 53.5034 90.6041 53.5034 92.705V94.2808H52.2428C46.1137 94.2808 43.3499 96.6572 43.3499 100.232C43.3499 103.312 45.1695 105.063 48.5994 105.063C50.8056 105.063 52.4499 104.153 53.992 102.82L54.2739 104.711H57.0735Z" fill="#EA2D2E"/> + <path fill-rule="evenodd" clip-rule="evenodd" d="M6.88172 107.674C5.86495 109.159 4.22063 110.336 2.42204 111L0.660004 108.924C2.03012 108.221 3.2037 107.086 3.74972 106.03C4.22063 105.089 4.41668 103.879 4.41668 100.983V81.0784H8.2094V100.708C8.2094 104.582 7.90017 106.148 6.88172 107.674Z" fill="#EA2D2E"/> +</svg> diff --git a/apps/web/src/images/secrets-manager/sdks/php.svg b/apps/web/src/images/secrets-manager/sdks/php.svg new file mode 100644 index 0000000000..5e36980ca0 --- /dev/null +++ b/apps/web/src/images/secrets-manager/sdks/php.svg @@ -0,0 +1,4 @@ +<svg width="191" height="100" viewBox="0 0 191 100" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path d="M95.5 99.9169C148.187 99.9169 190.899 77.6617 190.899 50.2085C190.899 22.7552 148.187 0.5 95.5 0.5C42.8127 0.5 0.101196 22.7552 0.101196 50.2085C0.101196 77.6617 42.8127 99.9169 95.5 99.9169Z" fill="#8993BE"/> + <path fill-rule="evenodd" clip-rule="evenodd" d="M26.8911 79.6411L37.3462 26.662H61.5216C71.976 27.3163 77.2035 32.5483 77.2035 41.7059C77.2035 57.4032 64.789 66.5601 53.6803 65.9058H41.9201L39.3064 79.6411H26.8911ZM44.5332 56.0954L47.8006 36.4732H56.2948C60.8687 36.4732 64.1354 38.4352 64.1354 42.3594C63.4825 53.4791 58.255 55.4412 52.3745 56.0954H44.5332ZM74.7746 65.9058L85.229 12.9275H97.6442L95.0304 26.662H106.791C117.246 27.3163 121.167 32.5483 119.86 39.0895L115.286 65.9058H102.218L106.792 41.7059C107.445 38.4352 107.445 36.4732 102.872 36.4732H93.0703L87.1898 65.9058H74.7746ZM114.449 79.6411L124.904 26.662H149.08C159.535 27.3163 164.762 32.5483 164.762 41.7059C164.762 57.4032 152.347 66.5601 141.239 65.9058H129.478L126.864 79.6411H114.449ZM132.091 56.0954L135.358 36.4732H143.853C148.427 36.4732 151.694 38.4352 151.694 42.3594C151.041 53.4791 145.813 55.4412 139.933 56.0954H132.091H132.091Z" fill="#232531"/> +</svg> diff --git a/apps/web/src/images/secrets-manager/sdks/python.svg b/apps/web/src/images/secrets-manager/sdks/python.svg new file mode 100644 index 0000000000..36f5cd7f05 --- /dev/null +++ b/apps/web/src/images/secrets-manager/sdks/python.svg @@ -0,0 +1,19 @@ +<svg width="93" height="111" viewBox="0 0 93 111" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path d="M46.0221 0.000755848C42.2534 0.0182675 38.6543 0.33969 35.4874 0.900061C26.1583 2.54821 24.4645 5.99792 24.4645 12.3598V20.7619H46.5103V23.5626H24.4645H16.1909C9.78376 23.5626 4.17349 27.4136 2.41867 34.7396C0.39451 43.137 0.304727 48.377 2.41867 57.1452C3.98577 63.6719 7.72821 68.3223 14.1353 68.3223H21.7152V58.2501C21.7152 50.9735 28.0111 44.5549 35.4874 44.5549H57.5076C63.6372 44.5549 68.5305 39.508 68.5305 33.3521V12.3598C68.5305 6.38524 63.4903 1.89719 57.5076 0.900061C53.7204 0.269643 49.7909 -0.0167561 46.0221 0.000755848ZM34.0999 6.75839C36.3771 6.75839 38.2367 8.64839 38.2367 10.9723C38.2367 13.2879 36.3771 15.1605 34.0999 15.1605C31.8146 15.1605 29.9631 13.2879 29.9631 10.9723C29.9631 8.64839 31.8146 6.75839 34.0999 6.75839Z" fill="url(#paint0_linear_91_2213)"/> + <path d="M71.2798 23.5625V33.3521C71.2798 40.9418 64.8451 47.3299 57.5075 47.3299H35.4874C29.4557 47.3299 24.4645 52.4922 24.4645 58.5326V79.525C24.4645 85.4995 29.6598 89.0137 35.4874 90.7278C42.4659 92.7797 49.1579 93.1506 57.5075 90.7278C63.0577 89.1208 68.5305 85.8869 68.5304 79.525V71.1229H46.5103V68.3222H68.5304H79.5534C85.9605 68.3222 88.3481 63.8531 90.5763 57.1451C92.878 50.2394 92.78 43.5984 90.5763 34.7396C88.9929 28.3612 85.9687 23.5625 79.5534 23.5625H71.2798ZM58.895 76.7243C61.1804 76.7243 63.0318 78.5969 63.0318 80.9125C63.0318 83.2364 61.1804 85.1264 58.895 85.1264C56.6178 85.1264 54.7582 83.2364 54.7582 80.9125C54.7582 78.5969 56.6178 76.7243 58.895 76.7243Z" fill="url(#paint1_linear_91_2213)"/> + <path opacity="0.44382" d="M46.7605 110.727C63.0768 110.727 76.3037 108.162 76.3037 104.998C76.3037 101.835 63.0768 99.2698 46.7605 99.2698C30.4442 99.2698 17.2172 101.835 17.2172 104.998C17.2172 108.162 30.4442 110.727 46.7605 110.727Z" fill="url(#paint2_radial_91_2213)"/> + <defs> + <linearGradient id="paint0_linear_91_2213" x1="0.866699" y1="-2.38577e-07" x2="51.702" y2="43.3187" gradientUnits="userSpaceOnUse"> + <stop stop-color="#5A9FD4"/> + <stop offset="1" stop-color="#306998"/> + </linearGradient> + <linearGradient id="paint1_linear_91_2213" x1="58.3652" y1="80.2061" x2="40.1275" y2="54.6496" gradientUnits="userSpaceOnUse"> + <stop stop-color="#FFD43B"/> + <stop offset="1" stop-color="#FFE873"/> + </linearGradient> + <radialGradient id="paint2_radial_91_2213" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(46.7605 104.998) rotate(-90) scale(5.72869 25.18)"> + <stop stop-color="#B8B8B8" stop-opacity="0.498039"/> + <stop offset="1" stop-color="#7F7F7F" stop-opacity="0"/> + </radialGradient> + </defs> +</svg> diff --git a/apps/web/src/images/secrets-manager/sdks/ruby.png b/apps/web/src/images/secrets-manager/sdks/ruby.png new file mode 100644 index 0000000000000000000000000000000000000000..2dac1c8da81848acf1994ce7d9446f8f3ac5a70a GIT binary patch literal 19253 zcmZ^J1#}#}vgVj6X0~HyW@ct)rWnRBX687tV`k=<nQ_b*GegYmn3-ww-+TAHx97Z7 zAGN+#)mK%Ly8E<R3RhK@K}IA%1ONcYa<Y=@e@n-IGCb_x^Uh$7DF6Ur2$Yaem6MPl zRdsQ+0@_;w0JQP$alH&7&UnKj7W%sRDYQU1IxhW&kdr9Rvsx>=)<5fDe6E|5bxNKm zs4Pkzo;KbKa_6l|EEjPU0S$1i@f6sM(EIr4<!JRru>0^qG*Ux@-VScj1%`s3<0!{O z*G)iv88{ps8K5*(Axepng3;B)H`=ny*oog{HK_bN<XN2)y|sTbwyhfbY_N$%ROV)k z`xbY5GD;gX!SMMvBW+~MccV~7st#-C;-O(28aqGzs&J{0zNNnP=ZF-H0IYjT!dFG8 z8cUwr8luXx>?1=z<wg~kAXbjn@W`Pb7^Gr9=!!p%u5^6ARu(MxnI?Z?!1^LC`dKQa z(dox$ehaT}{$5mXi&cS}wdZBVX}obIIma8bLRfJ5b}i?~se1wqPR_-g5-)9)RrXUX zu`jl3df%G;=lm3cETHK<i=T`+Cq~g5g?qf8Fu{{w&cDGizWnWOFH2oHD<vfW{a*|Z zfPugOK>bA!e-8iz0RZM-7yyumAp9?^4ng}L9Y_Em3<!Y!kB<J|@}DI8H~(e-r-aOd z{4Wj1JgEQ1|EQ{A68{qef|IPCD*%9m`A>!bWMtt10Fcl?O<gx#B}IM<M+auJFOKGx z%w7&o|F8gpUi^QdgQc4psh5L2$d%tqi2Pp~{D1L3)hy(s|59<Y6C&4DQYDpebg?Am zW@cq(B^O2{B_$Pf`C`SdE-C#V`rj)davL`{Cw>+dPft%~PYz~B7i$(aK0ZDcR(2M4 zcBa1?Os?J_H#09LkSoQ%jr?Cbl9sL(E<h(Ypd*O%AG>Dej_z(k<mCT2`mglwINg9& z|LqCn`k!X~Z6M1(5*9XQR+j&bX6Xg||Iq%C{G0Z#cKzF(;6KLrRa}6Ue*^v}EMYdm ze>wPn>i#=C!GEmqs{*|&?R6!A4wfL-e>m*y?1C)+tK@%W>ivJ2|3mU0rXb5d{q#Tj z?cam)uiC$(B8(`=@?Rq?jMy}ko&x}g0^}scG`%3!I}x(1bo34b&;3(Z!0kR4ZZw^R zxERfO2zpuHV8!qu5u?P=o+4v{@~&qf5D;x3t8?|<uRXu+<gudc*sO`-=usm<VARKb zD-@t4I}+;DTAFR@)VjF;c+YX2?0oRAXcs^r=PfzjXb?K&8@mvk(mQW^P+I#aDqaVb zoTnJf?tN#mAD}1R?DTaSY<6H_(xMkpSjMRi%WP;UI@t57U~?~zp5XdzoR;QPH4?6E z;nK%NL$l&Os1(&iI6!A?7OjDKcJSKa%Cw}sQl(u0(_#faTdDHMWIaD?T(IRqaP~69 zY)`RMN;Mey(k8U-Hgax7(RQOJfJ@x=NURfU*)iF6?bu7_TqqDpmrM1&smXaJm2Grf z-QAT<$(fviBk@S3TeIr}v&3C(!%U0(sa2iMeF1Iun^Z_MXxc+b)uz#n%c!Ai>o~go zcl7rRyYYusoPao}vZQ(Oa9_7wpaxcuYm7RVJQI%*gx1&RybAOVRa=&|m$Tz@5#t;` z>^Kpv^sMfErpbjGig(8>hE7;C=bXjT`j6sh2#?+9GVlQboT;8A!lw9OK-LLY^>42( zhGd2FUt~N2K0g;goGWro8QR)az+|0D7X-EFGCH2X$2_~3*f#2OQupcr+gYx7$--GK zr=?C#eZho3D@aI-0%%-N^4#7~<b1o@aHUO|pwNIuz%)~w{fyW#vz%OfF`$x}XV`&E zj=1M}&Ih;^Xe_|b@1D(14!g>qzb19#h$BcKiw#wS(vyOIUw?mkYiA3G+QG|rJurQn z1GoOT)O~D=Dm1Tnf3Cb%s-OilDzeT#p<iWjdebr7hplOyy|Z80DP%txj%hs@Z?sbc zqy=+@Gx3P#WRPYt1TAN!L(tA8F84q<*&gGV-Bh(_I&iH)veQnhCd)SVNMKlk3!|0c zRwQ-w<rBXJ^2~X}HjN$mZn&F2*U%M%Lw|0xe#3gtgq$iIe;6voSs2p_8(Vu_vnwI? z`E&dvgqmgY@_Yum9YO_rvjVmY2a@jrw;2%%6%=0Tsnew+r=<!p(>|=KDtKNegzMK! zoxMe4I;|`3{?%Lp7PHi7_=g<^c?g<?(|`fcgu=5a!0D4K2*RCzNpu1-955D)2^l0C zKS3SdvJdB^*~u)*s3FF4kq}EWam~`{>*wb}$Mx)wY79{eYAUTeNxs`1uEq!7+#j;| z?Q8QqUx~Q6h5?~6>im@Xl)`vOQ-kL7@S#(lTeynC8DHg(FPP7cXg=RH9R@M5c!DNq zgpWoK9X&Z7Hcv8K8;YgVYt#0m!V?{EMVs@1Jn@av{S$1&%yA`(gw+YG5Q)LiztX{Y zD7wd?K_wWFKV5~2II|sEE*N++Yg#?*&sBMXHe0rsig1P>&W*bKZVpma5AAHjP`muj zbHL9iZ@<IVmrJ@tG25&xjp*c3vJ;E%^loQAEA{*m|3;7RP-BNZWD<pR1^gg5q(*7m zLpbElvt>90xg@r1>>WpiFad0%6n3Bqnym^oN>mFHLFap=ndxTZUtSXzu}l!md1PTJ z;emjgyaNncIgYJb4m5lM2B3br>-j3MsOzLN>;MH<tMvHJ-x`2|^{PufV!`h0?QJ95 zFj%oZzn3yI^`7Xv_jcjI;funPPDNdGc1SJfA7cl51j!2TYRWG*8+5n}V-l~tt(1CS z<$U({<~w%fX;U(Jg~fb`$ZXwL0G%6vjqLJ4yLp;6u0Ep+F(j^-FAw1K8H!@&WNXg# za`kuVu=#zABQYd(9q3q2fL=`!p!V$Csgr`G1GFiuq?w$I{LI#Ufn<O|vsF7~w|H`q zm*)wwrbD;JB6Q^$*tx%ml-hD0a7LKX6cV<&K$IyzGPK>ubtgHSA>&U5JbrpT<nCfr z+`b}bEr<jfA|MEyCm=PqKY<^>b=_~9)ttP0+8wvppx>xx?8QZ0#;LdW?IQ&;>+x{s zo+T*4+d5D%TLU_?MV|}#UAsLEy6QfsXLDQvJD|zqlb^J~8=1waO$RgqxGuwcQi#x# zn>5lG#n-0#(bBsmVwWmux}QqDWYmmeJ3joQ%QCxCv|$bPjRUJ)8!VWCi<OY+Nxfp% zNfpwYQqgthjKZj6NBl*sOb-RZG=}?axZsJv4=Ei2E7E~+lI6qufIlHs!|z+DbBjan z%;~(xvxw1^8|YDR^r`eU{GEuF!R8dz(gUUH9AyQ5ZmX;Z-gLL+Ny>4o`Y(MnoO4<v zyzBv)^E8-s7oBvF!vv>Fz_MF4(MshD|BkDki_T4<*MpX}p^cED(dL^p1&-GxWp#lJ zQS6A~twJK`WYC5r7*AR;Wo-v4o{?!bbgBKo2o)Tx^U+!X;!Q=6n@g6_Ztb!*ndkM| zdF-26?b4Rxc5uIgu}VrRN}k3xE!{Id{^DB;os<GMle-EV)l*kLdY{T-*gF*?F)Qw0 ze8<Um*sS47vI#Zv@L4eEhF-L@!nm88E>+~$&y};cOcl;<=Y{I)<)w4k@>L!hAW1`S zdcnN5L}v}-J}RJW<mc*Y7rsEhq&Wh)cdftEOvui$(YZbiwc%7XgV9F=i+T*T*+PE) z>|)?Y{TDNv+WTdbO<mg>-FU{N7sF_xH19=<8@cpBk8}jNaG~lXBvDz1L^OK{hRaW? zZBV8g)OmnBI4&qoHBYMd*Ril?;g5x~Hi^x-Gd2hsF1h#x*4e{u;k)I6Gq`|{mQ@*% zYuuvIV=*24&mL8E{z-aHkFeU1+zhX_20rq{gD?snk*sPojTu#=*fbRSO_U!Aum#&G z@^>Qzgp=AD3U<x4_rk@Z@VbQNK(U<r+a#92rKY1__h(HtUC$!7Xom7rt6M82(nb_j zKj?R<{s@|gsE#-qFsPOhH%H}xkkgR<C`QA!M3lRz5?XGS*27B9Zw9@@T;O{d_4vp2 zb@*@6d{n<}F$8`kLRg$#<(|Q#G4#D=O-AKXy{d}ezl_d>Xn+oqmTW~X5e|1mfQr+# zL$%kJiT+ZB^^`}wU=zJtx$}F~trE_%u?IJ3`G~{#?)W`l093R7I$B}i3WuUop2^+1 zNT=D}3^V>iqC^djF<!_1QPvF1Jg-EG!!lp^v;YC%UWjTM^muVU%$jqguSk6J+=O$| zN0bbIan(ON+iX1-s`3R?xB2L)|MrWqi#;o0nEZ{Ghf$W$H?f*A=-onp*8e5!5)TK6 zW&9};1j)qYmhI<aT*XiLF3*T*4`E{rB4qBYd>96S3!0vfQzw`1Nj#3Wehg-x;oL;K znOX#Cj}*lAeX9LKvZFTWGBl4~y?=vjBuC;A*xA&umea=S5%g;p41(Kg#8Q2D-JIx$ z3*1TJc~f+Kb>%4`SY4Z^#TcgNll@L#6d^6}fJR_5f9WDm#8f0+wUnSrKmUhR0F$KH zZCv;5anHva7VVlXbL3MZ{ZjQ5`2ID^uC9e|Ugy~R>ZQ7Bv(82DtgibpGbhKLK@Vg* zHvU}VB2a*y%#1D4qO_f1l3grGSuRA!!BtwzHPBx=_O8{-04SmA5m8+Ceo+9m-y(WI z8nw0iC)&kcP~c(NFa}duc)9)dHvNJ8%T_^7A9e76up>=(T+ztZus3`ugy|sIn3GIN zfXSdgc_{~q?4<iiPiU8;XT?>JA}zDop$_Mq&qGK4Ond)H#K&@FGpp+CX-wrINt;=r z>25J)R47_ayWOSdrwAPL5Fs*TY+uUCtgiP=H-Q&GM_TK;v{L9aV<M2>Z`Z`gd-Y^v zy0n^@RtGlJ@MJH5fG$XSOy4(DKI}f~%+})|o<dR7JdOUChSdC<jO<97KuIc3<OO7H zRVYon-z2PB+jEzU;msqaAf)(?EoAJQVz6%iL5B4(RoRlfKy&5kng7E6%|qb!q)UY+ zQ^`Vobo@}E6XwNEGE^%bZvm1P8GeJZkgZGRhgJ^*pe5$eEZ6_GpGWZG-EMRB&#Lr0 z$L=XQu_fe|fi4VJDq`et7|sgsKqRxB7heisdOlavc!;z3>Q)U>Nk3T)I*zU@i@h_m znKeY$sp+NbfC^dbh$>b_43;=Rj!I<#d0Co54_vw8dsTfnF5>mkwKzVKZtI<g7!*t< zxIdc=h4U5}%t0`U1C+CVORyZE#(o0ZBKA0B^7&nIpuY89oO<oW3a|aytSee0*o?(c zt@C4x_&&0h#O-RG)b$IoB{I|Vg{_K15%P>mpVx=q-u3V{<Lj5JB4H2510x@=UL&u^ zIM+Fv^#K{-9`27gyD4P|tRNs}hI7?|srKkzU(OL33e$XHU*U?Cwe6h@vA_(!_p7E# zQHyvOtzz6S7RW!rBf*iP=}LT&0aX1pQZMtKTGxm@M!ZkIT7<zbP~C07&0)vNhqE<x zVDx#@;;gMAGJU})qYGwj6123>Z*Os4H@Q41nQ;9iG3A=lC{%^A#cjH!n|h<_dY7Wi z$G+(5t`D+9*rhtY=x^>pFi!f~v{4n<Ye<mOZKKV163Eo}H^akL6?C1lPw-LXq_!bc z{oo)Z&P&$k-U|XRn5`TUpO4Pmt_KGZu+OdC1J84_V|k<U>BMl0c;@!_N=^S85oBqo zk34;&br3>ttAWQIgHhj2+Yw7(s8X)6Rd1B@>AP1Rxv8u^U{b*ZC65w~kIwP*uuZw3 z-pQ+c@`|DxKFSo~hcYQttpb$Hi<i&r^k5fKF<=cfcH<*Du;Zr0gv;x=7b-f(fpAQT znmQnRPYd3XvtTr9<{!_BZa)<0-JDU$9%$<bF2v<RS-^c0XMt-I8*fyppf+gtkxv>4 zgU<@XpZO`31Ikp9hM6)IR2J2Ujv{PlHHhh_E^>j*IQlWayCP-~458Je;stKtTACMJ zrf<G?9p4iXic%Of#ZJ)I2nS4M^8~zcu^eXx_!aA|`zW{ZO@=5p<K_u)sIux|C~Bp_ zTawQy&Ed=1Ll=sccD(r@AEQX;2%B}-3A;?+y@N@dLROo>1%X#3)xt_eR=RTwcqd== zZ)b4|Spd|cTB7^;jP8jJ-aFz`clJbzCofj|cSlz0GoO{Nh)CVP54M7~e*L^+q4vg7 zRU+GJ1Oy6UJY3Zn`IYU`IW^Q<`j>Hy^kvSL*Djtoxiu1Pv*TQ3D}c4y<_e>aZoZ*; zK$H&g&awYo7Z@nME+q7xIPtz82w4t%hg;h6%FE;(gf0SMCfJWllz*bdhR0+aGv>Mq zaEuhU7O02{F?dcK>V3NJ8~HAYkro?L0-<bVbOaUWV%xp_#a`r9WfOX<hiOJ{u0>g} z@kN5Cb4w5OE(titdI@nx#;vc5qLZ!zsEqihH0~5Bn{z*mXMd;bdihOat?#4LdcRR{ z?j0*Uem;`q`{!q*(ev6w<%#l)PpLQ>-PbRmj7|H|Lf0(YqbO;V>+zj%&GfA~{#jW9 zl}kMF!RdOKQFztfbAa-;y+eEBjIGEe(S?ILyd2>e3e@?^N<I`x`%^NDlPis3#kT=2 zD5pNf?~y}m`8`Bty$wH_1zs6}Rb;Ir#lWhozcHaJN0#%f)>5`_pyv=0yN_)?yOxd< zo$sYOHWiH9$Y;LukYO>Q)yt+Bq>?N(#&EPS;fiMs3C?_D&EB5JlkK;Q&#q=@qR;eo zV~=n^Vklv8jAOmlgU%X_oDMc#jSi5p<p7j)%%hPwVOm_g^>MLy&c$Zg-(_`^IjCGp zl9tP(o6QFud$S+l!1MC=`Q4hYoL|z!Yey@g`+<z;=a=;kI1>;x%-N!w-f036zh!_< zU31xmma?qHK+xJMxr&~Bk}SIg5;SUg$rHmkyHSgOAkfOU3ys$GK?@Ju@(wYI$y%C4 z{K!u7AcsNY!+|;28l+ajU*`_FzPl<-!xvsXcMbwP4$&m1s>%7nDNp2{05TkVwT`0e zWcJUO)|~Q@o3JtFUcF3!&XxHFPK+ONL!ux8qB6UHmpnqBCseQ&i&uQ+>w!P$*|nwJ z?NHi7wfK`Dl<E^5Q>vJCvp18t8oRU1pbb+r&G=3-jhoNF+~G>H1F)j5MXNd%tS)uT znkqcCN+}t2Mc*|KQWRur!Vk7H@g%Ycd|}zXYi8-b(a9JVe)3)dTe!}0ZV8^~>W*o( znBsh|5MyH#Tp6sr7^srQQsL4&U=F?ev=TCNv+=H6nufgQj*E%=XUyk*M88@AM%q2S zYzj|BCEk|P9p8az!<n#Z5Y9cqfaqt;1Y}VMUs>v6tMoluIegwRa{<52Qd7&K-*JMB z)h9>pE`)3qm(JH5)c4eb-H*H2*^O+2gvF6gF)iWfebG30Y*yZhA-nG=kIZM(>Umk= zL!umz@JJYGw>2JjG7n&E$49xA^Y1KHLOQMmxmpWp^scu0$dw912FH-s%iVn9je~?$ z*9cb%#aeEr<g51`c%$5n>0jCV&Q;3qvEb`bpnwUGe?OI;8ixdfhte~Lz_Tt-`<w42 zf%a=34Bgu2>ve-oLk|b#>M&m~6{E}4nSRn~^7G~jqGI5Qtf%ZK;oWfutdvI3H)>fu zd~2QPzKpE&8v|X7#oWHVS!b`dZ*bwhg5igz(h|d(nx_;HV2XbJT+I@jk|ISe6(#h3 z;`C^_%_+{Vv}Ms8&B6=lm@m*M^^hOeprbP29CwzlS{y0iJWxY37>*|4tH<98aM;-v zI*%do)mmu?KDxU_U~3LllP0k6v-GprZh)lM9kS_gtphWh;R925#GW~#gX_ptLCpBN z7ss(gPKU20CN34f)#C93w=(%7w#$?0kz8}Ov3nEBIp~jrD4r{~v125zq%qgHr5ZU0 zz7I%a3KBI=vthbRRQXuK`8vNS{GI+aN~T8e)u~oCNbRm^K%sUm-j6L+S(w^XJzleP zhg**d%2w4{2y@_|HK^-ve-uGr9R^jfzz1lpA>LuF+qQU=!Hs0JP`DkscBrBn-RT}a z;(p+RILW7uUaP~snOLAO96dv<^B5syaY{7bNd({|uHtK^!oxqsY-m^b1^705*RHJ! zrfbKE7KS~^dyA>u=0JL}wov?8o{|ykzGGPGo+5hLS(1NSopP-Thk`Y<*Tp7v-Gx6{ zEZO)Ki~wVl<Wa`DpZPs;ajhM@zrY~6#Xh?t(6`(7Aorupp!o)}AafgidcfpLepsKH z)#odz9Fn31RHn(1#gjR4gld<rv;=}URshVkBXEF&ve}Sn)DST_zG}$=qO!3~`0#m@ z$un(I+tR6$^HgfBgwnh2bGP@aT}#ns$GazChyZtkN!yeMy}6qC0r}(_Zx;<f9X2!d zTf(7bA6HKY%9gg_jm7zEt`Ahw#^>%sknxnbC9}c)3Z@`m$qbXXW~$dXs(16}xYYtn zOjPHHT0N(z8}voyfTyaUj_eMWLzZsj?8Z%k4L9{JSNp-FRfm|`);b!l1GQROcb0VD zo#};Ku#XOy{d9DKE+*>+cj_xVqa~VHe|jdGUR2=hIY*(tx}fYP8|*c!l(pu-eR}tM zW>)`~K5?kAz}v{&7JgaF<FcSA#}@z`KE2916g8idaO<37BU|)ot(~}4GghAG??pUC zcaVkvlYZVy%MbYeO;fV`-Jp#4>hde$P}dT?#KTEM;^f&`(aedx6~9d*lJ~e>n3=_` zMPz$!67DjMss-Mak*!U7T!&;KR|aDKcP2$=l@+2iMyVRs^t4#}bd;62mZryFuq{FJ zMqd4J>!XKndBKIlnhh$SJ$7^7WKyrozh*0;^FQ;9g;3?RyWK=@;Ggfps6OoGye2g^ z28*&0tU=D8tok$$iJaFCyzU)tgTVTZN8@|Ym?;e*a3ZtIkKPmS2B5C%^M?;(JEzrR z^$^!&MWOJo1um*&wP1)wFA^r};-Mr->CfUWH@P!JKc-||r2`mIQ`o@Jtz0#}O{-T! zAAfENA<g|yd*KL%QKM7o5A8aAdO~oJ2+qg@ku>B@;JkPD`&|G6{Wp^*E$n+s;zA{< z-OCWrUL=P+YUv$pC)68$+?QYHj|Oxu_TK%wonP)QnnLocjt~-g@9@eL?|Mr4b3X;) zo0prhmS;JJHU!a;|Co~Q=FKVov%R%~cd^mC^K<jvU+0*FwQTpYAs)3-#5yMQC^I+P z(x-=Ex`VxVNGqj10%Wq77MbxotW*iBT)`89yrs{gOzFGCIgpV&a>75y<d#8+MPP*< zYEna<I5?Jby}3~NV8Zuf3O5&gu~@@HZ}7V%>yQE7N%cTm_j5f=)^`{Kg#B3RcFPKy zk+fBT)WT@a;DI@;S&wg?SK&|isEv|f)Jv~=*O^q|i_m7!)I62jkn?^_wh)Dm;cO)_ z-+|AlP_fIrs82I-@J(~iwVAgeF}0O~QNEWjN5K`<r-hL|AtPcMo$o_c$GR}EKDOHe ziKkM<r0x=M7thZE-Pa_Wr_b6fDum-bg@F{hR*CI|t$%PWOg`mGg$bmzi_W26Brf}_ zdC7ul1q;Kstu)fyzBFW7v%N=&JO{21k-VexlSC86RB;p=lw+{ORkoiZ`WAF(o{32v z84*v(VZBG>d3VFL&?fUu=h%5ZtM-_B6HHYstFx{By1a$_{7j~8r@lS<;zm8t6mS<m z^*Yz4%HvIu6#IPnCl?Dni#B+$77y3K63x|U@0_;Cd`k4Bz%?W8zOh3}jw}KDuziKX zcRjSFW5($6#<A`e7O!EXnTU4hjepTC&28N>)sB5eJ}p=updsjc9aEyK+180At%^I$ z;LjHQTIa#mq3ba?botXjzOx0SZaw2QjNQFwpJ)tRHC;Xw@>s3vc6qq#-hH0bOXOA| zy0IWC<6=YSozXtjvV<;{SB>&*PsGDkEkb78H#$XuR+uei<rd@nlk#zieZGVG8m!>` zAgaEUA${liK6?`Q8l($RtJPuMBVgd*&nS9P&@gi0N)?S&a9qY{Nts;dw}Crmz^L@; z2G}tLPUcqGJc^m$8PFYWACENJ5-fkD>ZMp4T}sP*S`Z5K-G%qB9~qSeul-KQBmCS{ zPR-2j{Cipn6N%s#e=zAZtjlZ|Btgxem}W@aHmKbgCjx;pgMEFOw7g4Twt~p5b+z>! zfq>{dug7$EM9~3mPkUS+jL&aTl`{6r4r}g`i*+M;oy!EdcRkkYg#zMHgs*DHnmOj3 zny@71F;t+Rq*Ap>&8w0DIjOmp9^dP5+n~K*%6x|?TGnohU*5Y34KG5QfUd`_a_Jbr zERAh&uUhbqWT61_vjv%Xz68+#w}Et8rU8!<oP(#IsXb><etOa3@=9du?EwpTn`p|( z$H;Dbb<=GkkzYfin+p;hB2v4DQu)bic@(Lci~3@H0=LFz`9zW*N&59<+K#g_p#+#I z-r<R(utJ1yi_NI!k%>nQmhZ14veMmMP*}IcHHI{V6eJn${Oz~YWdI^B&F4+i&-{i6 z(1=}b77Swis@_<D$P_AdXDgJlrd6x@uFMm>nGW68HapdD6tJ`$IAC=DTcxcj)dV<v zf6(R3z@k8Gz8}H=<&--ds*%>;r=Uc=tgwQ+T8UT#oo`2xO(1HN$+9fk;_Pb=ora09 z@}6@BMN3VEr&O+W=G<YORO*VFoq(DIgG=X1rQW6aWvS#EEco%)Iozh#@Ll)W^HMcE z7heU5R=)0ld$t^D7FcZ5=2xk}<ZLOHp*F$aw>nR}HIyn4=O#~&FKh=^`}g?uPHf<~ z!}CK;t%j)v&s(S6=k<#OdQ5bs_Ck6p2YOSd69jQ}y0&0>0~VTw5Z;ggO57P!w}*ny zC|~yvSLKU!y{paDW#hh&rCiZPHp^$f>@&>%s$y-@NcXEy)HL_YTOjQyT#0hK;PaVU zM|iQG2=3}qcRl!`4R;c$YI%&VtRnTjoTrRajXyd6>lgkt=p2L|@=lduA(+<uJ5UZ$ zQsnzEGHCcDFf?`}%PsNa5q(LpJ=VfMq=Q<ilfr4am>F3#1L*_u>7J&kF!1E~V0@S6 z5jsUDYLo)<aJ@}LK7T&3tW-5MO}bgJv9sppeAiDQEfe;5pGkLu(}I6+C+9Fj?Klm0 zf3omg3JG(!rsJdgEx?y*Ld}|I(AcfSybe=(6@ccNYaNz^Emm*yhuojKO>%&ZqPF&{ z73i}Him2$8!Y8^|uLHhGr<pKEC*1efRl~PG5IpxYo4XuQ9pXdy$=tcw>|05Z5Qh#( z{f=gwh)5T+zn-!0%7#@gi-NGk7SeVVV3HSf0@@1VO;DYvaQGpM_ob{mN}`VTBL&Hg zW|$MLQRz8C!-X3aq9<n&h@BHO(>-sbV>3HHwzW6!CyJ6HG@yh-4^Ss0w|+ved`kR0 z&<daCqX;dhh`c4P8^vL8CC?(R7?#<?0a+?-?Wgdp-JTKjk|^Rbwby(6`YyBnCoqtQ z#Gg=RCk-^2Ud(o|SUEZnRmkGHWA#;)nQAN)PU~~?u15U>6=wP|`kTC^*?<gAd8}sH zy?+xbKCr`R%YseVKc2eX*E>I-);-uzJNjmo@1KVoJD$b+v>_M7#-4P`C9CasE77e` zyp&RCMjk|zK-oONgn*&Oww}N_5Lr@wT(PuI?IZ^PW;ODUYro_fc)jMm1dF_2dTvj3 z4o(D|1bFVCZua(N(!rF^PF#D*WKqa37Nc`wXGBe)6@Q)8dnvc<oSZv&b2V5)m?w9V z->K0xe;p;C@6n_~E=*ydbC7n#w+wi~!nGqFZU>cN;_J(kd?6Ikd4rNTgpc*(9SZvp zipdVW3VaW{Cnf2rxY_UE#zvQ|`yqv(S-p-?*zOrb05m1uO{xx|9uT4e0MX<OA}=sF z?}<E8{dQQ^CjTy1Dy{p53EaPQ_t?As7=bg2TZ&#NU-a9vQ?lf)hA)PBfyL>`*u2wz z1WgU?R~=Dc`bjDXq{$x{^n$^!Fk6tm1*rrM&}erV>2SLE2OWEng4k;!xf86|*wGG> znEuz_Gya%!9+bIqW^`W>ibU_i4HLI^A9cN}mOb>{7><IM_?7liErw4YYZX=Vi2!lF zn!)sPaHTUfpPefpBW>U#W)b8{Gis84Std=cd5BvxAvqX_;@P#4Jg=3kr=%L64LQDj z6}a~mIZ4RBq2&m2p5@y$6`k>7bG_+TLbm;0|2fL=sUj8i(*sRnnAg{z#zlf`?Y^nW z122QcfY1`Ua`lfmtw>*C!RcO_S$b6m?W%%iKGVG$*4fho*V{4Zox6mP&X^*Zk);UR zgYB@>BB8EpD0xldk(;50NW~L<-q4)FEdCyK$Ubuc_hG3xoS8VrA-Y^u-3r-*(7Z-J z3Ysnyy{Xl^{mk_t2G6#c!1qD{A0ZZr>dvG=G^fj)R_9-${R|fGHgu_Y(kKwJ@k#iN z&p!0@AsyW3;VUJVJ!vM;3IJ<As7@8A_s2|Hi;+4xGtvk@F3wvfCU@yFZh#MAM-8qQ z*<0p}1?~=L^1`?BTRj;?y?Lbu<a%MJ+WSHLzz8;#Dv?H$K4RgVT#2Z9)~x$9nwvfV zIOH}+&7J-Lww&UdGP(;_0VhxSHT&+=2p`87eYjS;y$^mm<mW83>MQ*{anEx>HU7a8 zq0_1+URu|c>xpo;F81N?uORJ4<J3jvmzBT*IZOXLT|3fOF5el^`&iD0r3aXT|Fx3w zdnU}>mu6A8N`Nu=4bxVvlhj1SXIaY=5#HwMxJ{8UuBo0tkYM&v?i1EGT<bL0!at^S z5Vp5rtwn`%^+C#lmh%ck23q?46GHV<RUfD<t~%?N=-g<FU?`Gz3D4IR6ZfJ>7MExs z=lAB0Uz{4T^rF88Vx3vJu>4zf(pxPB+pXyAa7KVlHK{ddQ#AL8@P3lOdF|%43%*B4 z83HTJos6O@PMmMh@hM&uWG2~PQ|7UOf|dceJj_tBHSd;NLV#3;%qD`|g*3PQg;OZ` zyXBa674(SHbORJJLUv=(@5lCCEV2-c3DRYPgtAgwTH<1Pe`k}my_Nai#9JO6huP=# z4*@;NkETbvA7`HHSDqci$u&MGJ#1`jTQ*ow!2?8j_zZ!OQaTRe#6Ks33jwyO;yxAF zJRDjUxg5&+re=mXP&C?C5kf3C73z8&K^!#;iTghcv<-9{xo@$4uLoX?T^(4nsyVEj zGg#l!iNumfFGuIj?P5HK*+*T|8w-!8iavM~D6?q5_k4oy11}UPJ>|z&&A8E(HS*fb zlZ}o|?f=*i8+)1UeoX^kmIMq57hFyIZXPMWPX@a0k1@Ns3vUn=QLlvNX3TyZUhM%n zafm6TaMpAW`laot{a;^$nNk@wod?vp;|CR(FxTrB;H5;WeohOH`-bE~gd^(Mh$z@4 zDQujAC){j(BVtzUWiCUb`M~GLGiUi`_we5Edj>(y15h@MN*#hXe;Vsq!I_664YODf zYKE}I@-jhngE6yOg|V}X2kVzfs40`A4h(@#VK4cF!pDmyJ}{!u;IM!<k+wRKr?d(| z#}d1&y|Ex|J5b#{w50V9KXp#|FGqb!`zQHoWBZ!@Ia+g2hrI|@rog0uglzOy!yUoS zA)$LbK#i0N-FN(|!&UFF$)19KLazv}OIpfYa;g*rd7w?YNohS(;b2jeW$$4|<t^f6 zughRF0`@Ph(jUn1r*mH=v+8zIAY^5MvYP_i6%v4!8zM)~)c50>wq9gHqq9HgEiXjf zr_{n%p4Hct4%Nb==PHa^w05wC$&>On-m)3~#vW7Vkum3Z3oV~){Ps2%(q;43x8k92 z*Y(<&x6FTIIdYogt+6?1<@S?lU(h2(MWw1eXJ&+7>cV6?upfAjSR53T#0XlRD~-OA ze(UlwwIj4c*c4EdfinGqJ8Gu1v`wA*REwhd1$r8BkO_)zAl*c9q+$ZsVs4wOdT5A! z4znc^)y?E_x270|0d(IJ@G!yCJ(K<X%2a(F#Z=Aa1AkWp$3CR@dD3P+SaY&LU1AIP zO4N~He7j<=ab%$+6iq9L?E%4Sf`;lMt??t)%+D~)UzHoiH1kL_w{@4<XDt1R><oyB zgPyhrk)sXgA(_`6YCHurGgc(~wv$knrVvXQ0x&hjX$|#4x;=;C58Bw)CU?hgsDnN* z2gEmTBm12PtV}?xHS=_B7+sw|v}$d?1(!LVIX`r6bNQiKwp5%hk4xW};T-{T@x~CF zvv6u}nTAGUq_8bNv0R65|2z@9zQr{BIac!(+5>{(oG+W#{h7*rg)Mo0u%n2(z<@47 z+H(4^7LD}HDMW6=Q4I?!79iXVBe8K`meUXU_^4*?C9>)P<&xr3Rs#l@Mxpa=Wb;Yx z<KRKvahF<UiTx1JqTOnS)2`p$ys)~zl4k=NKXwZr<h*1oeU1JSTegt1e!ije22X#B zptIpY9P7nf#I!|jy6h~U^IdQB6=y6xjMkf&zzCN14DE^bbMTGIICj*kiMPa|S-dYw z5^A0F)a(#Qqlx(JiBA2bqbQ2I^{GXL`lk>zFVGefO>K3<YWGim7yB-e2v5o_h!?f# zuJ(hpv^8QBZ?|W$IKyA?8;>*xR66}kcIWGjy%mzElodTzw{XwdW5Dg}?u(r7tkB{5 zg<ER(6YkotlpBs_8t<ds4Fwjnj{HN1^<NiHpMw!%g_<bQTrKGt0O&WUy~6BksfZ1< z41{!KwLs;qBHki&6(2A4eTQ+rfB=OIT7p4qA{kIA6C+yMW<#@+bK>(k1aD^sV@nio z#>)4WDj;^KLAG)8&Rr+!y{yVP&;^4xt^Q^0upCJuIY8QyM?~PT+`xm>UvWz1b4Y<T zDxnH9GoD6)iw#D9h&Pfp^}CGhC4<fmLgEbGawBY*#uWQKahNw^EeS%dQ>Mv1hUb1$ z_ja>!)0oX2XC~(O`EnHx*w?f37rpL^3rd(}de-CNs`R0OoYHqP!v3C@dfy)tuL^vh zDpXY>b<&Xl<uIBr#QH9<v1{mg@L3PJ#Jsx-WP@X|u5InN&X=0ar{PJIM0=JP*2+x` zFQ;p&nifmnm4HPI;G@puXS^|da@--l^Xa9-XHUbeV&y9%$Txb7bVTC2BxMTIR?6UK zvbvvYVu%6$UX5z_ZdEzLEwvwcnFomvgf*=LM(;76ZPxF96pSo`;EGZJ*ho%c%Qjsf zmNiZz^Z`PJfqXqgCE~Xq%cZ^AQ_Wf8R&{I~{3Ldo-*@&bBVYN+aVyN~mf#sET8tq1 z+DFK+$i8hq%(RiDQkF>==~s5A&)W$oV^VlUcFK`-?5FH4XlP=&coJ2%>hEj#b{QGT z8oV82Bh6`qd(ca7+$y*9w6<Ihhy{H9N@dFU;}E{c8OM`3V&aB9kW-qiiy)yZ$(ds& zMBQVg<jIRWEPX8l_cacDiW~S_=w^_`+5h#|x2X9%#L=qG3&#(B?=6C%jwT9PAK`4? zesrmiFB3KbAlWDgD?Jwj#7|cjvU_iJ@_JZ64*t4P|AU<{1$)S9k9r@AaA9M_`}5DO z<2AC1D!Mqd9dnOsw~tlO(T^2;uU;|&eHqa%q1lwHOPbFvXVVtRKK}We9|P3%S0{ld zuz|NUYuAAf<UIFJD!*B@+bkhfr|zJ`-|Mf#*n4tnx>jfey<-h9BP0#1R8HM*WE-rc z3<9qg&eKKSI6yDexb5`aQxv<y%%~aizI*qpggiz!E7zBcN^<ToQ}4$|JUu`~14A<l zv5y<2FTx9tij8F9jKO(6k`&AVB_{j5I)C6J=9p`&{NCjVXCB}7+rGT_a1IS#62tld zS)Ra3pXP4W;1=f<{0w9OYMI}XA-yUjX2v*TMOGBTE*wg6Z&vIrKIlA;52nCPey(<3 zmSS7We9xVlpki<0@eL3o1e&I$lx3yKk7mOw>8!#mh7B_}$x1m4%j+q?ITy{Hx5H~~ zEKh1J_j*&4R&kL1h&0Bwr`(ZnqWnHDXV^mI2Vr7H;SC&}dj-8hIkB2W78V0T6A*dB zcBdFY(s-GaUY!fhQ?!t(8CQb{67kcc3V`lho0SlCI5hs?NbeVRA0o;Y%_8O0r%Q6{ zs}XrK4rQ+JCgjT9)-V%T1jTxPC1<+S<2wl9<}tVL8>QB6C)};+nQ^TjhfLKSF3AP~ z3z6%m8;;=LVGoy?<uAW)_N*swe@P`06XE6(yPhS!7_tYhX-VhoKL>8fx4b8etzEk2 z97^ZB8R{4bCnTp#1*-ZZGW6)e4Lv~LmYD~j|0a4GFChxUF?7h-Eb7n67i`cJ7wUpD zAhq4n)+=D+Dt?|5wE~(`LS^Ic?;C9xII*}SEY3R(&1=vquFk#=us}H9PW7_APyf14 zoiYHewlvner%*WEr<|ijf`bcRP03n#5%`iJ)JB?zk_rv&IN{2?Mp9D+a-GZlpOtz1 zH`lW+0_hU8JfIX!pQkprk54HUhov0?)kNY}chJ@Q&Sk=D8d9HM3{i9IF7!GxoRurY z>i5eeT|HLp1sD)apR-AWt(pUJ0R(S27t7~DpzGqyBUT1pGC@adHJC4rVnPc0jXi1< zh@wNJ;2nHF7C~-`2F?t+u)W1RVW%T70Ut+>+Cg&vJoh#~Hg!&X!>peEDCYX${?}2K z3G~uhf#@PFRVN#qnv3|AM2#_p<2lOdzuRSz1%)%r>AWJ#V7z>?_8OXEjSDnY{GUGX zyi&4vbm43tq?QswQNfK*ecq9%Hmk!8Pvq^{^4thxVu=I$0GB)j{x|*WI)RJZO<^;O z4!?-p`eN1NigGYLX#vJbxf<~sWFDm|vVL#Xd0!35<02RTW*+taBNUS1#t)USVC$9< zi6jColxWQ)O)#oj1i91o3p;%mkD{u1+Nn+ZzUpnW66{WtLyDMY<}xfXQF23M+jg&m zohowA?q}@WYK0GB&w|Y7ahdf#gAI_KFa^!xd{#gi6fk}O-loBMl@B*tGDRMlekKH~ z_u7^n@=#2ZKOlkt5WDH!5<GJ-r>2f$nO8bYC2J$4r5ZY!4$&r><*3+QcM?3m*W)I) zINE!NFSrkOwB4^!0gkf?`z|0meXo}!9W=&F(%tC5WQ=$IkyK=O%W*<g4A^0~?7@fz zuD1;K(`7ms?^aA{c+M9W=R>mGuNIEN)n5qiNjp9#2!vJ9zp*=K!s6)03`;v#(wFza zYZhY&P%4(!V+umyO>0#iSrmX??|l{T2m<NzH&Iic|D5NScU@TW%2vQeV9SIm`CymF zha(Fo%D4Gjgy~0)%(0F}>ZmXl9nnGhZ6K=ab#~M2vn8|%RhdUpS7`jWPuX;ynNm== zcGFa>k*x6NJgBhU_xRld7G`C*A)6B83hlDTZIHKpm45tgx8~pd=vwW$PS4XCuVf)K z2ekoeN(~--T%gl(wrBX7qEO@xnEg{YxGWLd?k&uwX56pV#a9Kpz<KPRr5!V?Qz_(Z zMe8)pdVnoj9>V<QQrR<a0<CU(opcR^qU%Z!l>5fLwEsX2H#x<4)0#D;VbwEOh!1y> z_uak?jY@?p*@-M*ZozglNqF$eptf6iJ6hXSbzvJJ=xRYJS!m?2WEh_q_4Q4pHM{0; zsp&S7NIe{1R!X%!n0{y<RHIDQCYIb+F?ewIr$1H=>(fhKc{6>bulR$;?xE7Qk!^Wt z5+`-}&kfolm3XKT8gvBAIQH_)LK)FxjO-}&oQWUO8UaRr%k#V$&=|~3_C3GH)wBGB z_C}S+QnzlkiH&JGc|&m3!y-qIe~|2MORm_IT3d%YRyM;nm00=4LRUmIL`e+OSg-X& zn0f96`i)olo+~ePXT*lP3=LMu%ju!W!%EBGaRyl&CyYe>QDWyxZmEkWRy20y;Np3& z3K8r*Y+{^{Z20L7x6nhT>PkVdxzwK>^b^|S=7(r`Fc<o@sBIJ{>1q}cK$amED=9fY zQ)b-gN}qjv8o5LN;ns(MxTVWaRuTOSGA+2R<r?h8!TVcroSFeLmX&aE1ynE0Uyk5u z#Y+m~wU{FqqQj37HOajOY4Fs4>N@F)b`-wab==&wWxlkfYeXKz*)Z=$BDdWl4i(4g z^a;){Q5}LxtHKD)@~4ZujKVMMVRFBSiK7+g4RM0)<#sQtp&xQT5&ul-7$?W8Rh|nE zdA8#n_)DXeb_f(lo63`y#ZPqWT{c1t24V5u9-@#6Iojzg6|_5fjHOObdA%iwKJ67< zwmthQntY0E9Av#sz&7zs$5OdV+>=vMF)Zo|BU@gs)3ibTQ&FW<t5Nhg0n0N<lb|^D zlYTDP*z>d*ymsuG3eM@GC(#HIDxajJlZ{h$*a!6T0KSmY5k4XR5>&n=;2Da&#{5l> zF8!s;khl~Nq{TXjwXfv1@rBdn@!9HCQVdP{wPUPfyBKXj4;H9+`zGRG#`*#~pbd`o zh}V=?GH*PwsRmRZK~2mzS?h7?rPud}%R1qRv(qQTIoGOa-9e=-QQN?~(USLF@s63) z>R-Eb&YU{hG}HA`+)`S@p>rDmZiN2s=ki@&3obYxIG0%qB~mo>^6sC74YCh5Yx<hP zn%NM_dmKw*wVoO@Fdu&&-rq4;5Swf(cxQ>|Nl}+ss#OQSu8tkMjJ2CI^s>+2FtcL< zh|D1%|GO>wi(F47I%#HgF8}RJF!UExP?xlVFYu)r-jMA<u$z&Sw<GyGLB1b-5iClO zI9)ITcSfB5#QcJORVz3_OCi!NU@$@aCeJx#C}#&D2swokOQJJ#2$yR;+>ywoVi9E( z^rK7=0@5Fv`dr)#LBo7jEHPN85JnIiWM#fML6m)9V8V%TJF1e3?^-J6UtlY-%up^n zZ5jFAh{uGT9yWr_;*3pLqaP2cQ6Wniobis-6EI3&)`<M2FRA$k1VJ`Gd_d;n9=b2w zh)&G3xFF0<ptHY+mc%se$T+XAGJA(vL}~sM^9z54a#oj|B5!tEnCI?~8Bd*mwn0<C zHwU)o;vS7^m^j62L{e{p83=%1$Cpthyi*$KlC-(Y5u=<1IZRK2V4UdD{UY*EEQe&y zTxT4MpQ}q$&mCuWX@}bq`?0CwN$lm?*v_BexyCE2*nme4ZFE8*m}JG7Ix@09PK!t0 z+9ErxGJSv0JgpKC0d^scdaWLZQV3NcsNCEAVsE}{--H)&$20};c3rd*;+y|;=t0MH zn6*s6?YRx|&iKSRgT!kuG>9kCOYJEYA!j5V$~)EopCzSRIv<{$e<k+YsEqMQ9S=X5 z7Dq;(KT_n4Y4(>koeqbYffUWs0Eo{(fd9KlZ`XqPd6(=EAM2I&UMsK4;E$;+A9fU` z$-6`AoOhT+14Qg?-ZDaa)(NR&{%aaqPO9cY$HFP4Q=#)>o=@Oju~2)DR|+ruG+0Gq z;$5l*bk3RbknwAmb4HVBMPGb}PXZ=%U|$-|x=+HQMzY5o9;_5UT4H8jdVX0Q6-Of! zbfyK*GH5nOv?h4>(l=Gy;vqBG6`r^3OGB{EN<;|k5i@@GR^C`R8VlznOxN(m4}^-@ zuXfPR+&iOZo%sT^iGUo8-pFbyiLh@nt(>aM6Xx%YK`KuqrVWn(jHK{KsautWB!!2; zEw7fp4sfzG#W{!L-aCB4R$_Cl=qfsePd@%RZnXnV;T3{yL7@a<InY)tTb^t@zJpX+ zq7O1C8>a)8e&tLH<O@lPk^4+nHxtvD^!1|Bpsw0GiUs0C)u-X{mQ~Npda$drkAJqu zVth<7ea6lvN)7?$n30dt97pe^E;f8@hHlvLC;!ItLI5pvyJTtC!TPmNw>2pS`XG;I zBO@PIG=?NxsHQxAl;IE^?3ZS9O8;Vc#KGjr%QE)alI=g0U4FKrS4IlnsbdsdUkA!Q zC160IYxN*Oo));gQ*TQM#T}b9*0da=UqG_}lVYQ;9e#2*Z4jEL0OxqQb>)lHKi`m- zF{p5RiGBa-UO~=A$NgjNS$<<}uS0-O_+*hq#vM^`2FAm<ef|Qz_Tw`vl!n{9mzcGF z@uQ+tY#}oxO4|so#DKw2*nYk~QrQ*G+<_Ti)1g|2Ot?|jhK3&0PLe;$Y;)8G*}`D= zer7PvE22(kXlpaImS<jP4@%U%nuQU+LgGsY!^4Z<-FK=J!04RIz^c4Mwx^I_ft*U6 zW*i(W{nzwAXmdN3ojI@~Y{sr!UD~7ww(MSGAXeB65_j8xLcSY`IW%Om=d(6oR}T|* zWy6OzO8(F983P>>QOQa?2*G!<-n90G43t`m@DD-yJwOm!=T9w9pp0Bh5TQ0@9z3aB zy1f$RPm|aZKVFn~xYGIi#wDhb-wrL`_M{S1**vj#wQMs+bM)v*4A?2<n5rm0W4JWa z5gm;s)kqT_-v&efICZ7Czp7V7&tonrK3LzgXIZtjgc{VCkrjh}6BptnFlz>=U2y5F z_F!(N>B$h2`6g~mu1Q3-G^RDlP<bT_yBI{Inp?D%XvvnS>0y<iIzQ~_j;*9x?A_Z8 zO+Ei23!=^pWWk<cGGMH8|6&1mfqSvfwo-TSRp?2?s4IKB%~3lO=W=EdPr|m!um;a- zl$<~aj~LVI*dcoeb&(G&sNGmla$qD<=0FXU{3Q<frCIoEpGUMv*x!e`8Wh^XJ&%CZ zlGfeFFR_2}=_+6jdhyFKUN+~3<HQli<T;<i<Xl*p@ff^eKc?Em9`rX#RwxAkI&<1j zEPCIMH6{Q(!7ThG(dY4UZgtOEV-7TUl%d0{!<sEYMep%C6~!)ouHrtWh398}?xN(c zJZWp_nmyh>%GDV*$0ZMDn&AcMxH8jnJQ;rDA0Yz>kQ)kl>8?z%-vY8SB-R)29#=K3 z#^uX|Y=&rH4N3)t8wHg6iwNYl*_b_F$|VD%a9cnHyk$GdZrnz;EKq}t!8;3IV9Dd9 z|NhP~If{ZcW}EN;obFFw75Jt10a|>RwIEI4lJ&FACYzDm_okq~-1e=5o5s&rAukZ_ zn9b{-!^M)47;Dq$ppl2_uG-oSpn~qX(eQofA~EpNp+!C&$%3RWj50G}8x;LeJf=7r z#Gl{i@o7yy1vflt>ZO|-y2<F1Tm*BQgXD{{^%nw}I0h}38*3pNk~pPA$wi4P;jxYw zy-_M(f3A-zaQFvZnmW)Esx0V5P^V}#KgwToS{<joiB|~0vNYka>D?&_oZck2q<nfc zrxL){9q&o#cQbE3Pa*Rj9UtOT_Z6J46c$LN@oBi$T+0&n4W0MS^Os}qN&y$4xzmo# zYDYDp<k6d%r|lcb7GL7w9_^)O>_v;VgkC#ukT8)zTt+iD!=kz8x5Iw>zMYyK41AxP zqpflEEzgo5@_3j~M*PGXx`}wfm@W@zQcZv<662b_Ck7n*hL)@a1Cu6*M;wfUrMKaR zPem6A!p^Y>0U^ri4>>ZeWQuZrE&4XCqV7ikQDIfK8l{1O*SGx}c<mRtJ=&7y8KptB z5aLh&WL`4%ba;d_91U2Lr(}+Cg#4vo2tW=jN%EflyX@r%*Qz-!A3{{5!BpEt50F_i z`b?W+wKzd8m?W}D+wznW<Dhfwj8;_Vd0qLH55dxb#*+>DNZ~vAjr%_H@NZ^pn*<rj z=e(GC8;LRK-!qP&CwDHRXbh>Rd;Bs=VO)r~uqTWmyo<5^pCYpg&w>tn2WMHz$Xn6# zg#kaTgtSb?iiT!lVhQwiOJXvuacA>c{hw6K4p2BV`WT{%B7u?$1W2g2x(7PK$2Jvw zb3Xf*h^O?i-f6Az#dD974wwRzocnPq`tnNGpx;duA-vfHx!&L2?`nNWkkMB9AJjK1 z{7u6M^#ur&gROp<rbxBi^9$JoWG^Lc_F&}^ufZ^x)}LCqhAB!+mj8$y*|pdwmurmN z9=sk*+PKH3H@0=b>)~b}MZT%5hS!ItfRYu>i)0Ui#`-@2s0mm0^^G8Q0#pQ%F%-Jk z5d^%sNpApRFXy20m7FBs*gN3nXe058CaW8csb-U{72-<%)Yz^_6zufvI`){fzv6!u z$vOv(w?t2dhsK2bl3(j0)_rk7j)eIIy9ZF;a%mE$XM$3hO+7lt6lE+cY$#Bka|k$% z<<oh7B8krf%^lLju`f<a)&ZI>UX--5dteFx1#lD|8{C6cpdk)2Yw!q=(I~COEn!<+ zOrws^$Kg1{eC5b;_Y7Vzo#mmlmq1@D-NBr*M2%z~Nv;OTbE{W(G?#X8q&xo()%jMO zns{Nn<k>t!?!1jmDl}+`iv@GzK)dThy6nwYK|^G=FgHDk+Egx<z~`^Kci}LXV9P@k zD$_~l!;1G}3ZLE=R=`QxWVG9{f~yFcsImzM_0gX|u`v!?K#0a{g`Xzg^B~q}BE9#= zu`$a>Tmo;kMGwGSF}#d%u;lI=A7j5*DX0-`hQPIFn)Ou2#j2%}jd?e^;<mKZKc@d= z?Uw14efP3Y{5*ywo~i3qFt}r^aXQ=<On45e226d?k{_Y9UghypQxaiFB6aBQ*a?aa zt{mj=4D<dXnBDPN*?e()dOcf09Y)}IGZ4^9e0JD3njnZ$6B|*r^Z=cv!6DR4E&~-F zO4|Y28EKSj;0;~IM@*DAB6b<4^oID-(pQch<Hf5e^(?E{p{XV9W`DVCPtmWrv9p=d zzd_nwqmAr#^2d7Qj-j~)Oz|R+s2Ha4j!Bw@nH&EMnQHIS?`L}TDV3=Up#)Sl0cq?W zT>+jvkoX8$Auyy_9O5Gsu9C~v<VD}=TMK9#IN>!&*`(geQa>Q4MX!HR#g9cAhoZX6 z)FAW`&}e6173gC}SY&t5z)SvkQP9XUMdCEDHYwAyMwYo3pSMvJ(6<2y=D0t?z)8;j zv;Y7G-$_J4RIIcyJTef^ulNdwdzo|Zrd<w>&Hm4GSM>i9`*P1=AEpUd_5zM|v(PXe zTQw3T5SWn_m90^T8U-we?;=O3v-KSB@~QGZg;M#HS^B^y!WQEr`=XtvE{2jq5o#bB zgx@o^H;SZk)Ma$`MW4>ln4rBvjqjSE$t@t&X;nOxF>W*qNQGHL7vhaB^TRN6lec^! zoq-+v9;_TRYYWPnXhtM@+yZdt@Q~~4+2X#mZ!cd|WO~J5J=<cnK0Fk`>u55YWCrP5 z`17Roj(R)!B9A{fE^m)o;jE_QhKXuV1<u_nA!w8NEF0AW?7EoBwbry$n(B%LiRAat zbd-XK95_!C>2hDEOAN&$f~Gf(QhKWjqX~-NOY*sB%RnyGLMH@dWQ&6uT#aiWV@qm1 zs3k9<G_1x-y_p)p7T}U(172xitT+SA{MKw4kd`0eMo2peFz?1TH)9=47I1G`HsoHh zbEo^_-o5RkM;AC0#*||1+C`hv;%Q@^sZFsvS^LtTkM#deUz)s!mx`P4(|dd5fZ-lN z^ll6FK!y1EYMIxXSm$KGg$TWnKx3)hLfZwG56QzsAL)E(cbumPL=;e<(K`t6#Z<gp zxsPZC2IF&-#{(*$r5!9E?alIs=JI~xJTC%ph!5{t8BnH*$zPL>9u813BgEEGK*DgF zYQ`^BjlW$*`=|qW^~w5@w=VU3ly?)&CPHBIdn$?B&@-64@`9c1&sPql`^Nb#f$e>I zytl{{>nOL?;UlXC{;E<bzjm6>W79lRpDJ@^OtK<z7S8Mr_p<4l>zm-c2K6W<<c9^5 zfj;Z-Y8e0L8_73R42v;&oC;IC<WV`=RJ5h9Ll|gTv6gg*YtiIB@5=qcr<>ZRREcD$ zgmp{#bb<`q`$6Bv9vdSFl@N*^1IYF{;WA~_k7a9S*~dDM58v<`eROsXVD(}5Fw^WI zte1<nM}7M5Pki~{u{&SA=kVpqQY^WAVQ`q_Vav3yqdB-*KR-J2x(}}&bo~O52oxw| zAOi^|2a&3ZRWNdE@>1)qa!6J4PZ?yDUkLG8D$jToT}EheOE+CeMvrXKqxD0*I_&yn zOgBH}PQkF`ig)j%HW3&f>7zxix(HXVX@f_SMTE2+#!vCR+b5Lt%Ge2LIM<tv22+2I zr?lts^7U1FC1jkp!za-3e1+VN^8jLujhBg@N@b$Iw|{K&s+Ch0tQtB98|xd`v20>( z&1#N#yDk7I{~xbe_I}=0f1h>?7VM0>ifhX`lO&&7^}-M(Th@vbGwteJowwA`yZt=c zl#Zap@vkUb>a!hwqap-m)*+}>lU~~@sK$_$&sTYS6Dt;Naf<HP@Jh;+S6m)ln$XCY z+x&DElB@Am4{Z+N7g3$&m;<^~1ZyHOMLyHC!W5lqnwn0crp8f-<9wKCqSUI7SJLV@ zzkxQv`PAv5v@*LQY0PbQ%WIo2xul-lc%wFco|dtSbf>(o0N{sK4Zfh!Q~uA@G`&P> zQIFzi0P2FrkMnB8peQtP;VcJR2E)frmD;6pJMH635vYzLK;Es+L~GC<_J>0-Di5B8 zwgp&IA1d`y7RxEyuB8GLX5$hh;%VZ-T{(FW3;>l^FAMQaniLh$o}d^LNP^Z<T*(9z zm~oEZPcZ%fN;1W4beb>qPgK*=I5h{D=2TB_d48i?Hh1}<LpAcXRw4b<HOX~kd?D8! zt{3d$ix{gt&poW|h>Mcf!9TV^N1!|cg!O!?$&6;B$2N|<j>qx8fcm_GfjLnlYv)^y zg7HX%8ET0EVa=h*q@w-9ejU;kKmF-vqSf>58+w^E>Q!F5XhEr7l#zI56Ah)5F%WQu znrK3))c`+x$1YO4Jw?iKX37&_KTe(#H2iow0T}y=LTj4&=oCNWd#W#Oj}Mfq<9x(- z(hZC+tUl!GFZ7T0l+DvM2EcAg&+^L&1Tg{-D?EO)e=FD)>2tDh?|f?a0*|E=hGj%1 z+BclL?2=}?{Bpp%oZ>y3RO{kKECME3vQiG6nsv)K@09lGe4kX0w#f7n1^b++C{3R@ z?AzouUbeN9Sx6?4P=1Bjt03yma4SHP3Ebm+vkPeu5a~F3C`botsnwY6sg~vjT79$g zqoeaQ8b@G#BQ0H<vxXV~P^Ih1l+V;z!=4TRaSILjv;jD4iOxW9R`5dF1Q1OMi@ZKD z<W|k~^=;?f&7J%n_a+v&{4`{nFT*AMX`|KKJD)UBJyS^4WRtOg^J}Q3_82;A0*G;^ zsG!zFrKdDAT(8y!$H!|g^WO(3X7g0t5digiQ$vwK5ricqKxE$&5MueBs{Q{1I9Ih$ Tck!r@00000NkvXXu0mjffOK4E literal 0 HcmV?d00001 diff --git a/apps/web/src/images/secrets-manager/sdks/wasm.svg b/apps/web/src/images/secrets-manager/sdks/wasm.svg new file mode 100644 index 0000000000..0ed253e242 --- /dev/null +++ b/apps/web/src/images/secrets-manager/sdks/wasm.svg @@ -0,0 +1,11 @@ +<svg width="112" height="111" viewBox="0 0 112 111" fill="none" xmlns="http://www.w3.org/2000/svg"> + <g clip-path="url(#clip0_91_2175)"> + <path d="M68.5294 0C68.5294 0.195882 68.5294 0.391765 68.5294 0.598529C68.5294 7.62853 62.8307 13.3254 55.8025 13.3254C48.7725 13.3254 43.0756 7.62672 43.0756 0.598529C43.0756 0.391765 43.0756 0.195882 43.0756 0L0.333344 0V111H111.333V0H68.5294Z" fill="#654FF0"/> + <path d="M26.1173 59.8184H33.4737L38.4959 86.5654H38.5866L44.6227 59.8184H51.504L56.956 86.8937H57.063L62.7872 59.8184H70.0022L60.627 99.12H53.3268L47.9201 72.373H47.7786L41.991 99.12H34.5547L26.1173 59.8184ZM78.2963 59.8184H89.8933L101.41 99.12H93.8218L91.3171 90.3742H78.1059L76.1725 99.12H68.7815L78.2963 59.8184ZM82.7109 69.5055L79.5025 83.9246H89.4888L85.8052 69.5055H82.7109Z" fill="white"/> + </g> + <defs> + <clipPath id="clip0_91_2175"> + <rect width="111" height="111" fill="white" transform="translate(0.333344)"/> + </clipPath> + </defs> +</svg> diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index f14574508c..845816562b 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -7906,6 +7906,9 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, + "unassignedItemsBannerSelfHost": { + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, "unassignedItemsBannerNotice": { "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." }, @@ -7962,6 +7965,55 @@ "errorAssigningTargetFolder": { "message": "Error assigning target folder." }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations":{ + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, "createNewClientToManageAsProvider": { "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." }, diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-card/integration-card.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-card/integration-card.component.html new file mode 100644 index 0000000000..15b2519dae --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-card/integration-card.component.html @@ -0,0 +1,29 @@ +<div + class="tw-block tw-h-full tw-overflow-hidden tw-rounded tw-border tw-border-solid tw-border-secondary-600 tw-relative tw-transition-all xl:tw-w-64 hover:tw-scale-105 focus-within:tw-outline-none focus-within:tw-ring focus-within:tw-ring-primary-700 focus-within:tw-ring-offset-2" +> + <div + class="tw-flex tw-h-36 tw-bg-secondary-100 tw-items-center tw-justify-center tw-py-2 tw-px-6 lg:tw-py-4 lg:tw-px-12" + > + <div class="tw-flex tw-items-center tw-justify-center tw-h-28 tw-w-28 lg:tw-w-40"> + <img + #imageEle + [src]="image" + alt="" + class="tw-block tw-mx-auto tw-h-auto tw-max-w-full tw-max-h-full" + /> + </div> + </div> + <div class="tw-p-5"> + <h3 class="tw-mb-4 tw-text-lg tw-font-semibold">{{ name }}</h3> + <a + class="tw-block tw-mb-0 tw-font-bold hover:tw-no-underline focus:tw-outline-none after:tw-content-[''] after:tw-block after:tw-absolute after:tw-w-full after:tw-h-full after:tw-left-0 after:tw-top-0" + [href]="linkURL" + [rel]="[externalURL ? 'noopener noreferrer' : null]" + > + {{ linkText }} + </a> + <span *ngIf="showNewBadge()" bitBadge class="tw-mt-3" variant="secondary"> + {{ "new" | i18n }} + </span> + </div> +</div> diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-card/integration-card.component.spec.ts b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-card/integration-card.component.spec.ts new file mode 100644 index 0000000000..94cec5f627 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-card/integration-card.component.spec.ts @@ -0,0 +1,174 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { BehaviorSubject } from "rxjs"; + +import { SYSTEM_THEME_OBSERVABLE } from "../../../../../../../libs/angular/src/services/injection-tokens"; +import { ThemeType } from "../../../../../../../libs/common/src/platform/enums"; +import { ThemeStateService } from "../../../../../../../libs/common/src/platform/theming/theme-state.service"; + +import { IntegrationCardComponent } from "./integration-card.component"; + +describe("IntegrationCardComponent", () => { + let component: IntegrationCardComponent; + let fixture: ComponentFixture<IntegrationCardComponent>; + + const systemTheme$ = new BehaviorSubject<ThemeType>(ThemeType.Light); + const usersPreferenceTheme$ = new BehaviorSubject<ThemeType>(ThemeType.Light); + + beforeEach(async () => { + // reset system theme + systemTheme$.next(ThemeType.Light); + + await TestBed.configureTestingModule({ + declarations: [IntegrationCardComponent], + providers: [ + { + provide: ThemeStateService, + useValue: { selectedTheme$: usersPreferenceTheme$ }, + }, + { + provide: SYSTEM_THEME_OBSERVABLE, + useValue: systemTheme$, + }, + ], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(IntegrationCardComponent); + component = fixture.componentInstance; + + component.name = "Integration Name"; + component.image = "test-image.png"; + component.linkText = "Get started with integration"; + component.linkURL = "https://example.com/"; + + fixture.detectChanges(); + }); + + it("assigns link href", () => { + const link = fixture.nativeElement.querySelector("a"); + + expect(link.href).toBe("https://example.com/"); + }); + + it("renders card body", () => { + const name = fixture.nativeElement.querySelector("h3"); + const link = fixture.nativeElement.querySelector("a"); + + expect(name.textContent).toBe("Integration Name"); + expect(link.textContent.trim()).toBe("Get started with integration"); + }); + + it("assigns external rel attribute", () => { + component.externalURL = true; + fixture.detectChanges(); + + const link = fixture.nativeElement.querySelector("a"); + + expect(link.rel).toBe("noopener noreferrer"); + }); + + describe("new badge", () => { + beforeEach(() => { + jest.useFakeTimers(); + jest.setSystemTime(new Date("2023-09-01")); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it("shows when expiration is in the future", () => { + component.newBadgeExpiration = "2023-09-02"; + expect(component.showNewBadge()).toBe(true); + }); + + it("does not show when expiration is not set", () => { + expect(component.showNewBadge()).toBe(false); + }); + + it("does not show when expiration is in the past", () => { + component.newBadgeExpiration = "2023-08-31"; + expect(component.showNewBadge()).toBe(false); + }); + + it("does not show when expiration is today", () => { + component.newBadgeExpiration = "2023-09-01"; + expect(component.showNewBadge()).toBe(false); + }); + + it("does not show when expiration is invalid", () => { + component.newBadgeExpiration = "not-a-date"; + expect(component.showNewBadge()).toBe(false); + }); + }); + + describe("imageDarkMode", () => { + it("ignores theme changes when darkModeImage is not set", () => { + systemTheme$.next(ThemeType.Dark); + usersPreferenceTheme$.next(ThemeType.Dark); + + fixture.detectChanges(); + + expect(component.imageEle.nativeElement.src).toContain("test-image.png"); + }); + + describe("user prefers the system theme", () => { + beforeEach(() => { + component.imageDarkMode = "test-image-dark.png"; + }); + + it("sets image src to imageDarkMode", () => { + usersPreferenceTheme$.next(ThemeType.System); + systemTheme$.next(ThemeType.Dark); + + fixture.detectChanges(); + + expect(component.imageEle.nativeElement.src).toContain("test-image-dark.png"); + }); + + it("sets image src to light mode image", () => { + component.imageEle.nativeElement.src = "test-image-dark.png"; + + usersPreferenceTheme$.next(ThemeType.System); + systemTheme$.next(ThemeType.Light); + + fixture.detectChanges(); + + expect(component.imageEle.nativeElement.src).toContain("test-image.png"); + }); + }); + + describe("user prefers dark mode", () => { + beforeEach(() => { + component.imageDarkMode = "test-image-dark.png"; + }); + + it("updates image to dark mode", () => { + systemTheme$.next(ThemeType.Light); // system theme shouldn't matter + usersPreferenceTheme$.next(ThemeType.Dark); + + fixture.detectChanges(); + + expect(component.imageEle.nativeElement.src).toContain("test-image-dark.png"); + }); + }); + + describe("user prefers light mode", () => { + beforeEach(() => { + component.imageDarkMode = "test-image-dark.png"; + }); + + it("updates image to light mode", () => { + component.imageEle.nativeElement.src = "test-image-dark.png"; + + systemTheme$.next(ThemeType.Dark); // system theme shouldn't matter + usersPreferenceTheme$.next(ThemeType.Light); + + fixture.detectChanges(); + + expect(component.imageEle.nativeElement.src).toContain("test-image.png"); + }); + }); + }); +}); diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-card/integration-card.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-card/integration-card.component.ts new file mode 100644 index 0000000000..bf5f5bd311 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-card/integration-card.component.ts @@ -0,0 +1,93 @@ +import { + AfterViewInit, + Component, + ElementRef, + Inject, + Input, + OnDestroy, + ViewChild, +} from "@angular/core"; +import { Observable, Subject, combineLatest, takeUntil } from "rxjs"; + +import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens"; +import { ThemeType } from "@bitwarden/common/platform/enums"; +import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; + +@Component({ + selector: "sm-integration-card", + templateUrl: "./integration-card.component.html", +}) +export class IntegrationCardComponent implements AfterViewInit, OnDestroy { + private destroyed$: Subject<void> = new Subject(); + @ViewChild("imageEle") imageEle: ElementRef<HTMLImageElement>; + + @Input() name: string; + @Input() image: string; + @Input() imageDarkMode?: string; + @Input() linkText: string; + @Input() linkURL: string; + + /** Adds relevant `rel` attribute to external links */ + @Input() externalURL?: boolean; + + /** + * Date of when the new badge should be hidden. + * When omitted, the new badge is never shown. + * + * @example "2024-12-31" + */ + @Input() newBadgeExpiration?: string; + + constructor( + private themeStateService: ThemeStateService, + @Inject(SYSTEM_THEME_OBSERVABLE) + private systemTheme$: Observable<ThemeType>, + ) {} + + ngAfterViewInit() { + combineLatest([this.themeStateService.selectedTheme$, this.systemTheme$]) + .pipe(takeUntil(this.destroyed$)) + .subscribe(([theme, systemTheme]) => { + // When the card doesn't have a dark mode image, exit early + if (!this.imageDarkMode) { + return; + } + + if (theme === ThemeType.System) { + // When the user's preference is the system theme, + // use the system theme to determine the image + const prefersDarkMode = + systemTheme === ThemeType.Dark || systemTheme === ThemeType.SolarizedDark; + + this.imageEle.nativeElement.src = prefersDarkMode ? this.imageDarkMode : this.image; + } else if (theme === ThemeType.Dark || theme === ThemeType.SolarizedDark) { + // When the user's preference is dark mode, use the dark mode image + this.imageEle.nativeElement.src = this.imageDarkMode; + } else { + // Otherwise use the light mode image + this.imageEle.nativeElement.src = this.image; + } + }); + } + + ngOnDestroy(): void { + this.destroyed$.next(); + this.destroyed$.complete(); + } + + /** Show the "new" badge when expiration is in the future */ + showNewBadge() { + if (!this.newBadgeExpiration) { + return false; + } + + const expirationDate = new Date(this.newBadgeExpiration); + + // Do not show the new badge for invalid dates + if (isNaN(expirationDate.getTime())) { + return false; + } + + return expirationDate > new Date(); + } +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-grid/integration-grid.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-grid/integration-grid.component.html new file mode 100644 index 0000000000..a0c82d2f34 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-grid/integration-grid.component.html @@ -0,0 +1,15 @@ +<ul + class="tw-inline-grid tw-grid-cols-3 tw-gap-6 tw-m-0 tw-p-0 tw-w-full tw-auto-cols-auto tw-list-none lg:tw-grid-cols-4 lg:tw-gap-10 lg:tw-w-auto" +> + <li *ngFor="let integration of integrations"> + <sm-integration-card + [name]="integration.name" + [linkText]="integration.linkText" + [linkURL]="integration.linkURL" + [image]="integration.image" + [imageDarkMode]="integration.imageDarkMode" + [externalURL]="integration.type === IntegrationType.SDK" + [newBadgeExpiration]="integration.newBadgeExpiration" + ></sm-integration-card> + </li> +</ul> diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-grid/integration-grid.component.spec.ts b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-grid/integration-grid.component.spec.ts new file mode 100644 index 0000000000..e74e057e06 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-grid/integration-grid.component.spec.ts @@ -0,0 +1,81 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { By } from "@angular/platform-browser"; +import { mock } from "jest-mock-extended"; +import { of } from "rxjs"; + +import { SYSTEM_THEME_OBSERVABLE } from "../../../../../../../libs/angular/src/services/injection-tokens"; +import { IntegrationType } from "../../../../../../../libs/common/src/enums"; +import { ThemeType } from "../../../../../../../libs/common/src/platform/enums"; +import { ThemeStateService } from "../../../../../../../libs/common/src/platform/theming/theme-state.service"; +import { IntegrationCardComponent } from "../integration-card/integration-card.component"; +import { Integration } from "../models/integration"; + +import { IntegrationGridComponent } from "./integration-grid.component"; + +describe("IntegrationGridComponent", () => { + let component: IntegrationGridComponent; + let fixture: ComponentFixture<IntegrationGridComponent>; + const integrations: Integration[] = [ + { + name: "Integration 1", + image: "test-image1.png", + linkText: "Get started with integration 1", + linkURL: "https://example.com/1", + type: IntegrationType.Integration, + }, + { + name: "SDK 2", + image: "test-image2.png", + linkText: "View SDK 2", + linkURL: "https://example.com/2", + type: IntegrationType.SDK, + }, + ]; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [IntegrationGridComponent, IntegrationCardComponent], + providers: [ + { + provide: ThemeStateService, + useValue: mock<ThemeStateService>(), + }, + { + provide: SYSTEM_THEME_OBSERVABLE, + useValue: of(ThemeType.Light), + }, + ], + }); + + fixture = TestBed.createComponent(IntegrationGridComponent); + component = fixture.componentInstance; + component.integrations = integrations; + fixture.detectChanges(); + }); + + it("lists all integrations", () => { + expect(component.integrations).toEqual(integrations); + + const cards = fixture.debugElement.queryAll(By.directive(IntegrationCardComponent)); + + expect(cards.length).toBe(integrations.length); + }); + + it("assigns the correct attributes to IntegrationCardComponent", () => { + expect(component.integrations).toEqual(integrations); + + const card = fixture.debugElement.queryAll(By.directive(IntegrationCardComponent))[1]; + + expect(card.componentInstance.name).toBe("SDK 2"); + expect(card.componentInstance.image).toBe("test-image2.png"); + expect(card.componentInstance.linkText).toBe("View SDK 2"); + expect(card.componentInstance.linkURL).toBe("https://example.com/2"); + }); + + it("assigns `externalURL` for SDKs", () => { + const card = fixture.debugElement.queryAll(By.directive(IntegrationCardComponent)); + + expect(card[0].componentInstance.externalURL).toBe(false); + expect(card[1].componentInstance.externalURL).toBe(true); + }); +}); diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-grid/integration-grid.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-grid/integration-grid.component.ts new file mode 100644 index 0000000000..058d59d702 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-grid/integration-grid.component.ts @@ -0,0 +1,15 @@ +import { Component, Input } from "@angular/core"; + +import { IntegrationType } from "@bitwarden/common/enums"; + +import { Integration } from "../models/integration"; + +@Component({ + selector: "sm-integration-grid", + templateUrl: "./integration-grid.component.html", +}) +export class IntegrationGridComponent { + @Input() integrations: Integration[]; + + protected IntegrationType = IntegrationType; +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations-routing.module.ts b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations-routing.module.ts new file mode 100644 index 0000000000..91402113a9 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations-routing.module.ts @@ -0,0 +1,17 @@ +import { NgModule } from "@angular/core"; +import { RouterModule, Routes } from "@angular/router"; + +import { IntegrationsComponent } from "./integrations.component"; + +const routes: Routes = [ + { + path: "", + component: IntegrationsComponent, + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class IntegrationsRoutingModule {} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.html new file mode 100644 index 0000000000..a2f2188861 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.html @@ -0,0 +1,16 @@ +<app-header> + <sm-new-menu></sm-new-menu> +</app-header> + +<section class="tw-mb-9"> + <p bitTypography="body1">{{ "integrationsDesc" | i18n }}</p> + <sm-integration-grid [integrations]="integrations"></sm-integration-grid> +</section> + +<section class="tw-mb-9"> + <h2 bitTypography="h2"> + {{ "sdks" | i18n }} + </h2> + <p bitTypography="body1">{{ "sdksDesc" | i18n }}</p> + <sm-integration-grid [integrations]="sdks"></sm-integration-grid> +</section> diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.spec.ts b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.spec.ts new file mode 100644 index 0000000000..10fbaa1f3f --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.spec.ts @@ -0,0 +1,77 @@ +import { Component } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { By } from "@angular/platform-browser"; +import { mock } from "jest-mock-extended"; +import { of } from "rxjs"; + +import { SYSTEM_THEME_OBSERVABLE } from "../../../../../../libs/angular/src/services/injection-tokens"; +import { I18nService } from "../../../../../../libs/common/src/platform/abstractions/i18n.service"; +import { ThemeType } from "../../../../../../libs/common/src/platform/enums"; +import { ThemeStateService } from "../../../../../../libs/common/src/platform/theming/theme-state.service"; +import { I18nPipe } from "../../../../../../libs/components/src/shared/i18n.pipe"; + +import { IntegrationCardComponent } from "./integration-card/integration-card.component"; +import { IntegrationGridComponent } from "./integration-grid/integration-grid.component"; +import { IntegrationsComponent } from "./integrations.component"; + +@Component({ + selector: "app-header", + template: "<div></div>", +}) +class MockHeaderComponent {} + +@Component({ + selector: "sm-new-menu", + template: "<div></div>", +}) +class MockNewMenuComponent {} + +describe("IntegrationsComponent", () => { + let fixture: ComponentFixture<IntegrationsComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ + IntegrationsComponent, + IntegrationGridComponent, + IntegrationCardComponent, + MockHeaderComponent, + MockNewMenuComponent, + I18nPipe, + ], + providers: [ + { + provide: I18nService, + useValue: mock<I18nService>({ t: (key) => key }), + }, + { + provide: ThemeStateService, + useValue: mock<ThemeStateService>(), + }, + { + provide: SYSTEM_THEME_OBSERVABLE, + useValue: of(ThemeType.Light), + }, + ], + }).compileComponents(); + fixture = TestBed.createComponent(IntegrationsComponent); + fixture.detectChanges(); + }); + + it("divides Integrations & SDKS", () => { + const [integrationList, sdkList] = fixture.debugElement.queryAll( + By.directive(IntegrationGridComponent), + ); + + // Validate only expected names, as the data is constant + expect( + (integrationList.componentInstance as IntegrationGridComponent).integrations.map( + (i) => i.name, + ), + ).toEqual(["GitHub Actions", "GitLab CI/CD", "Ansible"]); + + expect( + (sdkList.componentInstance as IntegrationGridComponent).integrations.map((i) => i.name), + ).toEqual(["C#", "C++", "Go", "Java", "JS WebAssembly", "php", "Python", "Ruby"]); + }); +}); diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.ts new file mode 100644 index 0000000000..f11048b6a3 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.ts @@ -0,0 +1,113 @@ +import { Component } from "@angular/core"; + +import { IntegrationType } from "@bitwarden/common/enums"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; + +import { Integration } from "./models/integration"; + +@Component({ + selector: "sm-integrations", + templateUrl: "./integrations.component.html", +}) +export class IntegrationsComponent { + private integrationsAndSdks: Integration[] = []; + + constructor(i18nService: I18nService) { + this.integrationsAndSdks = [ + { + name: "GitHub Actions", + linkText: i18nService.t("setUpGithubActions"), + linkURL: "https://bitwarden.com/help/github-actions-integration/", + image: "../../../../../../../images/secrets-manager/integrations/github.svg", + imageDarkMode: "../../../../../../../images/secrets-manager/integrations/github-white.svg", + type: IntegrationType.Integration, + }, + { + name: "GitLab CI/CD", + linkText: i18nService.t("setUpGitlabCICD"), + linkURL: "https://bitwarden.com/help/gitlab-integration/", + image: "../../../../../../../images/secrets-manager/integrations/gitlab.svg", + imageDarkMode: "../../../../../../../images/secrets-manager/integrations/gitlab-white.svg", + type: IntegrationType.Integration, + }, + { + name: "Ansible", + linkText: i18nService.t("setUpAnsible"), + linkURL: "https://bitwarden.com/help/ansible-integration/", + image: "../../../../../../../images/secrets-manager/integrations/ansible.svg", + type: IntegrationType.Integration, + }, + { + name: "C#", + linkText: i18nService.t("cSharpSDKRepo"), + linkURL: "https://github.com/bitwarden/sdk/tree/main/languages/csharp", + image: "../../../../../../../images/secrets-manager/sdks/c-sharp.svg", + type: IntegrationType.SDK, + }, + { + name: "C++", + linkText: i18nService.t("cPlusPlusSDKRepo"), + linkURL: "https://github.com/bitwarden/sdk/tree/main/languages/cpp", + image: "../../../../../../../images/secrets-manager/sdks/c-plus-plus.png", + type: IntegrationType.SDK, + }, + { + name: "Go", + linkText: i18nService.t("goSDKRepo"), + linkURL: "https://github.com/bitwarden/sdk/tree/main/languages/go", + image: "../../../../../../../images/secrets-manager/sdks/go.svg", + type: IntegrationType.SDK, + }, + { + name: "Java", + linkText: i18nService.t("javaSDKRepo"), + linkURL: "https://github.com/bitwarden/sdk/tree/main/languages/java", + image: "../../../../../../../images/secrets-manager/sdks/java.svg", + imageDarkMode: "../../../../../../../images/secrets-manager/sdks/java-white.svg", + type: IntegrationType.SDK, + }, + { + name: "JS WebAssembly", + linkText: i18nService.t("jsWebAssemblySDKRepo"), + linkURL: "https://github.com/bitwarden/sdk/tree/main/languages/js", + image: "../../../../../../../images/secrets-manager/sdks/wasm.svg", + type: IntegrationType.SDK, + }, + { + name: "php", + linkText: i18nService.t("phpSDKRepo"), + linkURL: "https://github.com/bitwarden/sdk/tree/main/languages/php", + image: "../../../../../../../images/secrets-manager/sdks/php.svg", + type: IntegrationType.SDK, + }, + { + name: "Python", + linkText: i18nService.t("pythonSDKRepo"), + linkURL: "https://github.com/bitwarden/sdk/tree/main/languages/python", + image: "../../../../../../../images/secrets-manager/sdks/python.svg", + type: IntegrationType.SDK, + }, + { + name: "Ruby", + linkText: i18nService.t("rubySDKRepo"), + linkURL: "https://github.com/bitwarden/sdk/tree/main/languages/ruby", + image: "../../../../../../../images/secrets-manager/sdks/ruby.png", + type: IntegrationType.SDK, + }, + ]; + } + + /** Filter out content for the integrations sections */ + get integrations(): Integration[] { + return this.integrationsAndSdks.filter( + (integration) => integration.type === IntegrationType.Integration, + ); + } + + /** Filter out content for the SDKs section */ + get sdks(): Integration[] { + return this.integrationsAndSdks.filter( + (integration) => integration.type === IntegrationType.SDK, + ); + } +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.module.ts b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.module.ts new file mode 100644 index 0000000000..0d26b626f1 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.module.ts @@ -0,0 +1,15 @@ +import { NgModule } from "@angular/core"; + +import { SecretsManagerSharedModule } from "../shared/sm-shared.module"; + +import { IntegrationCardComponent } from "./integration-card/integration-card.component"; +import { IntegrationGridComponent } from "./integration-grid/integration-grid.component"; +import { IntegrationsRoutingModule } from "./integrations-routing.module"; +import { IntegrationsComponent } from "./integrations.component"; + +@NgModule({ + imports: [SecretsManagerSharedModule, IntegrationsRoutingModule], + declarations: [IntegrationsComponent, IntegrationGridComponent, IntegrationCardComponent], + providers: [], +}) +export class IntegrationsModule {} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/models/integration.ts b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/models/integration.ts new file mode 100644 index 0000000000..51ca79b30f --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/models/integration.ts @@ -0,0 +1,21 @@ +import { IntegrationType } from "@bitwarden/common/enums"; + +/** Integration or SDK */ +export type Integration = { + name: string; + image: string; + /** + * Optional image shown in dark mode. + */ + imageDarkMode?: string; + linkURL: string; + linkText: string; + type: IntegrationType; + /** + * Shows the "New" badge until the defined date. + * When omitted, the badge is never shown. + * + * @example "2024-12-31" + */ + newBadgeExpiration?: string; +}; 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 c6c7bc6efb..e382fbd9a9 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 @@ -22,6 +22,12 @@ route="machine-accounts" [relativeTo]="route.parent" ></bit-nav-item> + <bit-nav-item + icon="bwi-providers" + [text]="'integrations' | i18n" + route="integrations" + [relativeTo]="route.parent" + ></bit-nav-item> <bit-nav-item icon="bwi-trash" [text]="'trash' | i18n" 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 10aa08612f..00ec259a12 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 @@ -5,6 +5,7 @@ import { AuthGuard } from "@bitwarden/angular/auth/guards"; import { organizationEnabledGuard } from "./guards/sm-org-enabled.guard"; import { canActivateSM } from "./guards/sm.guard"; +import { IntegrationsModule } from "./integrations/integrations.module"; import { LayoutComponent } from "./layout/layout.component"; import { NavigationComponent } from "./layout/navigation.component"; import { OverviewModule } from "./overview/overview.module"; @@ -60,6 +61,13 @@ const routes: Routes = [ titleId: "machineAccounts", }, }, + { + path: "integrations", + loadChildren: () => IntegrationsModule, + data: { + titleId: "integrations", + }, + }, { path: "trash", loadChildren: () => TrashModule, diff --git a/libs/common/src/enums/index.ts b/libs/common/src/enums/index.ts index 378af213e6..9ca806899a 100644 --- a/libs/common/src/enums/index.ts +++ b/libs/common/src/enums/index.ts @@ -3,6 +3,7 @@ export * from "./device-type.enum"; export * from "./event-system-user.enum"; export * from "./event-type.enum"; export * from "./http-status-code.enum"; +export * from "./integration-type.enum"; export * from "./native-messaging-version.enum"; export * from "./notification-type.enum"; export * from "./product-type.enum"; diff --git a/libs/common/src/enums/integration-type.enum.ts b/libs/common/src/enums/integration-type.enum.ts new file mode 100644 index 0000000000..acb9510697 --- /dev/null +++ b/libs/common/src/enums/integration-type.enum.ts @@ -0,0 +1,4 @@ +export enum IntegrationType { + Integration = "integration", + SDK = "sdk", +} From b26c9df056e0a0d665ac65271008d709b65933f1 Mon Sep 17 00:00:00 2001 From: Matt Gibson <mgibson@bitwarden.com> Date: Fri, 19 Apr 2024 05:41:46 -0400 Subject: [PATCH 222/351] Fix migrated state service data (#8815) State service held data in an encrypted pair, with potentially both encrypted and decrypted values. We want the encrypted form for these disk migrations (decrypted would always be empty on disk anyways). --- ...e-cipher-service-to-state-provider.spec.ts | 24 +++++++++++-------- ...7-move-cipher-service-to-state-provider.ts | 9 ++++--- 2 files changed, 20 insertions(+), 13 deletions(-) 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 index 499cff1c89..f51699bc79 100644 --- 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 @@ -26,11 +26,13 @@ function exampleJSON() { }, }, ciphers: { - "cipher-id-10": { - id: "cipher-id-10", - }, - "cipher-id-11": { - id: "cipher-id-11", + encrypted: { + "cipher-id-10": { + id: "cipher-id-10", + }, + "cipher-id-11": { + id: "cipher-id-11", + }, }, }, }, @@ -150,11 +152,13 @@ describe("CipherServiceMigrator", () => { }, }, ciphers: { - "cipher-id-10": { - id: "cipher-id-10", - }, - "cipher-id-11": { - id: "cipher-id-11", + encrypted: { + "cipher-id-10": { + id: "cipher-id-10", + }, + "cipher-id-11": { + id: "cipher-id-11", + }, }, }, }, 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 index e71d889bb7..80c776e1b6 100644 --- 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 @@ -4,7 +4,9 @@ import { Migrator } from "../migrator"; type ExpectedAccountType = { data: { localData?: unknown; - ciphers?: unknown; + ciphers?: { + encrypted: unknown; + }; }; }; @@ -37,7 +39,7 @@ export class CipherServiceMigrator extends Migrator<56, 57> { } //Migrate ciphers - const ciphers = account?.data?.ciphers; + const ciphers = account?.data?.ciphers?.encrypted; if (ciphers != null) { await helper.setToUser(userId, CIPHERS_DISK, ciphers); delete account.data.ciphers; @@ -68,7 +70,8 @@ export class CipherServiceMigrator extends Migrator<56, 57> { const ciphers = await helper.getFromUser(userId, CIPHERS_DISK); if (account.data && ciphers != null) { - account.data.ciphers = ciphers; + account.data.ciphers ||= { encrypted: null }; + account.data.ciphers.encrypted = ciphers; await helper.set(userId, account); } await helper.setToUser(userId, CIPHERS_DISK, null); From ec1973b334814f7b64be39a3072bcd857d014a17 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 19 Apr 2024 10:39:43 +0000 Subject: [PATCH 223/351] Autosync the updated translations (#8826) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/browser/src/_locales/ar/messages.json | 40 ++++++++++---- apps/browser/src/_locales/az/messages.json | 54 +++++++++++++------ apps/browser/src/_locales/be/messages.json | 40 ++++++++++---- apps/browser/src/_locales/bg/messages.json | 40 ++++++++++---- apps/browser/src/_locales/bn/messages.json | 40 ++++++++++---- apps/browser/src/_locales/bs/messages.json | 40 ++++++++++---- apps/browser/src/_locales/ca/messages.json | 40 ++++++++++---- apps/browser/src/_locales/cs/messages.json | 40 ++++++++++---- apps/browser/src/_locales/cy/messages.json | 40 ++++++++++---- apps/browser/src/_locales/da/messages.json | 40 ++++++++++---- apps/browser/src/_locales/de/messages.json | 40 ++++++++++---- apps/browser/src/_locales/el/messages.json | 40 ++++++++++---- apps/browser/src/_locales/en_GB/messages.json | 40 ++++++++++---- apps/browser/src/_locales/en_IN/messages.json | 40 ++++++++++---- apps/browser/src/_locales/es/messages.json | 44 ++++++++++----- apps/browser/src/_locales/et/messages.json | 40 ++++++++++---- apps/browser/src/_locales/eu/messages.json | 40 ++++++++++---- apps/browser/src/_locales/fa/messages.json | 40 ++++++++++---- apps/browser/src/_locales/fi/messages.json | 40 ++++++++++---- apps/browser/src/_locales/fil/messages.json | 40 ++++++++++---- apps/browser/src/_locales/fr/messages.json | 48 ++++++++++++----- apps/browser/src/_locales/gl/messages.json | 52 ++++++++++++------ apps/browser/src/_locales/he/messages.json | 40 ++++++++++---- apps/browser/src/_locales/hi/messages.json | 40 ++++++++++---- apps/browser/src/_locales/hr/messages.json | 40 ++++++++++---- apps/browser/src/_locales/hu/messages.json | 40 ++++++++++---- apps/browser/src/_locales/id/messages.json | 40 ++++++++++---- apps/browser/src/_locales/it/messages.json | 40 ++++++++++---- apps/browser/src/_locales/ja/messages.json | 40 ++++++++++---- apps/browser/src/_locales/ka/messages.json | 40 ++++++++++---- apps/browser/src/_locales/km/messages.json | 40 ++++++++++---- apps/browser/src/_locales/kn/messages.json | 40 ++++++++++---- apps/browser/src/_locales/ko/messages.json | 40 ++++++++++---- apps/browser/src/_locales/lt/messages.json | 40 ++++++++++---- apps/browser/src/_locales/lv/messages.json | 40 ++++++++++---- apps/browser/src/_locales/ml/messages.json | 40 ++++++++++---- apps/browser/src/_locales/mr/messages.json | 40 ++++++++++---- apps/browser/src/_locales/my/messages.json | 40 ++++++++++---- apps/browser/src/_locales/nb/messages.json | 40 ++++++++++---- apps/browser/src/_locales/ne/messages.json | 40 ++++++++++---- apps/browser/src/_locales/nl/messages.json | 40 ++++++++++---- apps/browser/src/_locales/nn/messages.json | 40 ++++++++++---- apps/browser/src/_locales/or/messages.json | 40 ++++++++++---- apps/browser/src/_locales/pl/messages.json | 44 ++++++++++----- apps/browser/src/_locales/pt_BR/messages.json | 40 ++++++++++---- apps/browser/src/_locales/pt_PT/messages.json | 40 ++++++++++---- apps/browser/src/_locales/ro/messages.json | 40 ++++++++++---- apps/browser/src/_locales/ru/messages.json | 40 ++++++++++---- apps/browser/src/_locales/si/messages.json | 40 ++++++++++---- apps/browser/src/_locales/sk/messages.json | 40 ++++++++++---- apps/browser/src/_locales/sl/messages.json | 40 ++++++++++---- apps/browser/src/_locales/sr/messages.json | 40 ++++++++++---- apps/browser/src/_locales/sv/messages.json | 40 ++++++++++---- apps/browser/src/_locales/te/messages.json | 40 ++++++++++---- apps/browser/src/_locales/th/messages.json | 40 ++++++++++---- apps/browser/src/_locales/tr/messages.json | 40 ++++++++++---- apps/browser/src/_locales/uk/messages.json | 40 ++++++++++---- apps/browser/src/_locales/vi/messages.json | 54 +++++++++++++------ apps/browser/src/_locales/zh_CN/messages.json | 40 ++++++++++---- apps/browser/src/_locales/zh_TW/messages.json | 40 ++++++++++---- apps/browser/store/locales/gl/copy.resx | 2 +- 61 files changed, 1829 insertions(+), 629 deletions(-) diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index 1f7c5bbe98..b7e26f3362 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "تغيير كلمة المرور الرئيسية" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "عبارة بصمة الإصبع", "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": "أُضيف المجلد" }, - "changeMasterPass": { - "message": "تغيير كلمة المرور الرئيسية" - }, - "changeMasterPasswordConfirmation": { - "message": "يمكنك تغيير كلمة المرور الرئيسية من خزنة الويب في bitwarden.com. هل تريد زيارة الموقع الآن؟" - }, "twoStepLoginConfirmation": { "message": "تسجيل الدخول بخطوتين يجعل حسابك أكثر أمنا من خلال مطالبتك بالتحقق من تسجيل الدخول باستخدام جهاز آخر مثل مفتاح الأمان، تطبيق المصادقة، الرسائل القصيرة، المكالمة الهاتفية، أو البريد الإلكتروني. يمكن تمكين تسجيل الدخول بخطوتين على خزنة الويب bitwarden.com. هل تريد زيارة الموقع الآن؟" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "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." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "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." + "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/az/messages.json b/apps/browser/src/_locales/az/messages.json index 2111ea6704..20834af27f 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Ana parolu dəyişdir" }, + "continueToWebApp": { + "message": "Veb tətbiqlə davam edilsin?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Ana parolunuzu Bitwarden veb tətbiqində dəyişdirə bilərsiniz." + }, "fingerprintPhrase": { "message": "Barmaq izi ifadəsi", "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": "Qovluq əlavə edildi" }, - "changeMasterPass": { - "message": "Ana parolu dəyişdir" - }, - "changeMasterPasswordConfirmation": { - "message": "Ana parolunuzu bitwarden.com veb anbarında dəyişdirə bilərsiniz. İndi saytı ziyarət etmək istəyirsiniz?" - }, "twoStepLoginConfirmation": { "message": "İki addımlı giriş, güvənlik açarı, kimlik doğrulayıcı tətbiq, SMS, telefon zəngi və ya e-poçt kimi digər cihazlarla girişinizi doğrulamanızı tələb edərək hesabınızı daha da güvənli edir. İki addımlı giriş, bitwarden.com veb anbarında qurula bilər. Veb saytı indi ziyarət etmək istəyirsiniz?" }, @@ -1045,7 +1045,7 @@ "message": "Bildiriş server URL-si" }, "iconsUrl": { - "message": "Nişan server URL-si" + "message": "İkon server URL-si" }, "environmentSaved": { "message": "Mühit URL-ləri saxlanıldı." @@ -1072,7 +1072,7 @@ "description": "Overlay appearance select option for showing the field on focus of the input element" }, "autofillOverlayVisibilityOnButtonClick": { - "message": "Avto-doldurma nişanı seçiləndə", + "message": "Avto-doldurma ikonu seçiləndə", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, "enableAutoFillOnPageLoad": { @@ -1109,7 +1109,7 @@ "message": "Anbarı açılan pəncərədə aç" }, "commandOpenSidebar": { - "message": "Anbar yan sətirdə aç" + "message": "Anbarı yan çubuqda aç" }, "commandAutofillDesc": { "message": "Hazırkı veb sayt üçün son istifadə edilən giriş məlumatlarını avto-doldur" @@ -1162,7 +1162,7 @@ "message": "Bu brauzer bu açılan pəncərədə U2F tələblərini emal edə bilmir. U2F istifadə edərək giriş etmək üçün bu açılan pəncərəni yeni bir pəncərədə açmaq istəyirsiniz?" }, "enableFavicon": { - "message": "Veb sayt nişanlarını göstər" + "message": "Veb sayt ikonlarını göstər" }, "faviconDesc": { "message": "Hər girişin yanında tanına bilən təsvir göstər." @@ -1724,7 +1724,7 @@ "message": "İcazə tələb xətası" }, "nativeMessaginPermissionSidebarDesc": { - "message": "Bu əməliyyatı kənar çubuqda icra edilə bilməz. Lütfən açılan pəncərədə yenidən sınayın." + "message": "Bu əməliyyat yan çubuqda icra edilə bilməz. Lütfən açılan pəncərədə yenidən sınayın." }, "personalOwnershipSubmitError": { "message": "Müəssisə Siyasətinə görə, elementləri şəxsi anbarınızda saxlamağınız məhdudlaşdırılıb. Sahiblik seçimini təşkilat olaraq dəyişdirin və mövcud kolleksiyalar arasından seçim edin." @@ -1924,10 +1924,10 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendLinuxChromiumFileWarning": { - "message": "Bir fayl seçmək üçün (mümkünsə) kənar çubuqdakı uzantını açın və ya bu bannerə klikləyərək yeni bir pəncərədə açın." + "message": "Bir fayl seçmək üçün (mümkünsə) yan çubuqdakı uzantını açın və ya bu bannerə klikləyərək yeni bir pəncərədə açın." }, "sendFirefoxFileWarning": { - "message": "Firefox istifadə edərək bir fayl seçmək üçün kənar çubuqdakı uzantını açın və ya bu bannerə klikləyərək yeni bir pəncərədə açın." + "message": "Firefox istifadə edərək bir fayl seçmək üçün yan çubuqdakı uzantını açın və ya bu bannerə klikləyərək yeni bir pəncərədə açın." }, "sendSafariFileWarning": { "message": "Safari istifadə edərək bir fayl seçmək üçün bu bannerə klikləyərək yeni bir pəncərədə açın." @@ -3000,16 +3000,36 @@ "message": "Kimlik məlumatlarını saxlama xətası. Detallar üçün konsolu yoxlayın.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Uğurlu" + }, "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." + "unassignedItemsBannerNotice": { + "message": "Bildiriş: Təyin edilməyən təşkilat elementləri artıq Bütün Anbarlar görünüşündə görünən olmayacaq və yalnız Admin Konsolu vasitəsilə əlçatan olacaq." }, - "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." + "unassignedItemsBannerSelfHostNotice": { + "message": "Bildiriş: 16 May 2024-cü il tarixindən etibarən, təyin edilməyən təşkilat elementləri Bütün Anbarlar görünüşündə görünən olmayacaq və yalnız Admin Konsolu vasitəsilə əlçatan olacaq." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Bu elementləri görünən etmək üçün", + "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": "bir kolleksiyaya təyin edin.", + "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 Konsolu" + }, + "errorAssigningTargetCollection": { + "message": "Hədəf kolleksiyaya təyin etmə xətası." + }, + "errorAssigningTargetFolder": { + "message": "Hədəf qovluğa təyin etmə xətası." } } diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index 08cb351abb..630ad48ee6 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Змяніць асноўны пароль" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Фраза адбітка пальца", "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": "Папка дададзена" }, - "changeMasterPass": { - "message": "Змяніць асноўны пароль" - }, - "changeMasterPasswordConfirmation": { - "message": "Вы можаце змяніць свой асноўны пароль у вэб-сховішчы на bitwarden.com. Перайсці на вэб-сайт зараз?" - }, "twoStepLoginConfirmation": { "message": "Двухэтапны ўваход робіць ваш уліковы запіс больш бяспечным, патрабуючы пацвярджэнне ўваходу на іншай прыладзе з выкарыстаннем ключа бяспекі, праграмы аўтэнтыфікацыі, SMS, тэлефоннага званка або электроннай пошты. Двухэтапны ўваход уключаецца на bitwarden.com. Перайсці на вэб-сайт, каб зрабіць гэта?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "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." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "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." + "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/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index 87dfc8d3be..edfcd8a9b4 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Промяна на главната парола" }, + "continueToWebApp": { + "message": "Продължаване към уеб приложението?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Може да промените главната си парола в уеб приложението на Битуорден." + }, "fingerprintPhrase": { "message": "Уникална фраза", "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": "Добавена папка" }, - "changeMasterPass": { - "message": "Промяна на главната парола" - }, - "changeMasterPasswordConfirmation": { - "message": "Главната парола на трезор може да се промени чрез сайта bitwarden.com. Искате ли да го посетите?" - }, "twoStepLoginConfirmation": { "message": "Двустепенното вписване защитава регистрацията ви, като ви кара да потвърдите влизането си чрез устройство-ключ, приложение за удостоверение, мобилно съобщение, телефонно обаждане или електронна поща. Двустепенното вписване може да се включи чрез сайта bitwarden.com. Искате ли да го посетите?" }, @@ -3000,16 +3000,36 @@ "message": "Грешка при запазването на идентификационните данни. Вижте конзолата за подробности.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Премахване на секретния ключ" }, "passkeyRemoved": { "message": "Секретният ключ е премахнат" }, - "unassignedItemsBanner": { - "message": "Известие: неразпределените елементи на организацията вече не се виждат в изгледа с „Всички трезори“, а са достъпни само през Административната конзола. Добавете тези елементи към някоя колекция в Административната конзола, за да станат видими." + "unassignedItemsBannerNotice": { + "message": "Известие: неразпределените елементи в организацията вече няма да се виждат в изгледа с всички трезори, а са достъпни само през Административната конзола." }, - "unassignedItemsBannerSelfHost": { - "message": "Известие: от 2 май 2024г. неразпределените елементи на организациите вече няма се виждат в изгледа с „Всички трезори“, а ще бъдат достъпни само през Административната конзола. Добавете тези елементи към някоя колекция в Административната конзола, за да станат видими." + "unassignedItemsBannerSelfHostNotice": { + "message": "Известие: след 16 май 2024, неразпределените елементи в организацията вече няма да се виждат в изгледа с всички трезори, а ще бъдат достъпни само през Административната конзола." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Добавете тези елементи към колекция в", + "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": "за да ги направите видими.", + "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": "Административна конзола" + }, + "errorAssigningTargetCollection": { + "message": "Грешка при задаването на целева колекция." + }, + "errorAssigningTargetFolder": { + "message": "Грешка при задаването на целева папка." } } diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index 1bdaeef7c6..2ab44bf7e1 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "মূল পাসওয়ার্ড পরিবর্তন" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "ফিঙ্গারপ্রিন্ট ফ্রেজ", "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": "ফোল্ডার জোড়া হয়েছে" }, - "changeMasterPass": { - "message": "মূল পাসওয়ার্ড পরিবর্তন" - }, - "changeMasterPasswordConfirmation": { - "message": "আপনি bitwarden.com ওয়েব ভল্ট থেকে মূল পাসওয়ার্ডটি পরিবর্তন করতে পারেন। আপনি কি এখনই ওয়েবসাইটটি দেখতে চান?" - }, "twoStepLoginConfirmation": { "message": "দ্বি-পদক্ষেপ লগইন অন্য ডিভাইসে আপনার লগইনটি যাচাই করার জন্য সিকিউরিটি কী, প্রমাণীকরণকারী অ্যাপ্লিকেশন, এসএমএস, ফোন কল বা ই-মেইল ব্যাবহারের মাধ্যমে আপনার অ্যাকাউন্টকে আরও সুরক্ষিত করে। bitwarden.com ওয়েব ভল্টে দ্বি-পদক্ষেপের লগইন সক্ষম করা যাবে। আপনি কি এখনই ওয়েবসাইটটি দেখতে চান?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "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." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "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." + "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/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index 92a667afeb..8d2a25db9c 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/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?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "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." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "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." + "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/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index fc67602b60..30d4c4e636 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Canvia la contrasenya mestra" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Frase d'empremta digital", "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": "Carpeta afegida" }, - "changeMasterPass": { - "message": "Canvia la contrasenya mestra" - }, - "changeMasterPasswordConfirmation": { - "message": "Podeu canviar la contrasenya mestra a la caixa forta web de bitwarden.com. Voleu visitar el lloc web ara?" - }, "twoStepLoginConfirmation": { "message": "L'inici de sessió en dues passes fa que el vostre compte siga més segur, ja que obliga a verificar el vostre inici de sessió amb un altre dispositiu, com ara una clau de seguretat, una aplicació autenticadora, un SMS, una trucada telefònica o un correu electrònic. Es pot habilitar l'inici de sessió en dues passes a la caixa forta web de bitwarden.com. Voleu visitar el lloc web ara?" }, @@ -3000,16 +3000,36 @@ "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." }, + "success": { + "message": "Success" + }, "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." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "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." + "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/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index d989d25bf2..42a4f9edb1 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Změnit hlavní heslo" }, + "continueToWebApp": { + "message": "Pokračovat do webové aplikace?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Hlavní heslo můžete změnit ve webové aplikaci Bitwardenu." + }, "fingerprintPhrase": { "message": "Fráze otisku prstu", "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": "Složka byla přidána" }, - "changeMasterPass": { - "message": "Změnit hlavní heslo" - }, - "changeMasterPasswordConfirmation": { - "message": "Hlavní heslo si můžete změnit na webové stránce bitwarden.com. Chcete tuto stránku nyní otevřít?" - }, "twoStepLoginConfirmation": { "message": "Dvoufázové přihlášení činí Váš účet mnohem bezpečnějším díky nutnosti po každém úspěšném přihlášení zadat ověřovací kód získaný z bezpečnostního klíče, aplikace, SMS, telefonního hovoru nebo e-mailu. Dvoufázové přihlášení lze aktivovat na webové stránce bitwarden.com. Chcete tuto stránku nyní otevřít?" }, @@ -3000,16 +3000,36 @@ "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." }, + "success": { + "message": "Úspěch" + }, "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é." + "unassignedItemsBannerNotice": { + "message": "Upozornění: Nepřiřazené položky organizace již nejsou viditelné ve vašem zobrazení všech trezorů a jsou nyní přístupné pouze v konzoli správce." }, - "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é." + "unassignedItemsBannerSelfHostNotice": { + "message": "Upozornění: 16. květba 2024 již nebudou nepřiřazené položky organizace viditelné ve vašem zobrazení všech trezorů a budou přístupné pouze v konzoli správce." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Přiřadit tyto položky ke kolekci z", + "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": "aby byly viditelné.", + "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": "Konzole správce" + }, + "errorAssigningTargetCollection": { + "message": "Chyba při přiřazování cílové kolekce." + }, + "errorAssigningTargetFolder": { + "message": "Chyba při přiřazování cílové složky." } } diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index 79178bc9d5..52d3cf7d56 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Newid y prif gyfrinair" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Ymadrodd unigryw", "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": "Ffolder wedi'i hychwanegu" }, - "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?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "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." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "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." + "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/da/messages.json b/apps/browser/src/_locales/da/messages.json index d808d97412..08aebe98e1 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Skift hovedadgangskode" }, + "continueToWebApp": { + "message": "Fortsæt til web-app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Hovedadgangskoden kan ændres via Bitwarden web-appen." + }, "fingerprintPhrase": { "message": "Fingeraftrykssætning", "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": "Mappe tilføjet" }, - "changeMasterPass": { - "message": "Skift hovedadgangskode" - }, - "changeMasterPasswordConfirmation": { - "message": "Du kan ændre din hovedadgangskode i bitwarden.com web-boksen. Vil du besøge hjemmesiden nu?" - }, "twoStepLoginConfirmation": { "message": "To-trins login gør din konto mere sikker ved at kræve, at du verificerer dit login med en anden enhed, såsom en sikkerhedsnøgle, autentificeringsapp, SMS, telefonopkald eller e-mail. To-trins login kan aktiveres i bitwarden.com web-boksen. Vil du besøge hjemmesiden nu?" }, @@ -3000,16 +3000,36 @@ "message": "Fejl under import. Tjek konsollen for detaljer.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Gennemført" + }, "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." + "unassignedItemsBannerNotice": { + "message": "Bemærk: Utildelte organisationsemner er ikke længere synlige i Alle Bokse-visningen, men er kun tilgængelige via Admin-konsol." }, - "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." + "unassignedItemsBannerSelfHostNotice": { + "message": "Bemærk: Pr. 16. maj 2024 er utildelte organisationsemner er ikke længere synlige i Alle Bokse-visningen, men er kun tilgængelige via Admin-konsol." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Tildel disse emner til en samling via", + "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": "for at gøre dem synlige.", + "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-konsol" + }, + "errorAssigningTargetCollection": { + "message": "Fejl ved tildeling af målsamling." + }, + "errorAssigningTargetFolder": { + "message": "Fejl ved tildeling af målmappe." } } diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index deb92e992d..4edca5557c 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Master-Passwort ändern" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fingerabdruck-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": "Ordner hinzugefügt" }, - "changeMasterPass": { - "message": "Master-Passwort ändern" - }, - "changeMasterPasswordConfirmation": { - "message": "Du kannst dein Master-Passwort im Bitwarden.com Web-Tresor ändern. Möchtest du die Seite jetzt öffnen?" - }, "twoStepLoginConfirmation": { "message": "Mit der Zwei-Faktor-Authentifizierung wird dein Konto zusätzlich abgesichert, da jede Anmeldung mit einem anderen Gerät wie einem Sicherheitsschlüssel, einer Authentifizierungs-App, einer SMS, einem Anruf oder einer E-Mail verifiziert werden muss. Die Zwei-Faktor-Authentifizierung kann im bitwarden.com Web-Tresor aktiviert werden. Möchtest du die Website jetzt öffnen?" }, @@ -3000,16 +3000,36 @@ "message": "Fehler beim Speichern der Zugangsdaten. Details in der Konsole.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "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." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "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." + "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": "Fehler beim Zuweisen der Ziel-Sammlung." + }, + "errorAssigningTargetFolder": { + "message": "Fehler beim Zuweisen des Ziel-Ordners." } } diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index 36b14e447f..75cd5ef2fa 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Αλλαγή Κύριου Κωδικού" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Φράση Δακτυλικών Αποτυπωμάτων", "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": "Προστέθηκε φάκελος" }, - "changeMasterPass": { - "message": "Αλλαγή Κύριου Κωδικού" - }, - "changeMasterPasswordConfirmation": { - "message": "Μπορείτε να αλλάξετε τον κύριο κωδικό στο bitwarden.com. Θέλετε να επισκεφθείτε την ιστοσελίδα τώρα;" - }, "twoStepLoginConfirmation": { "message": "Η σύνδεση σε δύο βήματα καθιστά πιο ασφαλή τον λογαριασμό σας, απαιτώντας να επαληθεύσετε τα στοιχεία σας με μια άλλη συσκευή, όπως κλειδί ασφαλείας, εφαρμογή επαλήθευσης, μήνυμα SMS, τηλεφωνική κλήση ή email. Μπορείτε να ενεργοποιήσετε τη σύνδεση σε δύο βήματα στο bitwarden.com. Θέλετε να επισκεφθείτε την ιστοσελίδα τώρα;" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "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." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "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." + "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 1ac55feb42..b4b7b314db 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/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?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "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." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organisation items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "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." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organisation 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_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index cbe214f0b3..6dd78dc292 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/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": "Added folder" }, - "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 enabled on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "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." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "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." + "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/es/messages.json b/apps/browser/src/_locales/es/messages.json index ee5666f3cc..20b9a91814 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Cambiar contraseña maestra" }, + "continueToWebApp": { + "message": "¿Continuar a la aplicación web?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Puedes cambiar la contraseña maestra en la aplicación web de Bitwarden." + }, "fingerprintPhrase": { "message": "Frase de huella digital", "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": "Carpeta añadida" }, - "changeMasterPass": { - "message": "Cambiar contraseña maestra" - }, - "changeMasterPasswordConfirmation": { - "message": "Puedes cambiar tu contraseña maestra en la caja fuerte web de bitwarden.com. ¿Quieres visitar ahora el sitio web?" - }, "twoStepLoginConfirmation": { "message": "La autenticación en dos pasos hace que tu cuenta sea mucho más segura, requiriendo que introduzcas un código de seguridad de una aplicación de autenticación cada vez que accedes. La autenticación en dos pasos puede ser habilitada en la caja fuerte web de bitwarden.com. ¿Quieres visitar ahora el sitio web?" }, @@ -3000,16 +3000,36 @@ "message": "Se produjo un error al guardar las credenciales. Revise la consola para obtener detalles.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { - "message": "Remove passkey" + "message": "Eliminar passkey" }, "passkeyRemoved": { - "message": "Passkey removed" + "message": "Passkey eliminada" }, - "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." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "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." + "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/et/messages.json b/apps/browser/src/_locales/et/messages.json index ea1758468e..c832528847 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Muuda ülemparooli" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Sõrmejälje fraas", "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": "Kaust on lisatud" }, - "changeMasterPass": { - "message": "Muuda ülemparooli" - }, - "changeMasterPasswordConfirmation": { - "message": "Saad oma ülemparooli muuta bitwarden.com veebihoidlas. Soovid seda kohe teha?" - }, "twoStepLoginConfirmation": { "message": "Kaheastmeline kinnitamine aitab konto turvalisust tõsta. Lisaks paroolile pead kontole ligipääsemiseks kinnitama sisselogimise päringu SMS-ga, telefonikõnega, autentimise rakendusega või e-postiga. Kaheastmelist kinnitust saab sisse lülitada bitwarden.com veebihoidlas. Soovid seda kohe avada?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "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." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "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." + "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/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index 529a1e8127..9504f06c65 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Aldatu pasahitz nagusia" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Hatz-marka digitalaren esaldia", "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": "Karpeta gehituta" }, - "changeMasterPass": { - "message": "Aldatu pasahitz nagusia" - }, - "changeMasterPasswordConfirmation": { - "message": "Zure pasahitz nagusia alda dezakezu bitwarden.com webgunean. Orain joan nahi duzu webgunera?" - }, "twoStepLoginConfirmation": { "message": "Bi urratseko saio hasiera dela eta, zure kontua seguruagoa da, beste aplikazio/gailu batekin saioa hastea eskatzen baitizu; adibidez, segurtasun-gako, autentifikazio-aplikazio, SMS, telefono dei edo email bidez. Bi urratseko saio hasiera bitwarden.com webgunean aktibatu daiteke. Orain joan nahi duzu webgunera?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "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." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "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." + "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/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index 669eb151f4..c96c5c35cf 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "تغییر کلمه عبور اصلی" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "عبارت اثر انگشت", "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": "پوشه اضافه شد" }, - "changeMasterPass": { - "message": "تغییر کلمه عبور اصلی" - }, - "changeMasterPasswordConfirmation": { - "message": "شما می‌توانید کلمه عبور اصلی خود را در bitwarden.com تغییر دهید. آیا می‌خواهید از سایت بازدید کنید؟" - }, "twoStepLoginConfirmation": { "message": "ورود دو مرحله ای باعث می‌شود که حساب کاربری شما با استفاده از یک دستگاه دیگر مانند کلید امنیتی، برنامه احراز هویت، پیامک، تماس تلفنی و یا ایمیل، اعتبار خود را با ایمنی بیشتر اثبات کند. ورود دو مرحله ای می تواند در bitwarden.com فعال شود. آیا می‌خواهید از سایت بازدید کنید؟" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "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." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "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." + "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/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index 17aea532ba..b3602afd82 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Vaihda pääsalasana" }, + "continueToWebApp": { + "message": "Avataanko verkkosovellus?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Voit vaihtaa pääsalasanasi Bitwardenin verkkosovelluksessa." + }, "fingerprintPhrase": { "message": "Tunnistelauseke", "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": "Kansio lisätty" }, - "changeMasterPass": { - "message": "Vaihda pääsalasana" - }, - "changeMasterPasswordConfirmation": { - "message": "Voit vaihtaa pääsalasanasi bitwarden.com-verkkoholvissa. Haluatko käydä sivustolla nyt?" - }, "twoStepLoginConfirmation": { "message": "Kaksivaiheinen kirjautuminen parantaa tilisi suojausta vaatimalla kirjautumisen vahvistuksen salasanan lisäksi todennuslaitteen, ‑sovelluksen, tekstiviestin, puhelun tai sähköpostin avulla. Voit ottaa kaksivaiheisen kirjautumisen käyttöön bitwarden.com‑verkkoholvissa. Haluatko avata sen nyt?" }, @@ -3000,16 +3000,36 @@ "message": "Virhe tallennettaessa käyttäjätietoja. Näet isätietoja hallinnasta.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "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." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "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." + "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": "Määritä nämä kohteet kokoelmaan", + "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": ", jotta ne näkyvät.", + "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": "Hallintapaneelista" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index 42d5060e28..28418d984d 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Baguhin ang Master Password" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Hulmabig ng Hilik ng Dako", "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": "Idinagdag na folder" }, - "changeMasterPass": { - "message": "Palitan ang master password" - }, - "changeMasterPasswordConfirmation": { - "message": "Maaari mong palitan ang iyong master password sa bitwarden.com web vault. Gusto mo bang bisitahin ang website ngayon?" - }, "twoStepLoginConfirmation": { "message": "Ang two-step login ay nagpapagaan sa iyong account sa pamamagitan ng pag-verify sa iyong login sa isa pang device tulad ng security key, authenticator app, SMS, tawag sa telepono o email. Ang two-step login ay maaaring magawa sa bitwarden.com web vault. Gusto mo bang bisitahin ang website ngayon?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "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." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "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." + "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/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index 6cced1cb0d..206f07800d 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Changer le mot de passe principal" }, + "continueToWebApp": { + "message": "Poursuivre vers l'application web ?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Vous pouvez modifier votre mot de passe principal sur l'application web de Bitwarden." + }, "fingerprintPhrase": { "message": "Phrase d'empreinte", "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." @@ -525,13 +531,13 @@ "message": "Impossible de scanner le QR code à partir de la page web actuelle" }, "totpCaptureSuccess": { - "message": "Clé de l'Authentificateur ajoutée" + "message": "Clé Authenticator ajoutée" }, "totpCapture": { "message": "Scanner le QR code de l'authentificateur à partir de la page web actuelle" }, "copyTOTP": { - "message": "Copier la clé de l'Authentificateur (TOTP)" + "message": "Copier la clé Authenticator (TOTP)" }, "loggedOut": { "message": "Déconnecté" @@ -557,12 +563,6 @@ "addedFolder": { "message": "Dossier ajouté" }, - "changeMasterPass": { - "message": "Changer le mot de passe principal" - }, - "changeMasterPasswordConfirmation": { - "message": "Vous pouvez changer votre mot de passe principal depuis le coffre web de bitwarden.com. Voulez-vous visiter le site web maintenant ?" - }, "twoStepLoginConfirmation": { "message": "L'authentification à deux facteurs rend votre compte plus sûr en vous demandant de vérifier votre connexion avec un autre dispositif tel qu'une clé de sécurité, une application d'authentification, un SMS, un appel téléphonique ou un courriel. L'authentification à deux facteurs peut être configurée dans le coffre web de bitwarden.com. Voulez-vous visiter le site web maintenant ?" }, @@ -659,7 +659,7 @@ "message": "Liste les éléments des cartes de paiement sur la Page d'onglet pour faciliter la saisie automatique." }, "showIdentitiesCurrentTab": { - "message": "Afficher les identités sur la page d'onglet" + "message": "Afficher les identités sur la Page d'onglet" }, "showIdentitiesCurrentTabDesc": { "message": "Liste les éléments d'identité sur la Page d'onglet pour faciliter la saisie automatique." @@ -802,7 +802,7 @@ "message": "En savoir plus" }, "authenticatorKeyTotp": { - "message": "Clé de l'Authentificateur (TOTP)" + "message": "Clé Authenticator (TOTP)" }, "verificationCodeTotp": { "message": "Code de vérification (TOTP)" @@ -3000,16 +3000,36 @@ "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." }, + "success": { + "message": "Success" + }, "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." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "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." + "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/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index 023e03b834..95b880d1a5 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -14,28 +14,28 @@ "message": "Log in or create a new account to access your secure vault." }, "createAccount": { - "message": "Create account" + "message": "Crea unha conta" }, "login": { - "message": "Log in" + "message": "Iniciar sesión" }, "enterpriseSingleSignOn": { "message": "Enterprise single sign-on" }, "cancel": { - "message": "Cancel" + "message": "Cancelar" }, "close": { - "message": "Close" + "message": "Pechar" }, "submit": { "message": "Submit" }, "emailAddress": { - "message": "Email address" + "message": "Enderezo de correo electrónico" }, "masterPass": { - "message": "Master password" + "message": "Contrasinal mestre" }, "masterPassDesc": { "message": "The master password is the password you use to access your vault. It is very important that you do not forget your master password. There is no way to recover the password in the event that you forget it." @@ -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?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "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." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "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." + "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/he/messages.json b/apps/browser/src/_locales/he/messages.json index 1e633f5eb9..efb82e64ec 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "החלף סיסמה ראשית" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "סיסמת טביעת אצבע", "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": "נוספה תיקייה" }, - "changeMasterPass": { - "message": "החלף סיסמה ראשית" - }, - "changeMasterPasswordConfirmation": { - "message": "באפשרותך לשנות את הסיסמה הראשית שלך דרך הכספת באתר bitwarden.com. האם ברצונך לפתוח את האתר כעת?" - }, "twoStepLoginConfirmation": { "message": "התחברות בשני-שלבים הופכת את החשבון שלך למאובטח יותר בכך שאתה נדרש לוודא בכל כניסה בעזרת מכשיר אחר כדוגמת מפתח אבטחה, תוכנת אימות, SMS, שיחת טלפון, או אימייל. ניתן להפעיל את \"התחברות בשני-שלבים\" בכספת שבאתר bitwarden.com. האם ברצונך לפתוח את האתר כעת?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "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." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "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." + "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/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index 44f645bc47..9c13fa6efa 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/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": "जोड़ा गया फ़ोल्डर" }, - "changeMasterPass": { - "message": "Change Master Password" - }, - "changeMasterPasswordConfirmation": { - "message": "आप वेब वॉल्ट bitwarden.com पर अपना मास्टर पासवर्ड बदल सकते हैं।क्या आप अब वेबसाइट पर जाना चाहते हैं?" - }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to enter a security code from an authenticator app whenever you log in. Two-step login can be enabled on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "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." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "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." + "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/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index e74a72bc4f..ee4c4e7859 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Promjeni glavnu lozinku" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Jedinstvena fraza", "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": "Mapa dodana" }, - "changeMasterPass": { - "message": "Promjeni glavnu lozinku" - }, - "changeMasterPasswordConfirmation": { - "message": "Svoju glavnu lozinku možeš promijeniti na web trezoru. Želiš li sada posjetiti bitwarden.com?" - }, "twoStepLoginConfirmation": { "message": "Prijava dvostrukom autentifikacijom čini tvoj račun još sigurnijim tako što će zahtijevati da potvrdiš prijavu putem drugog uređaja pomoću sigurnosnog koda, autentifikatorske aplikacije, SMS-om, pozivom ili e-poštom. Prijavu dvostrukom autentifikacijom možeš omogućiti na web trezoru. Želiš li sada posjetiti bitwarden.com?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "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." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "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." + "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/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index cadd72a475..7d6a8a208b 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Mesterjelszó módosítása" }, + "continueToWebApp": { + "message": "Tovább a webes alkalmazáshoz?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "A mesterjelszó a Bitwarden webalkalmazásban módosítható." + }, "fingerprintPhrase": { "message": "Ujjlenyomat kifejezés", "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": "A mappa hozzáadásra került." }, - "changeMasterPass": { - "message": "Mesterjelszó módosítása" - }, - "changeMasterPasswordConfirmation": { - "message": "Mesterjelszavadat a bitwarden.com webes széfén tudod megváltoztatni. Szeretnéd meglátogatni a most a weboldalt?" - }, "twoStepLoginConfirmation": { "message": "A kétlépcsős bejelentkezés biztonságosabbá teszi a fiókot azáltal, hogy ellenőrizni kell a bejelentkezést egy másik olyan eszközzel mint például biztonsági kulcs, hitelesítő alkalmazás, SMS, telefon hívás vagy email. A kétlépcsős bejelentkezést a bitwarden.com webes széfben lehet engedélyezni. Felkeressük a webhelyet most?" }, @@ -3000,16 +3000,36 @@ "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." }, + "success": { + "message": "Success" + }, "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." + "unassignedItemsBannerNotice": { + "message": "Megjegyzés: A nem hozzárendelt szervezeti elemek már nem láthatók az Összes széf nézetben a különböző eszközökön és csak az Adminisztrátori konzolon keresztül érhetők el." }, - "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." + "unassignedItemsBannerSelfHostNotice": { + "message": "Megjegyzés: 2024. május 16-tól 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 az Adminisztrátori konzolon keresztül lesznek elérhetők." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Rendeljük hozzá ezeket az elemeket a gyűjteményhez", + "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": "a láthatósághoz.", + "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": "Adminisztrátori konzol" + }, + "errorAssigningTargetCollection": { + "message": "Hiba történt a célgyűjtemény hozzárendelése során." + }, + "errorAssigningTargetFolder": { + "message": "Hiba történt a célmappa hozzárendelése során." } } diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index 9907a7520c..92b60324ad 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Ubah Kata Sandi Utama" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Frasa Sidik Jari", "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": "Tambah Folder" }, - "changeMasterPass": { - "message": "Ubah Kata Sandi Utama" - }, - "changeMasterPasswordConfirmation": { - "message": "Anda dapat mengubah kata sandi utama Anda di brankas web bitwarden.com. Anda ingin mengunjungi situs web sekarang?" - }, "twoStepLoginConfirmation": { "message": "Info masuk dua langkah membuat akun Anda lebih aman dengan mengharuskan Anda memverifikasi info masuk Anda dengan peranti lain seperti kode keamanan, aplikasi autentikasi, SMK, panggilan telepon, atau email. Info masuk dua langkah dapat diaktifkan di brankas web bitwarden.com. Anda ingin mengunjungi situs web sekarang?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "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." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "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." + "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/it/messages.json b/apps/browser/src/_locales/it/messages.json index 65a5a1ad04..6887b134df 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Cambia password principale" }, + "continueToWebApp": { + "message": "Passa al sito web?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Puoi modificare la tua password principale sul sito web di Bitwarden." + }, "fingerprintPhrase": { "message": "Frase impronta", "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": "Cartella aggiunta" }, - "changeMasterPass": { - "message": "Cambia password principale" - }, - "changeMasterPasswordConfirmation": { - "message": "Puoi cambiare la tua password principale sulla cassaforte online di bitwarden.com. Vuoi visitare ora il sito?" - }, "twoStepLoginConfirmation": { "message": "La verifica in due passaggi rende il tuo account più sicuro richiedendoti di verificare il tuo login usando un altro dispositivo come una chiave di sicurezza, app di autenticazione, SMS, telefonata, o email. Può essere abilitata nella cassaforte web su bitwarden.com. Vuoi visitare il sito?" }, @@ -3000,16 +3000,36 @@ "message": "Errore durante il salvataggio delle credenziali. Controlla la console per più dettagli.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "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." + "unassignedItemsBannerNotice": { + "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." }, - "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." + "unassignedItemsBannerSelfHostNotice": { + "message": "Avviso: dal 16 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." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assegna questi elementi ad una raccolta dalla", + "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": "per renderli visibili.", + "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": "Console di amministrazione" + }, + "errorAssigningTargetCollection": { + "message": "Errore nell'assegnazione della raccolta di destinazione." + }, + "errorAssigningTargetFolder": { + "message": "Errore nell'assegnazione della cartella di destinazione." } } diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index 05fb7fe5de..744c76a509 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "マスターパスワードの変更" }, + "continueToWebApp": { + "message": "ウェブアプリに進みますか?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Bitwarden ウェブアプリでマスターパスワードを変更できます。" + }, "fingerprintPhrase": { "message": "パスフレーズ", "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": "フォルダを追加しました" }, - "changeMasterPass": { - "message": "マスターパスワードの変更" - }, - "changeMasterPasswordConfirmation": { - "message": "マスターパスワードは bitwarden.com ウェブ保管庫で変更できます。ウェブサイトを開きますか?" - }, "twoStepLoginConfirmation": { "message": "2段階認証を使うと、ログイン時にセキュリティキーや認証アプリ、SMS、電話やメールでの認証を必要にすることでアカウントをさらに安全に出来ます。2段階認証は bitwarden.com ウェブ保管庫で有効化できます。ウェブサイトを開きますか?" }, @@ -3000,16 +3000,36 @@ "message": "資格情報の保存中にエラーが発生しました。詳細はコンソールを確認してください。", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "成功" + }, "removePasskey": { "message": "パスキーを削除" }, "passkeyRemoved": { "message": "パスキーを削除しました" }, - "unassignedItemsBanner": { - "message": "注意: 割り当てられていない組織項目は、すべての保管庫のビューでは表示されなくなり、管理コンソールからのみアクセスできます。 管理コンソールからコレクションにこれらのアイテムを割り当てると、表示するようにできます。" + "unassignedItemsBannerNotice": { + "message": "注意: 割り当てられていない組織アイテムは、すべての保管庫ビューでは表示されなくなり、管理コンソールからのみアクセスできるようになります。" }, - "unassignedItemsBannerSelfHost": { - "message": "お知らせ:2024年5月2日に、 割り当てられていない組織アイテムはデバイス間のすべての保管庫ビューに表示されなくなり、管理コンソールからのみアクセス可能になります。 管理コンソールからコレクションにこれらのアイテムを割り当てると、表示できるようになります。" + "unassignedItemsBannerSelfHostNotice": { + "message": "お知らせ:2024年5月16日に、 割り当てられていない組織アイテムは、すべての保管庫ビューに表示されなくなり、管理コンソールからのみアクセス可能になります。" + }, + "unassignedItemsBannerCTAPartOne": { + "message": "これらのアイテムのコレクションへの割り当てを", + "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": "で実行すると表示できるようになります。", + "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": "管理コンソール" + }, + "errorAssigningTargetCollection": { + "message": "ターゲットコレクションの割り当てに失敗しました。" + }, + "errorAssigningTargetFolder": { + "message": "ターゲットフォルダーの割り当てに失敗しました。" } } diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index d67b88ba9c..ab7b84d22e 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/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?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "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." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "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." + "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/km/messages.json b/apps/browser/src/_locales/km/messages.json index 023e03b834..67e1f24787 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/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?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "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." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "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." + "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/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index 61cfadc762..9b363cba1f 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "ಮಾಸ್ಟರ್ ಪಾಸ್ವರ್ಡ್ ಬದಲಾಯಿಸಿ" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "ಫಿಂಗರ್ಪ್ರಿಂಟ್ ಫ್ರೇಸ್", "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": "ಫೋಲ್ಡರ್ ಸೇರಿಸಿ" }, - "changeMasterPass": { - "message": "ಮಾಸ್ಟರ್ ಪಾಸ್ವರ್ಡ್ ಬದಲಾಯಿಸಿ" - }, - "changeMasterPasswordConfirmation": { - "message": "ನಿಮ್ಮ ಮಾಸ್ಟರ್ ಪಾಸ್‌ವರ್ಡ್ ಅನ್ನು ನೀವು bitwarden.com ವೆಬ್ ವಾಲ್ಟ್‌ನಲ್ಲಿ ಬದಲಾಯಿಸಬಹುದು. ನೀವು ಈಗ ವೆಬ್‌ಸೈಟ್‌ಗೆ ಭೇಟಿ ನೀಡಲು ಬಯಸುವಿರಾ?" - }, "twoStepLoginConfirmation": { "message": "ಭದ್ರತಾ ಕೀ, ದೃಢೀಕರಣ ಅಪ್ಲಿಕೇಶನ್, ಎಸ್‌ಎಂಎಸ್, ಫೋನ್ ಕರೆ ಅಥವಾ ಇಮೇಲ್‌ನಂತಹ ಮತ್ತೊಂದು ಸಾಧನದೊಂದಿಗೆ ನಿಮ್ಮ ಲಾಗಿನ್ ಅನ್ನು ಪರಿಶೀಲಿಸುವ ಅಗತ್ಯವಿರುವ ಎರಡು ಹಂತದ ಲಾಗಿನ್ ನಿಮ್ಮ ಖಾತೆಯನ್ನು ಹೆಚ್ಚು ಸುರಕ್ಷಿತಗೊಳಿಸುತ್ತದೆ. ಬಿಟ್ವಾರ್ಡೆನ್.ಕಾಮ್ ವೆಬ್ ವಾಲ್ಟ್ನಲ್ಲಿ ಎರಡು-ಹಂತದ ಲಾಗಿನ್ ಅನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಬಹುದು. ನೀವು ಈಗ ವೆಬ್‌ಸೈಟ್‌ಗೆ ಭೇಟಿ ನೀಡಲು ಬಯಸುವಿರಾ?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "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." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "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." + "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/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index c71fbdf7a8..3e4f5769c0 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "마스터 비밀번호 변경" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "지문 구절", "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": "폴더 추가함" }, - "changeMasterPass": { - "message": "마스터 비밀번호 변경" - }, - "changeMasterPasswordConfirmation": { - "message": "bitwarden.com 웹 보관함에서 마스터 비밀번호를 바꿀 수 있습니다. 지금 웹 사이트를 방문하시겠습니까?" - }, "twoStepLoginConfirmation": { "message": "2단계 인증은 보안 키, 인증 앱, SMS, 전화 통화 등의 다른 기기로 사용자의 로그인 시도를 검증하여 사용자의 계정을 더욱 안전하게 만듭니다. 2단계 인증은 bitwarden.com 웹 보관함에서 활성화할 수 있습니다. 지금 웹 사이트를 방문하시겠습니까?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "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." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "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." + "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/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index 0fc146c250..c690c2727b 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Keisti pagrindinį slaptažodį" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Pirštų atspaudų frazė", "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": "Katalogas pridėtas" }, - "changeMasterPass": { - "message": "Keisti pagrindinį slaptažodį" - }, - "changeMasterPasswordConfirmation": { - "message": "Pagrindinį slaptažodį galite pakeisti „bitwarden.com“ žiniatinklio saugykloje. Ar norite dabar apsilankyti svetainėje?" - }, "twoStepLoginConfirmation": { "message": "Prisijungus dviem veiksmais, jūsų paskyra tampa saugesnė, reikalaujant patvirtinti prisijungimą naudojant kitą įrenginį, pvz., saugos raktą, autentifikavimo programėlę, SMS, telefono skambutį ar el. paštą. Dviejų žingsnių prisijungimą galima įjungti „bitwarden.com“ interneto saugykloje. Ar norite dabar apsilankyti svetainėje?" }, @@ -3000,16 +3000,36 @@ "message": "Klaida išsaugant kredencialus. Išsamesnės informacijos patikrink konsolėje.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "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." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "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." + "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/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index efac417556..d26b10a30c 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Mainīt galveno paroli" }, + "continueToWebApp": { + "message": "Pāriet uz tīmekļa lietotni?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Savu galveno paroli var mainīt Bitwarden tīmekļa lietotnē." + }, "fingerprintPhrase": { "message": "Atpazīšanas vārdkopa", "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": "Pievienoja mapi" }, - "changeMasterPass": { - "message": "Mainīt galveno paroli" - }, - "changeMasterPasswordConfirmation": { - "message": "Galveno paroli ir iespējams mainīt bitwarden.com tīmekļa glabātavā. Vai tagad apmeklēt tīmekļvietni?" - }, "twoStepLoginConfirmation": { "message": "Divpakāpju pieteikšanās padara kontu krietni drošāku, pieprasot apstiprināt pieteikšanos ar tādu citu ierīču vai pakalpojumu starpniecību kā drošības atslēga, autentificētāja lietotne, īsziņa, tālruņa zvans vai e-pasts. Divpakāpju pieteikšanos var iespējot bitwarden.com tīmekļa glabātavā. Vai tagad apmeklēt tīmekļvietni?" }, @@ -3000,16 +3000,36 @@ "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." }, + "success": { + "message": "Success" + }, "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." + "unassignedItemsBannerNotice": { + "message": "Jāņem vērā: nepiešķirti apvienības vienumi vairs nav redzami skatā \"Visas glabātavas\" un ir pieejami tikai pārvaldības konsolē." }, - "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." + "unassignedItemsBannerSelfHostNotice": { + "message": "Jāņem vērā: no 2024. gada 16. maija nepiešķirti apvienības vienumi vairs nebūs redzami skatā \"Visas glabātavas\" un būs pieejami tikai pārvaldības konsolē." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Piešķirt šos vienumus krājumam", + "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": "lai padarītu tos redzamus.", + "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": "pārvaldības konsolē," + }, + "errorAssigningTargetCollection": { + "message": "Kļūda mērķa krājuma piešķiršanā." + }, + "errorAssigningTargetFolder": { + "message": "Kļūda mērķa mapes piešķiršanā." } } diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index 9b66e6f0d6..1db5f6458b 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "പ്രാഥമിക പാസ്‌വേഡ് മാറ്റുക" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "ഫിംഗർപ്രിന്റ് ഫ്രേസ്‌", "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": "ചേർക്കപ്പെട്ട ഫോൾഡർ" }, - "changeMasterPass": { - "message": "പ്രാഥമിക പാസ്‌വേഡ് മാറ്റുക" - }, - "changeMasterPasswordConfirmation": { - "message": "തങ്ങൾക്കു ബിറ്റ് വാർഡൻ വെബ് വാൾട്ടിൽ പ്രാഥമിക പാസ്‌വേഡ് മാറ്റാൻ സാധിക്കും.വെബ്സൈറ്റ് ഇപ്പോൾ സന്ദർശിക്കാൻ ആഗ്രഹിക്കുന്നുവോ?" - }, "twoStepLoginConfirmation": { "message": "സുരക്ഷാ കീ, ഓതന്റിക്കേറ്റർ അപ്ലിക്കേഷൻ, SMS, ഫോൺ കോൾ അല്ലെങ്കിൽ ഇമെയിൽ പോലുള്ള മറ്റൊരു ഉപകരണം ഉപയോഗിച്ച് തങ്ങളുടെ ലോഗിൻ സ്ഥിരീകരിക്കാൻ ആവശ്യപ്പെടുന്നതിലൂടെ രണ്ട്-ഘട്ട ലോഗിൻ തങ്ങളുടെ അക്കൗണ്ടിനെ കൂടുതൽ സുരക്ഷിതമാക്കുന്നു. bitwarden.com വെബ് വാൾട്ടിൽ രണ്ട്-ഘട്ട ലോഗിൻ പ്രവർത്തനക്ഷമമാക്കാനാകും.തങ്ങള്ക്കു ഇപ്പോൾ വെബ്സൈറ്റ് സന്ദർശിക്കാൻ ആഗ്രഹമുണ്ടോ?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "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." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "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." + "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/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index f9f37b2511..06cf84efff 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "मुख्य पासवर्ड बदला" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "अंगुलिमुद्रा वाक्यांश", "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?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "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." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "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." + "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/my/messages.json b/apps/browser/src/_locales/my/messages.json index 023e03b834..67e1f24787 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/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?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "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." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "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." + "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/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index 82d847ff0f..220fe95e23 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Endre hovedpassordet" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fingeravtrykksfrase", "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": "La til en mappe" }, - "changeMasterPass": { - "message": "Endre hovedpassordet" - }, - "changeMasterPasswordConfirmation": { - "message": "Du kan endre superpassordet ditt på bitwarden.net-netthvelvet. Vil du besøke det nettstedet nå?" - }, "twoStepLoginConfirmation": { "message": "2-trinnsinnlogging gjør kontoen din mer sikker, ved å kreve at du verifiserer din innlogging med en annen enhet, f.eks. en autentiseringsapp, SMS, e-post, telefonsamtale, eller sikkerhetsnøkkel. 2-trinnsinnlogging kan aktiveres i netthvelvet ditt på bitwarden.com. Vil du besøke bitwarden.com nå?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "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." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "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." + "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/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index 023e03b834..67e1f24787 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/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?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "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." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "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." + "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/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index 13d59c4546..808e599e70 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Hoofdwachtwoord wijzigen" }, + "continueToWebApp": { + "message": "Doorgaan naar web-app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Je kunt je hoofdwachtwoord wijzigen in de Bitwarden-webapp." + }, "fingerprintPhrase": { "message": "Vingerafdrukzin", "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": "Map is toegevoegd" }, - "changeMasterPass": { - "message": "Hoofdwachtwoord wijzigen" - }, - "changeMasterPasswordConfirmation": { - "message": "Je kunt je hoofdwachtwoord wijzigen in de kluis op bitwarden.com. Wil je de website nu bezoeken?" - }, "twoStepLoginConfirmation": { "message": "Tweestapsaanmelding beschermt je account door je inlogpoging te bevestigen met een ander apparaat zoals een beveiligingscode, authenticatie-app, SMS, spraakoproep of e-mail. Je kunt Tweestapsaanmelding inschakelen in de webkluis op bitwarden.com. Wil je de website nu bezoeken?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Succes" + }, "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." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the 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." + "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": "Fout bij toewijzen doelverzameling." + }, + "errorAssigningTargetFolder": { + "message": "Fout bij toewijzen doelmap." } } diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index 023e03b834..67e1f24787 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/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?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "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." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "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." + "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/or/messages.json b/apps/browser/src/_locales/or/messages.json index 023e03b834..67e1f24787 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/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?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "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." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "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." + "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/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index e4b97ec956..1ef79bac42 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Zmień hasło główne" }, + "continueToWebApp": { + "message": "Kontynuować do aplikacji internetowej?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Możesz zmienić swoje hasło główne w aplikacji internetowej Bitwarden." + }, "fingerprintPhrase": { "message": "Unikalny identyfikator konta", "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 został dodany" }, - "changeMasterPass": { - "message": "Zmień hasło główne" - }, - "changeMasterPasswordConfirmation": { - "message": "Hasło główne możesz zmienić na stronie sejfu bitwarden.com. Czy chcesz przejść do tej strony?" - }, "twoStepLoginConfirmation": { "message": "Logowanie dwustopniowe sprawia, że konto jest bardziej bezpieczne poprzez wymuszenie potwierdzenia logowania z innego urządzenia, takiego jak z klucza bezpieczeństwa, aplikacji uwierzytelniającej, wiadomości SMS, telefonu lub adresu e-mail. Logowanie dwustopniowe możesz włączyć w sejfie internetowym bitwarden.com. Czy chcesz przejść do tej strony?" }, @@ -2709,7 +2709,7 @@ "message": "Otwórz rozszerzenie w nowym oknie, aby dokończyć logowanie." }, "popoutExtension": { - "message": "Popout extension" + "message": "Otwórz rozszerzenie w nowym oknie" }, "launchDuo": { "message": "Uruchom DUO" @@ -2822,7 +2822,7 @@ "message": "Wybierz dane logowania do których przypisać passkey" }, "passkeyItem": { - "message": "Passkey Item" + "message": "Element Passkey" }, "overwritePasskey": { "message": "Zastąpić passkey?" @@ -3000,16 +3000,36 @@ "message": "Błąd podczas zapisywania danych logowania. Sprawdź konsolę, aby uzyskać szczegóły.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Sukces" + }, "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." + "unassignedItemsBannerNotice": { + "message": "Uwaga: Nieprzypisane elementy organizacji nie są już widoczne w widoku Wszystkie sejfy i są teraz dostępne tylko przez Konsolę Administracyjną." }, - "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." + "unassignedItemsBannerSelfHostNotice": { + "message": "Uwaga: 16 maja 2024 r. nieprzypisana elementy w organizacji nie będą już widoczne w widoku Wszystkie sejfy i będą dostępne tylko przez Konsolę Administracyjną." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Przypisz te elementy do kolekcji z", + "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": ", aby były widoczne.", + "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": "Konsola Administracyjna" + }, + "errorAssigningTargetCollection": { + "message": "Wystąpił błąd podczas przypisywania kolekcji." + }, + "errorAssigningTargetFolder": { + "message": "Wystąpił błąd podczas przypisywania folderu." } } diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index a4e0688e3d..9322e680d2 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Alterar Senha Mestra" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Frase Biométrica", "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": "Pasta adicionada" }, - "changeMasterPass": { - "message": "Alterar Senha Mestra" - }, - "changeMasterPasswordConfirmation": { - "message": "Você pode alterar a sua senha mestra no cofre web em bitwarden.com. Você deseja visitar o site agora?" - }, "twoStepLoginConfirmation": { "message": "O login de duas etapas torna a sua conta mais segura ao exigir que digite um código de segurança de um aplicativo de autenticação quando for iniciar a sessão. O login de duas etapas pode ser ativado no cofre web bitwarden.com. Deseja visitar o site agora?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "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." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "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." + "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/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index c35531e445..d4fdc1be81 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Alterar palavra-passe mestra" }, + "continueToWebApp": { + "message": "Continuar para a aplicação Web?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Pode alterar a sua palavra-passe mestra na aplicação Web Bitwarden." + }, "fingerprintPhrase": { "message": "Frase de impressão digital", "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": "Pasta adicionada" }, - "changeMasterPass": { - "message": "Alterar palavra-passe mestra" - }, - "changeMasterPasswordConfirmation": { - "message": "Pode alterar o seu endereço de e-mail no cofre do site bitwarden.com. Deseja visitar o site agora?" - }, "twoStepLoginConfirmation": { "message": "A verificação de dois passos torna a sua conta mais segura, exigindo que verifique o seu início de sessão com outro dispositivo, como uma chave de segurança, aplicação de autenticação, SMS, chamada telefónica ou e-mail. A verificação de dois passos pode ser configurada em bitwarden.com. Pretende visitar o site agora?" }, @@ -3000,16 +3000,36 @@ "message": "Erro ao guardar as credenciais. Verifique a consola para obter detalhes.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Com sucesso" + }, "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." + "unassignedItemsBannerNotice": { + "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." }, - "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." + "unassignedItemsBannerSelfHostNotice": { + "message": "Aviso: A 16 de maio de 2024, os itens da organização não atribuídos deixarão de estar visíveis na vista Todos os cofres e só estarão acessíveis através da consola de administração." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Atribua estes itens a uma coleção a partir da", + "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": "para os tornar visíveis.", + "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": "Consola de administração" + }, + "errorAssigningTargetCollection": { + "message": "Erro ao atribuir a coleção de destino." + }, + "errorAssigningTargetFolder": { + "message": "Erro ao atribuir a pasta de destino." } } diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index 885d70ca93..5e8c82e70b 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Schimbare parolă principală" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fraza amprentă", "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": "Dosar adăugat" }, - "changeMasterPass": { - "message": "Schimbare parolă principală" - }, - "changeMasterPasswordConfirmation": { - "message": "Puteți modifica parola principală în seiful web bitwarden.com. Doriți să vizitați saitul acum?" - }, "twoStepLoginConfirmation": { "message": "Autentificarea în două etape vă face contul mai sigur, prin solicitarea unei verificări de autentificare cu un alt dispozitiv, cum ar fi o cheie de securitate, o aplicație de autentificare, un SMS, un apel telefonic sau un e-mail. Autentificarea în două etape poate fi configurată în seiful web bitwarden.com. Doriți să vizitați site-ul web acum?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "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." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "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." + "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/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index 69d9ca200f..046fe2b931 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Изменить мастер-пароль" }, + "continueToWebApp": { + "message": "Перейти к веб-приложению?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Изменить мастер-пароль можно в веб-приложении Bitwarden." + }, "fingerprintPhrase": { "message": "Фраза отпечатка", "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": "Папка добавлена" }, - "changeMasterPass": { - "message": "Изменить мастер-пароль" - }, - "changeMasterPasswordConfirmation": { - "message": "Вы можете изменить свой мастер-пароль на bitwarden.com. Перейти на сайт сейчас?" - }, "twoStepLoginConfirmation": { "message": "Двухэтапная аутентификация делает аккаунт более защищенным, поскольку требуется подтверждение входа при помощи другого устройства, например, ключа безопасности, приложения-аутентификатора, SMS, телефонного звонка или электронной почты. Двухэтапная аутентификация включается на bitwarden.com. Перейти на сайт сейчас?" }, @@ -3000,16 +3000,36 @@ "message": "Ошибка сохранения учетных данных. Проверьте консоль для получения подробной информации.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Успешно" + }, "removePasskey": { "message": "Удалить passkey" }, "passkeyRemoved": { "message": "Passkey удален" }, - "unassignedItemsBanner": { - "message": "Обратите внимание: неприсвоенные элементы организации больше не видны в представлении \"Все хранилища\" и доступны только через консоль администратора. Назначьте эти элементы коллекции в консоли администратора, чтобы сделать их видимыми." + "unassignedItemsBannerNotice": { + "message": "Уведомление: Неприсвоенные элементы организации больше не видны в представлении \"Все хранилища\" и доступны только через консоль администратора." }, - "unassignedItemsBannerSelfHost": { - "message": "Уведомление: 2 мая 2024 года неприсвоенные элементы организации больше не будут видны в представлении \"Все хранилища\" и будут доступны только через консоль администратора. Назначьте эти элементы коллекции в консоли администратора, чтобы сделать их видимыми." + "unassignedItemsBannerSelfHostNotice": { + "message": "Уведомление: с 16 мая 2024 года не назначенные элементы организации больше не будут видны в представлении \"Все хранилища\" и будут доступны только через консоль администратора." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Назначьте эти элементы в коллекцию из", + "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": "чтобы сделать их видимыми.", + "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": "консоли администратора" + }, + "errorAssigningTargetCollection": { + "message": "Ошибка при назначении целевой коллекции." + }, + "errorAssigningTargetFolder": { + "message": "Ошибка при назначении целевой папки." } } diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index fb026226bb..e576a50dd7 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "ප්රධාන මුරපදය වෙනස්" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "ඇඟිලි සලකුණු වාක්ය ඛණ්ඩය", "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": "එකතු කරන ලද ෆෝල්ඩරය" }, - "changeMasterPass": { - "message": "ප්රධාන මුරපදය වෙනස්" - }, - "changeMasterPasswordConfirmation": { - "message": "bitwarden.com වෙබ් සුරක්ෂිතාගාරයේ ඔබේ ප්රධාන මුරපදය වෙනස් කළ හැකිය. ඔබට දැන් වෙබ් අඩවියට පිවිසීමට අවශ්යද?" - }, "twoStepLoginConfirmation": { "message": "ආරක්ෂක යතුරක්, සත්යාපන යෙදුම, කෙටි පණිවුඩ, දුරකථන ඇමතුමක් හෝ විද්යුත් තැපෑල වැනි වෙනත් උපාංගයක් සමඟ ඔබේ පිවිසුම සත්යාපනය කිරීමට ඔබට අවශ්ය වීමෙන් ද්වි-පියවර පිවිසුම ඔබගේ ගිණුම වඩාත් සුරක්ෂිත කරයි. බිට්වොන්.com වෙබ් සුරක්ෂිතාගාරයේ ද්වි-පියවර පිවිසුම සක්රීය කළ හැකිය. ඔබට දැන් වෙබ් අඩවියට පිවිසීමට අවශ්යද?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "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." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "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." + "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/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index a7948d78f3..e34fa525be 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Zmeniť hlavné heslo" }, + "continueToWebApp": { + "message": "Pokračovať vo webovej aplikácii?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Hlavné heslo si môžete zmeniť vo webovej aplikácii Bitwarden." + }, "fingerprintPhrase": { "message": "Fráza odtlačku", "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": "Pridaný priečinok" }, - "changeMasterPass": { - "message": "Zmeniť hlavné heslo" - }, - "changeMasterPasswordConfirmation": { - "message": "Teraz si môžete zmeniť svoje hlavné heslo vo webovom trezore bitwarden.com. Chcete navštíviť túto stránku teraz?" - }, "twoStepLoginConfirmation": { "message": "Dvojstupňové prihlasovanie robí váš účet bezpečnejším vďaka vyžadovaniu bezpečnostného kódu z overovacej aplikácie vždy, keď sa prihlásite. Dvojstupňové prihlasovanie môžete povoliť vo webovom trezore bitwarden.com. Chcete navštíviť túto stránku teraz?" }, @@ -3000,16 +3000,36 @@ "message": "Chyba pri ukladaní prihlasovacích údajov. Viac informácii nájdete v konzole.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Úspech" + }, "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." + "unassignedItemsBannerNotice": { + "message": "Upozornenie: Nepriradené položky organizácie už nie sú viditeľné v zobrazení Všetky trezory a sú prístupné iba cez Správcovskú konzolu." }, - "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." + "unassignedItemsBannerSelfHostNotice": { + "message": "Upozornenie: 16. mája 2024 nepriradené položky organizácie už nebudú viditeľné v zobrazení Všetky trezory a budú prístupné iba cez Správcovskú konzolu." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Priradiť tieto položky do zbierky zo", + "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": ", aby boli viditeľné.", + "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": "Správcovská konzola" + }, + "errorAssigningTargetCollection": { + "message": "Chyba pri priraďovaní cieľovej kolekcie." + }, + "errorAssigningTargetFolder": { + "message": "Chyba pri priraďovaní cieľového priečinka." } } diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index 2fac491c9c..c7f8ed0481 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Spremeni glavno geslo" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Identifikacijsko geslo", "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": "Mapa dodana" }, - "changeMasterPass": { - "message": "Spremeni glavno geslo" - }, - "changeMasterPasswordConfirmation": { - "message": "Svoje glavno geslo lahko spremenite v Bitwardnovem spletnem trezorju. Želite zdaj obiskati Bitwardnovo spletno stran?" - }, "twoStepLoginConfirmation": { "message": "Avtentikacija v dveh korakih dodatno varuje vaš račun, saj zahteva, da vsakokratno prijavo potrdite z drugo napravo, kot je varnostni ključ, aplikacija za preverjanje pristnosti, SMS, telefonski klic ali e-pošta. Avtentikacijo v dveh korakih lahko omogočite v spletnem trezorju bitwarden.com. Ali želite spletno stran obiskati sedaj?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "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." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "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." + "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/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index 6ec1b6181b..8d1ee8264f 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Промени главну лозинку" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Сигурносна Фраза Сефа", "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": "Фасцикла додата" }, - "changeMasterPass": { - "message": "Промени главну лозинку" - }, - "changeMasterPasswordConfirmation": { - "message": "Можете променити главну лозинку у Вашем сефу на bitwarden.com. Да ли желите да посетите веб страницу сада?" - }, "twoStepLoginConfirmation": { "message": "Пријава у два корака чини ваш налог сигурнијим захтевом да верификујете своје податке помоћу другог уређаја, као што су безбедносни кључ, апликација, СМС-а, телефонски позив или имејл. Пријављивање у два корака може се омогућити на веб сефу. Да ли желите да посетите веб страницу сада?" }, @@ -3000,16 +3000,36 @@ "message": "Грешка при чувању акредитива. Проверите конзолу за детаље.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Уклонити приступачни кључ" }, "passkeyRemoved": { "message": "Приступачни кључ је уклоњен" }, - "unassignedItemsBanner": { - "message": "Напомена: Недодељене ставке организације више нису видљиве у приказу Сви сефови и доступне су само преко Админ конзоле. Доделите ове ставке колекцији са Админ конзолом да бисте их учинили видљивим." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Обавештење: 2. маја 2024. недодељене ставке организације више неће бити видљиве у приказу Сви сефови и биће доступне само преко Админ конзоле. Доделите ове ставке колекцији са Админ конзолом да бисте их учинили видљивим." + "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/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index d798b98ea0..ebe5e6d281 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Ändra huvudlösenord" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fingeravtrycksfras", "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": "Lade till mapp" }, - "changeMasterPass": { - "message": "Ändra huvudlösenord" - }, - "changeMasterPasswordConfirmation": { - "message": "Du kan ändra ditt huvudlösenord på bitwardens webbvalv. Vill du besöka webbplatsen nu?" - }, "twoStepLoginConfirmation": { "message": "Tvåstegsverifiering gör ditt konto säkrare genom att kräva att du verifierar din inloggning med en annan enhet, t.ex. en säkerhetsnyckel, autentiseringsapp, SMS, telefonsamtal eller e-post. Tvåstegsverifiering kan aktiveras i Bitwardens webbvalv. Vill du besöka webbplatsen nu?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "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." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "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." + "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/te/messages.json b/apps/browser/src/_locales/te/messages.json index 023e03b834..67e1f24787 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/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?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "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." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "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." + "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/th/messages.json b/apps/browser/src/_locales/th/messages.json index 827ca72854..718440438e 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/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": "เพิ่มโฟลเดอร์แล้ว" }, - "changeMasterPass": { - "message": "Change Master Password" - }, - "changeMasterPasswordConfirmation": { - "message": "คุณสามารถเปลี่ยนรหัสผ่านหลักได้ที่เว็บตู้เซฟ bitwarden.com คุณต้องการเปิดเว็บไซต์เลยหรือไม่?" - }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to enter a security code from an authenticator app whenever you log in. Two-step login can be enabled on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "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." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "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." + "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/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index cd0e12e6b0..7633b47258 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Ana parolayı değiştir" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Parmak izi ifadesi", "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": "Klasör eklendi" }, - "changeMasterPass": { - "message": "Ana parolayı değiştir" - }, - "changeMasterPasswordConfirmation": { - "message": "Ana parolanızı bitwarden.com web kasası üzerinden değiştirebilirsiniz. Siteye gitmek ister misiniz?" - }, "twoStepLoginConfirmation": { "message": "İki aşamalı giriş, hesabınıza girererken işlemi bir güvenlik anahtarı, şifrematik uygulaması, SMS, telefon araması veya e-posta gibi ek bir yöntemle doğrulamanızı isteyerek hesabınızın güvenliğini artırır. İki aşamalı giriş özelliğini bitwarden.com web kasası üzerinden etkinleştirebilirsiniz. Şimdi siteye gitmek ister misiniz?" }, @@ -3000,16 +3000,36 @@ "message": "Kimlik bilgileri kaydedilirken hata oluştu. Ayrıntılar için konsolu kontrol edin.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "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." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "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." + "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/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index 4820860de2..b09d966142 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Змінити головний пароль" }, + "continueToWebApp": { + "message": "Продовжити у вебпрограмі?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Ви можете змінити головний пароль у вебпрограмі Bitwarden." + }, "fingerprintPhrase": { "message": "Фраза відбитка", "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": "Теку додано" }, - "changeMasterPass": { - "message": "Змінити головний пароль" - }, - "changeMasterPasswordConfirmation": { - "message": "Ви можете змінити головний пароль в сховищі на bitwarden.com. Хочете перейти на вебсайт зараз?" - }, "twoStepLoginConfirmation": { "message": "Двоетапна перевірка дає змогу надійніше захистити ваш обліковий запис, вимагаючи підтвердження входу з використанням іншого пристрою, наприклад, за допомогою ключа безпеки, програми автентифікації, SMS, телефонного виклику, або е-пошти. Ви можете налаштувати двоетапну перевірку в сховищі на bitwarden.com. Хочете перейти на вебсайт зараз?" }, @@ -3000,16 +3000,36 @@ "message": "Помилка збереження облікових даних. Перегляньте подробиці в консолі.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Успішно" + }, "removePasskey": { "message": "Вилучити ключ доступу" }, "passkeyRemoved": { "message": "Ключ доступу вилучено" }, - "unassignedItemsBanner": { - "message": "Увага: непризначені елементи організації більше не видимі у поданні \"Усі сховища\" і доступні лише в консолі адміністратора. Щоб зробити їх видимими, призначте ці елементи збірці в консолі адміністратора." + "unassignedItemsBannerNotice": { + "message": "Примітка: непризначені елементи організації більше не видимі у поданні \"Усі сховища\" і доступні лише в консолі адміністратора." }, - "unassignedItemsBannerSelfHost": { - "message": "Сповіщення: 2 травня 2024 року, непризначені елементи організації більше не будуть видимі в поданні \"Усі сховища\", і будуть доступні лише через консоль адміністратора. Щоб зробити їх видимими, призначте ці елементи збірці в консолі адміністратора." + "unassignedItemsBannerSelfHostNotice": { + "message": "Примітка: 16 травня 2024 року непризначені елементи організації більше не будуть видимі у поданні \"Усі сховища\" і будуть доступні лише через консоль адміністратора." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Призначте ці елементи збірці в", + "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": "щоб зробити їх видимими.", + "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": "консолі адміністратора," + }, + "errorAssigningTargetCollection": { + "message": "Помилка призначення цільової збірки." + }, + "errorAssigningTargetFolder": { + "message": "Помилка призначення цільової теки." } } diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index 234e60e756..af518878b9 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Thay đổi mật khẩu chính" }, + "continueToWebApp": { + "message": "Tiếp tục tới ứng dụng web?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Bạn có thể thay đổi mật khẩu chính của mình trên Bitwarden bản web." + }, "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." @@ -415,7 +421,7 @@ "message": "Khóa ngay" }, "lockAll": { - "message": "Lock all" + "message": "Khóa tất cả" }, "immediately": { "message": "Ngay lập tức" @@ -494,10 +500,10 @@ "message": "Tài khoản mới của bạn đã được tạo! Bạn có thể đăng nhập từ bây giờ." }, "youSuccessfullyLoggedIn": { - "message": "You successfully logged in" + "message": "Bạn đã đăng nhập thành công" }, "youMayCloseThisWindow": { - "message": "You may close this window" + "message": "Bạn có thể đóng cửa sổ này" }, "masterPassSent": { "message": "Chúng tôi đã gửi cho bạn email có chứa gợi ý mật khẩu chính của bạn." @@ -522,16 +528,16 @@ "message": "Không thể tự động điền mục đã chọn trên trang này. Hãy thực hiện sao chép và dán thông tin một cách thủ công." }, "totpCaptureError": { - "message": "Unable to scan QR code from the current webpage" + "message": "Không thể quét mã QR từ trang web hiện tại" }, "totpCaptureSuccess": { - "message": "Authenticator key added" + "message": "Đã thêm khóa xác thực" }, "totpCapture": { - "message": "Scan authenticator QR code from current webpage" + "message": "Quét mã QR xác thực từ trang web hiện tại" }, "copyTOTP": { - "message": "Copy Authenticator key (TOTP)" + "message": "Sao chép khóa Authenticator (TOTP)" }, "loggedOut": { "message": "Đã đăng xuất" @@ -557,12 +563,6 @@ "addedFolder": { "message": "Đã thêm thư mục" }, - "changeMasterPass": { - "message": "Thay đổi mật khẩu chính" - }, - "changeMasterPasswordConfirmation": { - "message": "Bạn có thể thay đổi mật khẩu chính trong trang web kho lưu trữ của Bitwarden. Bạn có muốn truy cập trang web ngay bây giờ không?" - }, "twoStepLoginConfirmation": { "message": "Xác thực hai lớp giúp cho tài khoản của bạn an toàn hơn bằng cách yêu cầu bạn xác minh thông tin đăng nhập của bạn bằng một thiết bị khác như khóa bảo mật, ứng dụng xác thực, SMS, cuộc gọi điện thoại hoặc email. Bạn có thể bật xác thực hai lớp trong kho bitwarden nền web. Bạn có muốn ghé thăm trang web bây giờ?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "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." + "unassignedItemsBannerNotice": { + "message": "Lưu ý: Các mục tổ chức chưa được chỉ định sẽ không còn hiển thị trong chế độ xem Tất cả Vault và chỉ có thể truy cập được qua Bảng điều khiển dành cho quản trị viên." }, - "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." + "unassignedItemsBannerSelfHostNotice": { + "message": "Lưu ý: Vào ngày 16 tháng 5 năm 2024, các mục tổ chức chưa được chỉ định sẽ không còn hiển thị trong chế độ xem Tất cả Vault và sẽ chỉ có thể truy cập được qua Bảng điều khiển dành cho quản trị viên." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Gán các mục này vào một bộ sưu tập từ", + "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": "để làm cho chúng hiển thị.", + "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": "Bảng điều khiển dành cho quản trị viên" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index 519313df81..5842d4f4c1 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "更改主密码" }, + "continueToWebApp": { + "message": "前往网页 App 吗?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "指纹短语", "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": "文件夹已添加" }, - "changeMasterPass": { - "message": "修改主密码" - }, - "changeMasterPasswordConfirmation": { - "message": "您可以在 bitwarden.com 网页版密码库修改主密码。您现在要访问这个网站吗?" - }, "twoStepLoginConfirmation": { "message": "两步登录要求您从其他设备(例如安全钥匙、验证器 App、短信、电话或者电子邮件)来验证您的登录,这能使您的账户更加安全。两步登录需要在 bitwarden.com 网页版密码库中设置。现在访问此网站吗?" }, @@ -3000,16 +3000,36 @@ "message": "保存凭据时出错。检查控制台以获取详细信息。", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "移除通行密钥" }, "passkeyRemoved": { "message": "通行密钥已移除" }, - "unassignedItemsBanner": { - "message": "注意:未分配的组织项目在「所有密码库」视图中不再可见,只能通过管理控制台访问。通过管理控制台将这些项目分配给集合以使其可见。" + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "注意:从 2024 年 5 月 2 日起,未分配的组织项目在「所有密码库」视图中将不再可见,只能通过管理控制台访问。通过管理控制台将这些项目分配给集合以使其可见。" + "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/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index b6f1ff574a..a8163dab98 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "變更主密碼" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "指紋短語", "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": "資料夾已新增" }, - "changeMasterPass": { - "message": "變更主密碼" - }, - "changeMasterPasswordConfirmation": { - "message": "您可以在 bitwarden.com 網頁版密碼庫變更主密碼。現在要前往嗎?" - }, "twoStepLoginConfirmation": { "message": "兩步驟登入需要您從其他裝置(例如安全鑰匙、驗證器程式、SMS、手機或電子郵件)來驗證您的登入,這使您的帳戶更加安全。兩步驟登入可以在 bitwarden.com 網頁版密碼庫啟用。現在要前往嗎?" }, @@ -3000,16 +3000,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "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." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "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." + "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/store/locales/gl/copy.resx b/apps/browser/store/locales/gl/copy.resx index d812256fb7..04e84f6677 100644 --- a/apps/browser/store/locales/gl/copy.resx +++ b/apps/browser/store/locales/gl/copy.resx @@ -158,7 +158,7 @@ Protexa e comparta datos confidenciais dentro da súa Caixa Forte de Bitwarden d <value>Sincroniza e accede á túa caixa forte desde múltiples dispositivos</value> </data> <data name="ScreenshotVault" xml:space="preserve"> - <value>Xestiona todos os teus usuarios e contrasinais desde unha caixa forte segura</value> + <value>Xestiona todos os teus inicios de sesión e contrasinais desde unha caixa forte segura</value> </data> <data name="ScreenshotAutofill" xml:space="preserve"> <value>Autocompleta rapidamente os teus datos de acceso en calquera páxina web que visites</value> From f6dee29a5fc0e48b7464181bd7f3c03280838fe6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 19 Apr 2024 10:41:39 +0000 Subject: [PATCH 224/351] Autosync the updated translations (#8824) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/web/src/locales/af/messages.json | 139 +++++++++- apps/web/src/locales/ar/messages.json | 139 +++++++++- apps/web/src/locales/az/messages.json | 141 +++++++++- apps/web/src/locales/be/messages.json | 139 +++++++++- apps/web/src/locales/bg/messages.json | 141 +++++++++- apps/web/src/locales/bn/messages.json | 139 +++++++++- apps/web/src/locales/bs/messages.json | 139 +++++++++- apps/web/src/locales/ca/messages.json | 151 ++++++++++- apps/web/src/locales/cs/messages.json | 141 +++++++++- apps/web/src/locales/cy/messages.json | 139 +++++++++- apps/web/src/locales/da/messages.json | 139 +++++++++- apps/web/src/locales/de/messages.json | 141 +++++++++- apps/web/src/locales/el/messages.json | 139 +++++++++- apps/web/src/locales/en_GB/messages.json | 139 +++++++++- apps/web/src/locales/en_IN/messages.json | 139 +++++++++- apps/web/src/locales/eo/messages.json | 139 +++++++++- apps/web/src/locales/es/messages.json | 139 +++++++++- apps/web/src/locales/et/messages.json | 139 +++++++++- apps/web/src/locales/eu/messages.json | 139 +++++++++- apps/web/src/locales/fa/messages.json | 139 +++++++++- apps/web/src/locales/fi/messages.json | 141 +++++++++- apps/web/src/locales/fil/messages.json | 139 +++++++++- apps/web/src/locales/fr/messages.json | 141 +++++++++- apps/web/src/locales/gl/messages.json | 139 +++++++++- apps/web/src/locales/he/messages.json | 139 +++++++++- apps/web/src/locales/hi/messages.json | 139 +++++++++- apps/web/src/locales/hr/messages.json | 139 +++++++++- apps/web/src/locales/hu/messages.json | 141 +++++++++- apps/web/src/locales/id/messages.json | 139 +++++++++- apps/web/src/locales/it/messages.json | 141 +++++++++- apps/web/src/locales/ja/messages.json | 143 +++++++++- apps/web/src/locales/ka/messages.json | 139 +++++++++- apps/web/src/locales/km/messages.json | 139 +++++++++- apps/web/src/locales/kn/messages.json | 139 +++++++++- apps/web/src/locales/ko/messages.json | 139 +++++++++- apps/web/src/locales/lv/messages.json | 153 ++++++++++- apps/web/src/locales/ml/messages.json | 139 +++++++++- apps/web/src/locales/mr/messages.json | 139 +++++++++- apps/web/src/locales/my/messages.json | 139 +++++++++- apps/web/src/locales/nb/messages.json | 139 +++++++++- apps/web/src/locales/ne/messages.json | 139 +++++++++- apps/web/src/locales/nl/messages.json | 139 +++++++++- apps/web/src/locales/nn/messages.json | 139 +++++++++- apps/web/src/locales/or/messages.json | 139 +++++++++- apps/web/src/locales/pl/messages.json | 293 ++++++++++++++------ apps/web/src/locales/pt_BR/messages.json | 323 ++++++++++++++++------- apps/web/src/locales/pt_PT/messages.json | 139 +++++++++- apps/web/src/locales/ro/messages.json | 139 +++++++++- apps/web/src/locales/ru/messages.json | 139 +++++++++- apps/web/src/locales/si/messages.json | 139 +++++++++- apps/web/src/locales/sk/messages.json | 139 +++++++++- apps/web/src/locales/sl/messages.json | 139 +++++++++- apps/web/src/locales/sr/messages.json | 141 +++++++++- apps/web/src/locales/sr_CS/messages.json | 139 +++++++++- apps/web/src/locales/sv/messages.json | 139 +++++++++- apps/web/src/locales/te/messages.json | 139 +++++++++- apps/web/src/locales/th/messages.json | 139 +++++++++- apps/web/src/locales/tr/messages.json | 139 +++++++++- apps/web/src/locales/uk/messages.json | 141 +++++++++- apps/web/src/locales/vi/messages.json | 139 +++++++++- apps/web/src/locales/zh_CN/messages.json | 145 +++++++++- apps/web/src/locales/zh_TW/messages.json | 139 +++++++++- 62 files changed, 8691 insertions(+), 321 deletions(-) diff --git a/apps/web/src/locales/af/messages.json b/apps/web/src/locales/af/messages.json index 6dfb5ec30d..51ba16e5e0 100644 --- a/apps/web/src/locales/af/messages.json +++ b/apps/web/src/locales/af/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices 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." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/ar/messages.json b/apps/web/src/locales/ar/messages.json index 1923502b1a..4377e5655b 100644 --- a/apps/web/src/locales/ar/messages.json +++ b/apps/web/src/locales/ar/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices 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." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/az/messages.json b/apps/web/src/locales/az/messages.json index 911031f0f1..2e94d219d8 100644 --- a/apps/web/src/locales/az/messages.json +++ b/apps/web/src/locales/az/messages.json @@ -1474,7 +1474,7 @@ "message": "Veb anbarda istifadə olunan dili dəyişdirin." }, "enableFavicon": { - "message": "Veb sayt nişanlarını göstər" + "message": "Veb sayt ikonlarını göstər" }, "faviconDesc": { "message": "Hər girişin yanında tanına bilən təsvir göstər." @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provayder Portal" }, + "success": { + "message": "Uğurlu" + }, "viewCollection": { "message": "Kolleksiyaya bax" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Maşın hesabına müraciət güncəlləndi" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "Özünüzü bir qrupa əlavə edə bilməzsiniz." }, "unassignedItemsBannerSelfHost": { "message": "Bildiriş: 2 May 2024-cü ildən etibarən təyin edilməmiş təşkilat elementləri artıq cihazlar arasında 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." + }, + "unassignedItemsBannerNotice": { + "message": "Bildiriş: Təyin edilməyən təşkilat elementləri artıq Bütün Anbarlar görünüşündə görünən olmayacaq və yalnız Admin Konsolu vasitəsilə əlçatan olacaq." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Bildiriş: 16 May 2024-cü il tarixindən etibarən, təyin edilməyən təşkilat elementləri cihazlar arasında və Bütün Anbarlar görünüşündə görünən olmayacaq və yalnız Admin Konsolu vasitəsilə əlçatan olacaq." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Bu elementləri görünən etmək üçün", + "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": "bir kolleksiyaya təyin edin.", + "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." + }, + "deleteProvider": { + "message": "Provayderi sil" + }, + "deleteProviderConfirmation": { + "message": "Bir provayderin silinməsi daimi və geri qaytarıla bilməyən prosesdir. Provayderin və əlaqəli bütün datanın silinməsini təsdiqləmək üçün ana parolunuzu daxil edin." + }, + "deleteProviderName": { + "message": "$ID$ silinə bilmir", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "$ID$ silinməzdən əvvəl bütün müştəriləri (client) ayırmalısınız", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provayder silindi" + }, + "providerDeletedDesc": { + "message": "Provayder və bütün əlaqəli datalar silindi." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "Bu Provayderi silmək üçün tələb göndərdiniz. Təsdiqləmək üçün aşağıdakı düyməni istifadə edin." + }, + "deleteProviderWarning": { + "message": "Provayderin silinməsi daimi prosesdir. Geri dönüşü olmayacaq." + }, + "errorAssigningTargetCollection": { + "message": "Hədəf kolleksiyaya təyin etmə xətası." + }, + "errorAssigningTargetFolder": { + "message": "Hədəf qovluğa təyin etmə xətası." + }, + "integrationsAndSdks": { + "message": "İnteqrasiyalar və SDK-lar", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "İnteqrasiyalar" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDK-lar" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Github Actions qur" + }, + "setUpGitlabCICD": { + "message": "GitLab CI/CD qur" + }, + "setUpAnsible": { + "message": "Ansible qur" + }, + "cSharpSDKRepo": { + "message": "C# repozitoriyasına bax" + }, + "cPlusPlusSDKRepo": { + "message": "C++ repozitoriyasına bax" + }, + "jsWebAssemblySDKRepo": { + "message": "JS WebAssembly repozitoriyasına bax" + }, + "javaSDKRepo": { + "message": "Java repozitoriyasına bax" + }, + "pythonSDKRepo": { + "message": "Python repozitoriyasına bax" + }, + "phpSDKRepo": { + "message": "php repozitoriyasına bax" + }, + "rubySDKRepo": { + "message": "Ruby repozitoriyasına bax" + }, + "goSDKRepo": { + "message": "Go repozitoriyasına bax" + }, + "createNewClientToManageAsProvider": { + "message": "Provayder kimi idarə etmək üçün yeni bir client təşkilatı yaradın. Əlavə yerlər növbəti faktura dövründə əks olunacaq." + }, + "selectAPlan": { + "message": "Bir plan seçin" + }, + "thirtyFivePercentDiscount": { + "message": "35% endirim" + }, + "monthPerMember": { + "message": "üzv başına ay" + }, + "seats": { + "message": "Yer" + }, + "addOrganization": { + "message": "Təşkilat əlavə et" + }, + "createdNewClient": { + "message": "Yeni client uğurla yaradıldı" + }, + "noAccess": { + "message": "Müraciət yoxdur" + }, + "collectionAdminConsoleManaged": { + "message": "Bu kolleksiya yalnız admin konsolundan əlçatandır" } } diff --git a/apps/web/src/locales/be/messages.json b/apps/web/src/locales/be/messages.json index c93f773382..2cfb8af125 100644 --- a/apps/web/src/locales/be/messages.json +++ b/apps/web/src/locales/be/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices 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." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/bg/messages.json b/apps/web/src/locales/bg/messages.json index b3c20f50e2..05a819f97a 100644 --- a/apps/web/src/locales/bg/messages.json +++ b/apps/web/src/locales/bg/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Портал за доставчици" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "Преглед на колекцията" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Достъпът на машинния акаунт е променен" }, - "unassignedItemsBanner": { - "message": "Известие: неразпределените елементи на организацията вече не се виждат в изгледа с „Всички трезори“ на различните устройства, а са достъпни само през Административната конзола. Добавете тези елементи към някоя колекция в Административната конзола, за да станат видими." + "restrictedGroupAccessDesc": { + "message": "Не може да добавяте себе си към групи." }, "unassignedItemsBannerSelfHost": { - "message": "Известие: от 2 май 2024г. неразпределените елементи на организациите вече няма се виждат в изгледа с „Всички трезори“ на различните устройства, а ще бъдат достъпни само през Административната конзола. Добавете тези елементи към някоя колекция в Административната конзола, за да станат видими." + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Известие: неразпределените елементи в организацията вече няма да се виждат в изгледа с всички трезори на различните устройства, а са достъпни само през Административната конзола." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Известие: след 16 май 2024, неразпределените елементи в организацията вече няма да се виждат в изгледа с всички трезори на различните устройства, а ще бъдат достъпни само през Административната конзола." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Добавете тези елементи към колекция в", + "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": "за да ги направите видими.", + "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." + }, + "deleteProvider": { + "message": "Изтриване на доставчик" + }, + "deleteProviderConfirmation": { + "message": "Изтриването на доставчик е окончателно и необратимо. Въведете главната си парола, за да потвърдите изтриването на доставчика и всички свързани данни." + }, + "deleteProviderName": { + "message": "Изтриването на $ID$ е невъзможно", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "Трябва да разкачите всички клиенти, преди да можете да изтриете $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Доставчикът е изтрит" + }, + "providerDeletedDesc": { + "message": "Доставчикът и всички свързани данни са изтрити." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "Заявили сте, че искате да изтриете този доставчик. Използвайте бутона по-долу, за да потвърдите това решение." + }, + "deleteProviderWarning": { + "message": "Изтриването на доставчика Ви е окончателно и необратимо." + }, + "errorAssigningTargetCollection": { + "message": "Грешка при задаването на целева колекция." + }, + "errorAssigningTargetFolder": { + "message": "Грешка при задаването на целева папка." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/bn/messages.json b/apps/web/src/locales/bn/messages.json index afde1e40d4..46d0e574ce 100644 --- a/apps/web/src/locales/bn/messages.json +++ b/apps/web/src/locales/bn/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices 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." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/bs/messages.json b/apps/web/src/locales/bs/messages.json index f5cddc79db..85880f9cbf 100644 --- a/apps/web/src/locales/bs/messages.json +++ b/apps/web/src/locales/bs/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices 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." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/ca/messages.json b/apps/web/src/locales/ca/messages.json index 2bec515116..cc072c3b96 100644 --- a/apps/web/src/locales/ca/messages.json +++ b/apps/web/src/locales/ca/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Portal del proveïdor" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "Mostra col·lecció" }, @@ -7625,13 +7628,13 @@ "message": "Assigna a aquestes col·leccions" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Seleccioneu les col·leccions amb les quals es compartiran els elements. Una vegada que un element s'actualitza en una col·lecció, es reflectirà a totes les col·leccions. Només els membres de l'organització amb accés a aquestes col·leccions podran veure els elements." }, "selectCollectionsToAssign": { - "message": "Select collections to assign" + "message": "Seleccioneu les col·leccions per assignar" }, "noCollectionsAssigned": { - "message": "No collections have been assigned" + "message": "No s'ha assignat cap col·lecció" }, "successfullyAssignedCollections": { "message": "Successfully assigned collections" @@ -7650,7 +7653,7 @@ } }, "items": { - "message": "Items" + "message": "Elements" }, "assignedSeats": { "message": "Assigned seats" @@ -7659,10 +7662,10 @@ "message": "Assigned" }, "used": { - "message": "Used" + "message": "Utilitzat" }, "remaining": { - "message": "Remaining" + "message": "Queden" }, "unlinkOrganization": { "message": "Unlink organization" @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices 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." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/cs/messages.json b/apps/web/src/locales/cs/messages.json index 82dfe2ecae..6b82766cd3 100644 --- a/apps/web/src/locales/cs/messages.json +++ b/apps/web/src/locales/cs/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Portál poskytovatele" }, + "success": { + "message": "Úspěch" + }, "viewCollection": { "message": "Zobrazit kolekci" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Přístup strojového účtu byl aktualizován" }, - "unassignedItemsBanner": { - "message": "Upozornění: Nepřiřazené položky organizace již nejsou viditelné ve Vašem zobrazení všech trezorů napříč zařízeními 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é." + "restrictedGroupAccessDesc": { + "message": "Do skupiny nemůžete přidat sami sebe." }, "unassignedItemsBannerSelfHost": { - "message": "Upozornění: Dne 2. května 2024 již nebudou nepřiřazené položky organizace viditelné v zobrazení Všechny trezory ve všech zařízeních 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é." + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Upozornění: Nepřiřazené položky organizace již nejsou viditelné ve vašem zobrazení všech trezorů napříč zařízeními a jsou nyní přístupné pouze v konzoli správce." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Upozornění: 16. květba 2024 již nebudou nepřiřazené položky organizace viditelné ve vašem zobrazení všech trezorů napříč zařízeními a budou přístupné pouze v konzoli správce." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Přiřadit tyto položky ke kolekci z", + "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": "aby byly viditelné.", + "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." + }, + "deleteProvider": { + "message": "Smazat poskytovatele" + }, + "deleteProviderConfirmation": { + "message": "Smazání poskytovatele je trvalé a nevratné. Zadejte hlavní heslo pro potvrzení smazání poskytovatele a všech souvisejících dat." + }, + "deleteProviderName": { + "message": "Nelze smazat $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "Před smazáním $ID$ musíte odpojit všechny klienty", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Poskytovatel byl smazán" + }, + "providerDeletedDesc": { + "message": "Poskytovatel a veškerá související data byla smazána." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "Požádali jste o smazání tohoto poskytovatele. Pro potvrzení použijte tlačítko níže." + }, + "deleteProviderWarning": { + "message": "Smazání poskytovatele je trvalé. Tuto akci nelze vrátit zpět." + }, + "errorAssigningTargetCollection": { + "message": "Chyba při přiřazování cílové kolekce." + }, + "errorAssigningTargetFolder": { + "message": "Chyba při přiřazování cílové složky." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Vytvořte novou klientskou organizaci pro správu jako poskytovatele. Další uživatelé budou reflektováni v dalším platebním cyklu." + }, + "selectAPlan": { + "message": "Vyberte plán" + }, + "thirtyFivePercentDiscount": { + "message": "35% sleva" + }, + "monthPerMember": { + "message": "měsíčně za člena" + }, + "seats": { + "message": "Počet" + }, + "addOrganization": { + "message": "Přidat organizaci" + }, + "createdNewClient": { + "message": "Nový klient byl úspěšně vytvořen" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/cy/messages.json b/apps/web/src/locales/cy/messages.json index 8ee7ab3569..7394c3fe2a 100644 --- a/apps/web/src/locales/cy/messages.json +++ b/apps/web/src/locales/cy/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices 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." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/da/messages.json b/apps/web/src/locales/da/messages.json index e52510ed1c..081fef7865 100644 --- a/apps/web/src/locales/da/messages.json +++ b/apps/web/src/locales/da/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Udbyderportal" }, + "success": { + "message": "Gennemført" + }, "viewCollection": { "message": "Vis samling" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Maskinekontoadgang opdateret" }, - "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." + "restrictedGroupAccessDesc": { + "message": "Man kan ikke føje sig selv til en gruppe." }, "unassignedItemsBannerSelfHost": { "message": "Bemærk: Pr. 2. maj 2024 vil utildelte organisationsemner ikke længere være synlige i Alle Bokse-visningen på tværs af enheder 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." + }, + "unassignedItemsBannerNotice": { + "message": "Bemærk: Utildelte organisationsemner er ikke længere synlige i Alle Bokse-visningen, men er kun tilgængelige via Admin-konsol." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Bemærk: Pr. 16. maj 2024 er utildelte organisationsemner er ikke længere synlige i Alle Bokse-visningen, men er kun tilgængelige via Admin-konsol." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Tildel disse emner til en samling via", + "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": "for at gøre dem synlige.", + "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." + }, + "deleteProvider": { + "message": "Slet udbyder" + }, + "deleteProviderConfirmation": { + "message": "Sletning af en udbyder er permanent og irreversibel. Angiv hovedadgangskoden for at bekræfte sletningen af udbyderen og alle tilknyttede data." + }, + "deleteProviderName": { + "message": "Kan ikke slette $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "Alle klienttilknytninger skal fjernes, før $ID$ kan slettes", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Udbyder er hermed slettet" + }, + "providerDeletedDesc": { + "message": "Udbyderen og alle tilknyttede data er hermed slettet." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "Der er anmodet sletning af din Bitwarden-konto. Klik på knappen nedenfor for at bekræfte." + }, + "deleteProviderWarning": { + "message": "Sletning af kontoen er permanent og irreversibel." + }, + "errorAssigningTargetCollection": { + "message": "Fejl ved tildeling af målsamling." + }, + "errorAssigningTargetFolder": { + "message": "Fejl ved tildeling af målmappe." + }, + "integrationsAndSdks": { + "message": "Integrationer og SDK'er", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrationer" + }, + "integrationsDesc": { + "message": "Synk automatisk hemmeligheder fra Bitwarden Secrets Manager til en tredjepartstjeneste." + }, + "sdks": { + "message": "SDK'er" + }, + "sdksDesc": { + "message": "Brug Bitwarden Secrets Manager SDK i flg. programmeringssprog til bygning af egne applikationer." + }, + "setUpGithubActions": { + "message": "Opsæt Github-handlinger" + }, + "setUpGitlabCICD": { + "message": "Opsæt GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Opsæt Ansible" + }, + "cSharpSDKRepo": { + "message": "Vis C#-repo" + }, + "cPlusPlusSDKRepo": { + "message": "Vis C++-repo" + }, + "jsWebAssemblySDKRepo": { + "message": "VIs JS WebAssembly-repo" + }, + "javaSDKRepo": { + "message": "Vis Java-repo" + }, + "pythonSDKRepo": { + "message": "Vis Python-repo" + }, + "phpSDKRepo": { + "message": "Vis php-repo" + }, + "rubySDKRepo": { + "message": "Vis Ruby-repo" + }, + "goSDKRepo": { + "message": "Vis Go-repo" + }, + "createNewClientToManageAsProvider": { + "message": "Opret en ny kundeorganisation til at håndtere som udbyder. Yderligere pladser afspejles i næste faktureringscyklus." + }, + "selectAPlan": { + "message": "Vælg en abonnementstype" + }, + "thirtyFivePercentDiscount": { + "message": "35% rabat" + }, + "monthPerMember": { + "message": "måned pr. medlem" + }, + "seats": { + "message": "Pladser" + }, + "addOrganization": { + "message": "Tilføj organisation" + }, + "createdNewClient": { + "message": "Ny kunde er hermed oprettet" + }, + "noAccess": { + "message": "Ingen adgang" + }, + "collectionAdminConsoleManaged": { + "message": "Denne samling er kun tilgængelig via Admin-konsol" } } diff --git a/apps/web/src/locales/de/messages.json b/apps/web/src/locales/de/messages.json index 0e9b0a983f..3fb7598416 100644 --- a/apps/web/src/locales/de/messages.json +++ b/apps/web/src/locales/de/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Anbieterportal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "Sammlung anzeigen" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Zugriff auf Gerätekonto aktualisiert" }, - "unassignedItemsBanner": { - "message": "Hinweis: Nicht zugewiesene Organisationseinträge sind nicht mehr geräteübergreifend in der Ansicht aller Tresore sichtbar und sind nun nur über die Administrator-Konsole zugänglich. Weise diese Einträge einer Sammlung aus der Administrator-Konsole zu, um sie sichtbar zu machen." + "restrictedGroupAccessDesc": { + "message": "Du kannst dich nicht selbst zu einer Gruppe hinzufügen." }, "unassignedItemsBannerSelfHost": { - "message": "Hinweis: Ab dem 2. Mai 2024 werden nicht zugewiesene Organisationseinträge nicht mehr geräteübergreifend 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." + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices 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." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Fehler beim Zuweisen der Ziel-Sammlung." + }, + "errorAssigningTargetFolder": { + "message": "Fehler beim Zuweisen des Ziel-Ordners." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/el/messages.json b/apps/web/src/locales/el/messages.json index 256a5644d6..742a76402d 100644 --- a/apps/web/src/locales/el/messages.json +++ b/apps/web/src/locales/el/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices 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." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/en_GB/messages.json b/apps/web/src/locales/en_GB/messages.json index 152c6a89e1..66f728cb4d 100644 --- a/apps/web/src/locales/en_GB/messages.json +++ b/apps/web/src/locales/en_GB/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices 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." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/en_IN/messages.json b/apps/web/src/locales/en_IN/messages.json index d4d22f6aba..c0c77d3d78 100644 --- a/apps/web/src/locales/en_IN/messages.json +++ b/apps/web/src/locales/en_IN/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices 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." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/eo/messages.json b/apps/web/src/locales/eo/messages.json index 899558160f..2a6db2ea4b 100644 --- a/apps/web/src/locales/eo/messages.json +++ b/apps/web/src/locales/eo/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices 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." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/es/messages.json b/apps/web/src/locales/es/messages.json index 78b051d588..9f0f085e9d 100644 --- a/apps/web/src/locales/es/messages.json +++ b/apps/web/src/locales/es/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices 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." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/et/messages.json b/apps/web/src/locales/et/messages.json index fdb656f31e..0c3f7cb300 100644 --- a/apps/web/src/locales/et/messages.json +++ b/apps/web/src/locales/et/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices 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." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/eu/messages.json b/apps/web/src/locales/eu/messages.json index 0125ceec25..c315d62520 100644 --- a/apps/web/src/locales/eu/messages.json +++ b/apps/web/src/locales/eu/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices 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." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/fa/messages.json b/apps/web/src/locales/fa/messages.json index e91ea40d05..32feda3edd 100644 --- a/apps/web/src/locales/fa/messages.json +++ b/apps/web/src/locales/fa/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices 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." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/fi/messages.json b/apps/web/src/locales/fi/messages.json index 18fa9c15fb..377f7b084f 100644 --- a/apps/web/src/locales/fi/messages.json +++ b/apps/web/src/locales/fi/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Toimittajaportaali" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "Tarkastele kokoelmaa" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Konetilin oikeuksia muutettiin" }, - "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." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "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." + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices 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." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/fil/messages.json b/apps/web/src/locales/fil/messages.json index 2cf9eb3b7e..50c9f8d4e8 100644 --- a/apps/web/src/locales/fil/messages.json +++ b/apps/web/src/locales/fil/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices 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." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/fr/messages.json b/apps/web/src/locales/fr/messages.json index 27b529442c..2934176956 100644 --- a/apps/web/src/locales/fr/messages.json +++ b/apps/web/src/locales/fr/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Portail fournisseur" }, + "success": { + "message": "Succès" + }, "viewCollection": { "message": "Afficher la Collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Accès au compte machine mis à jour" }, - "unassignedItemsBanner": { - "message": "Remarque : 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." + "restrictedGroupAccessDesc": { + "message": "Vous ne pouvez pas vous ajouter vous-même à un groupe." }, "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." + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Remarque : Les éléments d'organisation non assignés ne sont plus visibles dans la vue Tous les coffres sur les appareils et ne sont maintenant accessibles que via la Console Admin." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Remarque : À partir du 16 mai 2024, les éléments d'organisation non assignés ne seront plus visibles dans la vue Tous les coffres sur les appareils et ne seront maintenant accessibles que via la Console Admin." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assigner ces éléments à une collection depuis", + "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": "pour les rendre visibles.", + "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." + }, + "deleteProvider": { + "message": "Supprimer le fournisseur" + }, + "deleteProviderConfirmation": { + "message": "La suppression d'un fournisseur est permanente et irréversible. Entrez votre mot de passe principal pour confirmer la suppression du fournisseur et de toutes les données associées." + }, + "deleteProviderName": { + "message": "Impossible de supprimer $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "Vous devez dissocier tous les clients avant de pouvoir supprimer $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Fournisseur supprimé" + }, + "providerDeletedDesc": { + "message": "Le fournisseur et toutes les données associées ont été supprimés." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "Vous avez demandé à supprimer ce fournisseur. Utilisez le bouton ci-dessous pour confirmer." + }, + "deleteProviderWarning": { + "message": "La suppression de votre fournisseur est permanente. Elle ne peut pas être annulée." + }, + "errorAssigningTargetCollection": { + "message": "Erreur lors de l'assignation de la collection cible." + }, + "errorAssigningTargetFolder": { + "message": "Erreur lors de l'assignation du dossier cible." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Créez une nouvelle organisation de clients à gérer en tant que Fournisseur. Des sièges supplémentaires seront reflétés lors du prochain cycle de facturation." + }, + "selectAPlan": { + "message": "Sélectionnez un plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% de réduction" + }, + "monthPerMember": { + "message": "mois par membre" + }, + "seats": { + "message": "Licences" + }, + "addOrganization": { + "message": "Ajouter une organisation" + }, + "createdNewClient": { + "message": "Nouveau client créé avec succès" + }, + "noAccess": { + "message": "Aucun accès" + }, + "collectionAdminConsoleManaged": { + "message": "Cette collection n'est accessible qu'à partir de la Console Admin" } } diff --git a/apps/web/src/locales/gl/messages.json b/apps/web/src/locales/gl/messages.json index 781c9000cb..ed04c3a3ef 100644 --- a/apps/web/src/locales/gl/messages.json +++ b/apps/web/src/locales/gl/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices 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." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/he/messages.json b/apps/web/src/locales/he/messages.json index fd42338a28..b7b4d59227 100644 --- a/apps/web/src/locales/he/messages.json +++ b/apps/web/src/locales/he/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices 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." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/hi/messages.json b/apps/web/src/locales/hi/messages.json index f0487066d6..23321b27ef 100644 --- a/apps/web/src/locales/hi/messages.json +++ b/apps/web/src/locales/hi/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices 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." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/hr/messages.json b/apps/web/src/locales/hr/messages.json index 20a62053f0..edc6adaf3b 100644 --- a/apps/web/src/locales/hr/messages.json +++ b/apps/web/src/locales/hr/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices 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." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/hu/messages.json b/apps/web/src/locales/hu/messages.json index 35781b814a..4ec6f15029 100644 --- a/apps/web/src/locales/hu/messages.json +++ b/apps/web/src/locales/hu/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Szolgáltató portál" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "Gyűjtemény megtekintése" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "A gépi fiók elérése frissítésre került." }, - "unassignedItemsBanner": { - "message": "Megjegyzés: A nem hozzá rendelt szervezeti elemek már nem láthatók az Összes széf nézetben a különböző eszközökön és mostantól csak a Felügyeleti konzolon keresztül érhetők el. Rendeljük ezeket az elemeket egy gyűjteményhez az Adminisztrátori konzolból, hogy láthatóvá tegyeük azokat." + "restrictedGroupAccessDesc": { + "message": "Nem adhadjuk magunkat a csoporthoz." }, "unassignedItemsBannerSelfHost": { - "message": "Megjegyzés: A nem hozzá rendelt szervezeti elemek már nem láthatók az Összes széf nézetben a különböző eszközökön és mostantól csak a Felügyeleti konzolon keresztül érhetők el. Rendeljük ezeket az elemeket egy gyűjteményhez az Adminisztrátori konzolból, hogy láthatóvá tegyeük azokat." + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Megjegyzés: A nem hozzárendelt szervezeti elemek már nem láthatók az Összes széf nézetben a különböző eszközökön és csak az Adminisztrátori konzolon keresztül érhetők el." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Megjegyzés: 2024. május 16-tól 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 az Adminisztrátoir konzolon keresztül lesznek elérhetők." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Rendeljük hozzá ezeket az elemeket a gyűjteményhez", + "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": "a láthatósághoz.", + "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." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Hiba történt a célgyűjtemény hozzárendelése során." + }, + "errorAssigningTargetFolder": { + "message": "Hiba történt a célmappa hozzárendelése során." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "Nincs hozzáférés." + }, + "collectionAdminConsoleManaged": { + "message": "Ez a gyűjtemény csak az adminisztrátori konzolról érhető el." } } diff --git a/apps/web/src/locales/id/messages.json b/apps/web/src/locales/id/messages.json index 4cf235eb16..3bc9ef4e62 100644 --- a/apps/web/src/locales/id/messages.json +++ b/apps/web/src/locales/id/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices 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." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/it/messages.json b/apps/web/src/locales/it/messages.json index 1cdee4420d..11dec69959 100644 --- a/apps/web/src/locales/it/messages.json +++ b/apps/web/src/locales/it/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Portale Fornitori" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "Visualizza raccolta" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Accesso all'account macchina aggiornato" }, - "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." + "restrictedGroupAccessDesc": { + "message": "Non puoi aggiungerti a un gruppo." }, "unassignedItemsBannerSelfHost": { - "message": "Avviso: dal 2 maggio 2024, gli elementi dell'organizzazione non assegnati non saranno più visibili nella tua 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." + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "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." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Avviso: dal 2 maggio 2024, gli elementi dell'organizzazione non assegnati non saranno più visibili nella tua visualizzazione Tutte le Cassaforti su tutti i dispositivi e saranno accessibili solo tramite la Console di amministrazione." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assegna questi elementi ad una raccolta dalla", + "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": "per renderli visibili.", + "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." + }, + "deleteProvider": { + "message": "Elimina fornitore" + }, + "deleteProviderConfirmation": { + "message": "La cancellazione di un fornitore è permanente e irreversibile. Inserisci la tua password principale per confermare l'eliminazione del fornitore e di tutti i dati associati." + }, + "deleteProviderName": { + "message": "Impossibile eliminare $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "Devi scollegare tutti i client prima di poter eliminare $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Fornitore eliminato" + }, + "providerDeletedDesc": { + "message": "Il fornitore e tutti i dati associati sono stati eliminati." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "Hai richiesto di eliminare il questo fornitore. Clicca qui sotto per confermare." + }, + "deleteProviderWarning": { + "message": "L'eliminazione del fornitore è permanente. Questa azione non è reversibile." + }, + "errorAssigningTargetCollection": { + "message": "Errore nell'assegnazione della raccolta di destinazione." + }, + "errorAssigningTargetFolder": { + "message": "Errore nell'assegnazione della cartella di destinazione." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Crea una nuova organizzazione cliente da gestire come fornitore. Gli slot aggiuntivi saranno riflessi nel prossimo ciclo di fatturazione." + }, + "selectAPlan": { + "message": "Seleziona un piano" + }, + "thirtyFivePercentDiscount": { + "message": "Sconto del 35%" + }, + "monthPerMember": { + "message": "mese per membro" + }, + "seats": { + "message": "Slot" + }, + "addOrganization": { + "message": "Aggiungi organizzazione" + }, + "createdNewClient": { + "message": "Nuovo cliente creato con successo" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/ja/messages.json b/apps/web/src/locales/ja/messages.json index 3561c81bd1..17b58a4299 100644 --- a/apps/web/src/locales/ja/messages.json +++ b/apps/web/src/locales/ja/messages.json @@ -7601,11 +7601,14 @@ "message": "リリースブログを読む" }, "adminConsole": { - "message": "管理者コンソール" + "message": "管理コンソール" }, "providerPortal": { "message": "プロバイダーポータル" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "コレクションを表示" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "マシンアカウントへのアクセス権限を更新しました" }, - "unassignedItemsBanner": { - "message": "注意: 割り当てられていない組織項目は、デバイス間のすべての保管庫のビューでは表示されなくなり、管理コンソールからのみアクセスできます。 管理コンソールからコレクションにこれらのアイテムを割り当てると、表示するようにできます。" + "restrictedGroupAccessDesc": { + "message": "あなた自身をグループに追加することはできません。" }, "unassignedItemsBannerSelfHost": { - "message": "お知らせ:2024年5月2日に、 割り当てられていない組織アイテムはデバイス間のすべての保管庫ビューに表示されなくなり、管理コンソールからのみアクセス可能になります。 管理コンソールからコレクションにこれらのアイテムを割り当てると、表示できるようになります。" + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "注意: 割り当てられていない組織アイテムは、デバイス間のすべての保管庫ビューでは表示されなくなり、管理コンソールからのみアクセスできるようになりました。" + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "お知らせ:2024年5月16日に、 割り当てられていない組織アイテムは、デバイス間のすべての保管庫ビューに表示されなくなり、管理コンソールからのみアクセス可能になります。" + }, + "unassignedItemsBannerCTAPartOne": { + "message": "これらのアイテムのコレクションへの割り当てを", + "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": "で実行すると表示できるようになります。", + "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." + }, + "deleteProvider": { + "message": "プロバイダを削除" + }, + "deleteProviderConfirmation": { + "message": "プロバイダの削除は恒久的で元に戻せません。マスターパスワードを入力して、プロバイダと関連するすべてのデータの削除を確認してください。" + }, + "deleteProviderName": { + "message": "$ID$ を削除できません", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "$ID$ を削除する前に、すべてのクライアントのリンクを解除してください", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "プロバイダを削除しました" + }, + "providerDeletedDesc": { + "message": "プロバイダと関連するすべてのデータを削除しました。" + }, + "deleteProviderRecoverConfirmDesc": { + "message": "このプロバイダの削除をリクエストしました。下のボタンを使うと確認できます。" + }, + "deleteProviderWarning": { + "message": "プロバイダを削除すると元に戻すことはできません。" + }, + "errorAssigningTargetCollection": { + "message": "ターゲットコレクションの割り当てに失敗しました。" + }, + "errorAssigningTargetFolder": { + "message": "ターゲットフォルダーの割り当てに失敗しました。" + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/ka/messages.json b/apps/web/src/locales/ka/messages.json index d94a701410..6d34252b88 100644 --- a/apps/web/src/locales/ka/messages.json +++ b/apps/web/src/locales/ka/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices 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." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/km/messages.json b/apps/web/src/locales/km/messages.json index 781c9000cb..ed04c3a3ef 100644 --- a/apps/web/src/locales/km/messages.json +++ b/apps/web/src/locales/km/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices 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." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/kn/messages.json b/apps/web/src/locales/kn/messages.json index f2a5bdd829..ae6f2c2a89 100644 --- a/apps/web/src/locales/kn/messages.json +++ b/apps/web/src/locales/kn/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices 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." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/ko/messages.json b/apps/web/src/locales/ko/messages.json index b54268adbd..f9240f21aa 100644 --- a/apps/web/src/locales/ko/messages.json +++ b/apps/web/src/locales/ko/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices 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." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/lv/messages.json b/apps/web/src/locales/lv/messages.json index d68a62bdcf..0dc0dff097 100644 --- a/apps/web/src/locales/lv/messages.json +++ b/apps/web/src/locales/lv/messages.json @@ -3360,7 +3360,7 @@ "message": "Ja konts pastāv, tika nosūtīts e-pasta ziņojums ar turpmākām norādēm." }, "deleteRecoverConfirmDesc": { - "message": "Tika pieprasīts izdzēst Bitwarden kontu. Jānospiež zemāk esošā poga, lai apstiprinātu." + "message": "Tika pieprasīts izdzēst Bitwarden kontu. Jāizmanto zemāk esošā poga, lai apstiprinātu." }, "myOrganization": { "message": "Mana apvienība" @@ -3396,7 +3396,7 @@ "message": "Apvienība izdzēsta" }, "organizationDeletedDesc": { - "message": "Apvienība un visi ar to saistītie dati ir izdzēsti." + "message": "Apvienība un visi ar to saistītie dati tika izdzēsti." }, "organizationUpdated": { "message": "Apvienība atjaunināta" @@ -4927,7 +4927,7 @@ "message": "Uziacināt jaunu nodrošinātāja lietotāju, zemāk esošajā laukā ievadot tā Bitwarden konta e-pasta adresi. Ja tam vēl nav Bitwarden konta, tiks vaicāts izveidot jaunu." }, "joinProvider": { - "message": "Pievienot nodrošinātāju" + "message": "Pievienoties nodrošinātāju" }, "joinProviderDesc": { "message": "Tu esi uzaicināts pievienoties augstāk norādītajam nodrošinātājam. Lai to pieņemtu, jāpiesakās vai jāizveido jauns Bitwarden konts." @@ -4945,7 +4945,7 @@ "message": "Nodrošinātājs" }, "newClientOrganization": { - "message": "Jauna sniedzēja apvienība" + "message": "Jauna pasūtītāja apvienība" }, "newClientOrganizationDesc": { "message": "Izveidot jaunu pasūtītāja apvienību, kas būs piesaistīta šim kontam kā nodrošinātājam. Tas sniegs iespēju piekļūt šai apvienībai un to pārvaldīt." @@ -5661,7 +5661,7 @@ "message": "Rēķinu vēsture" }, "backToReports": { - "message": "Atgriezties pie atskaitēm" + "message": "Atgriezties pie pārskatiem" }, "organizationPicker": { "message": "Apvienību atlasītājs" @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Nodrošinātāju portāls" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "Skatīt krājumu" }, @@ -7757,7 +7760,7 @@ "description": "Message to encourage the user to start creating machine accounts." }, "machineAccountsNoItemsTitle": { - "message": "Nothing to show yet", + "message": "Vēl nav nekā, ko parādīt", "description": "Title to indicate that there are no machine accounts to display." }, "deleteMachineAccounts": { @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Jāņem vērā: nepiešķirti apvienības vienumi vairs nav redzami skatā \"Visas glabātavas\" dažādās ierīcēs un ir sasniedzami tikai no pārvaldības konsoles, kur šie vienumi jāpiešķir krājumam, lai padarītu tos redzamus." + "restrictedGroupAccessDesc": { + "message": "Sevi nevar pievienot kopai." }, "unassignedItemsBannerSelfHost": { - "message": "Jāņem vērā: 2024. gada 2. maijā nepiešķirti apvienības vienumi vairs nebūs redzami skatā \"Visas glabātavas\" dažādās ierīcēs un būs sasniedzami tikai no pārvaldības konsoles, kur šie vienumi jāpiešķir krājumam, lai padarītu tos redzamus." + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Jāņem vērā: nepiešķirti apvienības vienumi vairs nav redzami skatā \"Visas glabātavas\" dažādās ierīcēs un tagad ir pieejami tikai pārvaldības konsolē." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Jāņem vērā: no 2024. gada 16. maija nepiešķirti apvienības vienumi vairs nebūs redzami skatā \"Visas glabātavas\" dažādās ierīcēs un būs pieejami tikai pārvaldības konsolē." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Piešķirt šos vienumus krājumam", + "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": ", lai padarītu tos redzamus.", + "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." + }, + "deleteProvider": { + "message": "Izdzēst nodrošinātāju" + }, + "deleteProviderConfirmation": { + "message": "Nodrošinātāja izdzēšana ir paliekoša un neatgriezeniska. Jāievada sava galvenā parole, lai apliecinātu nodrošinātāja un visu saistīto datu izdzēšanu." + }, + "deleteProviderName": { + "message": "Nevar izdzēst $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "Ir jāatsaista visi klienti, pirms var izdzēst $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Nodrošinātājs izdzēsts" + }, + "providerDeletedDesc": { + "message": "Nodrošinātājs un visi ar to saistītie dati tika izdzēsti." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "Tika pieprasīts izdzēst šo nodrošinātāju. Jāizmanto zemāk esošā poga, lai apstiprinātu." + }, + "deleteProviderWarning": { + "message": "Nodrošinātāja izdzēšana ir paliekoša. To nevar atsaukt." + }, + "errorAssigningTargetCollection": { + "message": "Kļūda mērķa krājuma piešķiršanā." + }, + "errorAssigningTargetFolder": { + "message": "Kļūda mērķa mapes piešķiršanā." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/ml/messages.json b/apps/web/src/locales/ml/messages.json index 4f81532796..9b6d1ab565 100644 --- a/apps/web/src/locales/ml/messages.json +++ b/apps/web/src/locales/ml/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices 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." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/mr/messages.json b/apps/web/src/locales/mr/messages.json index 781c9000cb..ed04c3a3ef 100644 --- a/apps/web/src/locales/mr/messages.json +++ b/apps/web/src/locales/mr/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices 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." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/my/messages.json b/apps/web/src/locales/my/messages.json index 781c9000cb..ed04c3a3ef 100644 --- a/apps/web/src/locales/my/messages.json +++ b/apps/web/src/locales/my/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices 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." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/nb/messages.json b/apps/web/src/locales/nb/messages.json index 0719b129b3..4e5fb13d15 100644 --- a/apps/web/src/locales/nb/messages.json +++ b/apps/web/src/locales/nb/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices 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." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/ne/messages.json b/apps/web/src/locales/ne/messages.json index e0ca2b4f0a..f411847571 100644 --- a/apps/web/src/locales/ne/messages.json +++ b/apps/web/src/locales/ne/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices 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." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/nl/messages.json b/apps/web/src/locales/nl/messages.json index 3ab66e763b..7bf9b4018f 100644 --- a/apps/web/src/locales/nl/messages.json +++ b/apps/web/src/locales/nl/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Providerportaal" }, + "success": { + "message": "Succes" + }, "viewCollection": { "message": "Collectie weergeven" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Toegang tot machine-account bijgewerkt" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "Je kunt jezelf niet aan een groep toevoegen." }, "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." + }, + "unassignedItemsBannerNotice": { + "message": "Let op: Niet-toegewezen organisatie-items zijn niet langer zichtbaar in de weergave van alle kluissen op verschillende apparaten en zijn nu alleen toegankelijk via de Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Let op: Vanaf 16 mei 2024 zijn niet-toegewezen organisatie-items niet langer zichtbaar in de weergave van alle kluissen op verschillende apparaten en alleen toegankelijk via de Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Deze items toewijzen aan een collectie van de", + "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": "om ze zichtbaar te maken.", + "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." + }, + "deleteProvider": { + "message": "Provider verwijderen" + }, + "deleteProviderConfirmation": { + "message": "Het verwijderen van een provider is definitief en onomkeerbaar. Voer je hoofdwachtwoord in om het verwijderen van de provider en alle bijbehorende gegevens te bevestigen." + }, + "deleteProviderName": { + "message": "Kan $ID$ niet verwijderen", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "Je moet alle clients ontkoppelen voordat je $ID$ kunt verwijderen", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider verwijderd" + }, + "providerDeletedDesc": { + "message": "De organisatie en alle bijhorende gegevens zijn verwijderd." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "Je hebt het verwijderen van deze provider aangevraagd. Gebruik deonderstaande knop om te bevestigen." + }, + "deleteProviderWarning": { + "message": "Het verwijderen van een provider is definitief. Je kunt het niet ongedaan maken." + }, + "errorAssigningTargetCollection": { + "message": "Fout bij toewijzen doelverzameling." + }, + "errorAssigningTargetFolder": { + "message": "Fout bij toewijzen doelmap." + }, + "integrationsAndSdks": { + "message": "Integraties & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integraties" + }, + "integrationsDesc": { + "message": "Automatisch secrets synchroniseren van Bitwarden Secrets Manager met een dienst van derden." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Gebruik Bitwarden Secrets Manager SDK in de volgende programmeertalen om je eigen applicaties te bouwen." + }, + "setUpGithubActions": { + "message": "GitHub Actions instellen" + }, + "setUpGitlabCICD": { + "message": "GitLab CI/CD instellen" + }, + "setUpAnsible": { + "message": "Ansibel instellen" + }, + "cSharpSDKRepo": { + "message": "C#-repository bekijken" + }, + "cPlusPlusSDKRepo": { + "message": "C++-repository bekijken" + }, + "jsWebAssemblySDKRepo": { + "message": "JS WebAssembly-repository bekijken" + }, + "javaSDKRepo": { + "message": "Java-repository bekijken" + }, + "pythonSDKRepo": { + "message": "Python-repository bekijken" + }, + "phpSDKRepo": { + "message": "Php-repository bekijken" + }, + "rubySDKRepo": { + "message": "Ruby-repository bekijken" + }, + "goSDKRepo": { + "message": "Go-repository bekijken" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "Geen toegang" + }, + "collectionAdminConsoleManaged": { + "message": "Deze collectie is alleen toegankelijk vanaf de admin console" } } diff --git a/apps/web/src/locales/nn/messages.json b/apps/web/src/locales/nn/messages.json index a80816b511..3d54ea70fd 100644 --- a/apps/web/src/locales/nn/messages.json +++ b/apps/web/src/locales/nn/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices 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." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/or/messages.json b/apps/web/src/locales/or/messages.json index 781c9000cb..ed04c3a3ef 100644 --- a/apps/web/src/locales/or/messages.json +++ b/apps/web/src/locales/or/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices 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." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/pl/messages.json b/apps/web/src/locales/pl/messages.json index 9532e8d25c..57e0eb1929 100644 --- a/apps/web/src/locales/pl/messages.json +++ b/apps/web/src/locales/pl/messages.json @@ -296,7 +296,7 @@ "message": "Szukaj w organizacji" }, "searchMembers": { - "message": "Szukaj w użytkownikach" + "message": "Szukaj członków" }, "searchGroups": { "message": "Szukaj w grupach" @@ -854,7 +854,7 @@ "message": "Brak użytkowników do wyświetlenia." }, "noMembersInList": { - "message": "Brak użytkowników do wyświetlenia." + "message": "Brak członków do wyświetlenia." }, "noEventsInList": { "message": "Brak wydarzeń do wyświetlenia." @@ -1534,7 +1534,7 @@ "message": "Włącz dwustopniowe logowanie dla swojej organizacji." }, "twoStepLoginEnterpriseDescStart": { - "message": "Wymuszaj opcje logowania dwustopniowego Bitwarden dla użytkowników za pomocą ", + "message": "Wymuszaj opcje logowania dwustopniowego Bitwarden dla członków za pomocą ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Enforce Bitwarden Two-step Login options for members by using the Two-step Login Policy.'" }, "twoStepLoginPolicy": { @@ -2085,7 +2085,7 @@ "message": "Konto Premium" }, "premiumAccessDesc": { - "message": "Możesz przyznać dostęp premium wszystkim użytkownikom w Twojej organizacji za $PRICE$ /$INTERVAL$.", + "message": "Możesz przyznać dostęp premium wszystkim członkom Twojej organizacji za $PRICE$ /$INTERVAL$.", "placeholders": { "price": { "content": "$1", @@ -2541,7 +2541,7 @@ } }, "trialSecretsManagerThankYou": { - "message": "Thanks for signing up for Bitwarden Secrets Manager for $PLAN$!", + "message": "Dziękujemy za zarejestrowanie się do Menedżera Sekretów Bitwarden w planie $PLAN$!", "placeholders": { "plan": { "content": "$1", @@ -2691,7 +2691,7 @@ "message": "Czy na pewno chcesz usunąć tę kolekcję?" }, "editMember": { - "message": "Edytuj użytkownika" + "message": "Edytuj członka" }, "fieldOnTabRequiresAttention": { "message": "Pole na karcie '$TAB$' wymaga uwagi.", @@ -2751,7 +2751,7 @@ "message": "Administrator" }, "adminDesc": { - "message": "Administratorzy mają dostęp do wszystkich elementów, kolekcji i użytkowników w Twojej organizacji." + "message": "Administratorzy mają dostęp do wszystkich elementów, kolekcji i członków Twojej organizacji" }, "user": { "message": "Użytkownik" @@ -3234,7 +3234,7 @@ "message": "Dostęp grupowy" }, "groupAccessUserDesc": { - "message": "Zmień grupy, do których należy użytkownik." + "message": "Przyznaj członkowi dostęp do kolekcji, dodając go do 1 lub więcej grup." }, "invitedUsers": { "message": "Użytkownicy zostali zaproszeni" @@ -3270,10 +3270,10 @@ } }, "confirmUsers": { - "message": "Zatwierdź użytkowników" + "message": "Zatwierdź członków" }, "usersNeedConfirmed": { - "message": "Posiadasz użytkowników, którzy zaakceptowali zaproszenie, ale muszą jeszcze zostać potwierdzeni. Użytkownicy nie będą posiadali dostępu do organizacji, dopóki nie zostaną potwierdzeni." + "message": "Posiadasz członków, którzy zaakceptowali zaproszenie, ale muszą jeszcze zostać potwierdzeni. Członkowie nie będą posiadali dostępu do organizacji, dopóki nie zostaną potwierdzeni." }, "startDate": { "message": "Data rozpoczęcia" @@ -3486,10 +3486,10 @@ "message": "Wpisz identyfikator instalacji" }, "limitSubscriptionDesc": { - "message": "Ustaw limit liczby stanowisk subskrypcji. Po osiągnięciu tego limitu nie będziesz mógł zapraszać nowych użytkowników." + "message": "Ustaw limit liczby subskrypcji. Po osiągnięciu tego limitu nie będziesz mógł zapraszać nowych członków." }, "limitSmSubscriptionDesc": { - "message": "Ustaw limit liczby subskrypcji menedżera sekretów. Po osiągnięciu tego limitu nie będziesz mógł zapraszać nowych użytkowników." + "message": "Ustaw limit liczby subskrypcji menedżera sekretów. Po osiągnięciu tego limitu nie będziesz mógł zapraszać nowych członków." }, "maxSeatLimit": { "message": "Maksymalna liczba stanowisk (opcjonalnie)", @@ -3510,7 +3510,7 @@ "message": "Zmiany w subskrypcji spowodują proporcjonalne zmiany w rozliczeniach. Jeśli nowo zaproszeni użytkownicy przekroczą liczbę stanowisk w subskrypcji, otrzymasz proporcjonalną opłatę za dodatkowych użytkowników." }, "subscriptionUserSeats": { - "message": "Twoja subskrypcja pozwala na łączną liczbę $COUNT$ użytkowników.", + "message": "Twoja subskrypcja pozwala na łączną liczbę $COUNT$ członków.", "placeholders": { "count": { "content": "$1", @@ -3537,10 +3537,10 @@ "message": "Aby uzyskać dodatkową pomoc w zarządzaniu subskrypcją, skontaktuj się z działem obsługi klienta." }, "subscriptionUserSeatsUnlimitedAutoscale": { - "message": "Zmiany w subskrypcji spowodują proporcjonalne zmiany w rozliczeniach. Jeśli nowo zaproszeni użytkownicy przekroczą liczbę stanowisk w subskrypcji, otrzymasz proporcjonalną opłatę za dodatkowych użytkowników." + "message": "Zmiany w subskrypcji spowodują proporcjonalne zmiany w rozliczeniach. Jeśli nowo zaproszeni członkowie przekroczą liczbę miejsc w subskrypcji, otrzymasz proporcjonalną opłatę za dodatkowych członków." }, "subscriptionUserSeatsLimitedAutoscale": { - "message": "Korekty subskrypcji spowodują proporcjonalne zmiany w sumach rozliczeniowych. Jeśli nowo zaproszeni użytkownicy przekroczą liczbę miejsc objętych subskrypcją, natychmiast otrzymasz proporcjonalną opłatę za dodatkowych użytkowników, aż do osiągnięcia limitu miejsc $MAX$.", + "message": "Korekty subskrypcji spowodują proporcjonalne zmiany w sumach rozliczeniowych. Jeśli nowo zaproszeni członkowie przekroczą liczbę miejsc objętych subskrypcją, natychmiast otrzymasz proporcjonalną opłatę za dodatkowych członków, aż do osiągnięcia limitu miejsc $MAX$.", "placeholders": { "max": { "content": "$1", @@ -4083,7 +4083,7 @@ "message": "Identyfikator SSO" }, "ssoIdentifierHintPartOne": { - "message": "Podaj ten identyfikator swoim użytkownikom, aby zalogować się za pomocą SSO. Aby pominąć ten krok, ustaw ", + "message": "Podaj ten identyfikator swoim członkowm, aby zalogować się za pomocą SSO. Aby pominąć ten krok, ustaw ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" }, "unlinkSso": { @@ -5210,7 +5210,7 @@ "message": "Zweryfikuj certyfikaty" }, "spUniqueEntityId": { - "message": "Set a unique SP entity ID" + "message": "Ustaw unikalny identyfikator jednostki SP" }, "spUniqueEntityIdDesc": { "message": "Wygeneruj identyfikator unikalny dla Twojej organizacji" @@ -5426,13 +5426,13 @@ "message": "Opcje odszyfrowania użytkownika" }, "memberDecryptionPassDesc": { - "message": "Po uwierzytelnieniu użytkownicy odszyfrują dane sejfu za pomocą hasła głównego." + "message": "Po uwierzytelnieniu członkowie odszyfrują dane sejfu za pomocą hasła głównego." }, "keyConnector": { "message": "Key Connector" }, "memberDecryptionKeyConnectorDescStart": { - "message": "Połącz logowanie za pomocą SSO z Twoim serwerem kluczy odszyfrowania. Używając tej opcji, użytkownicy nie będą musieli używać swoich haseł głównych, aby odszyfrować dane sejfu.", + "message": "Połącz logowanie za pomocą SSO z Twoim serwerem kluczy odszyfrowania. Używając tej opcji, członkowie nie będą musieli używać swoich haseł głównych, aby odszyfrować dane sejfu.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Connect login with SSO to your self-hosted decryption key server. Using this option, members won’t need to use their master passwords to decrypt vault data. The require SSO authentication and single organization policies are required to set up Key Connector decryption. Contact Bitwarden Support for set up assistance.'" }, "memberDecryptionKeyConnectorDescLink": { @@ -6455,7 +6455,7 @@ "message": "Informacje o grupie" }, "editGroupMembersDesc": { - "message": "Przyznaj użytkownikom dostęp do kolekcji przypisanych do grupy." + "message": "Przyznaj członkom dostęp do kolekcji przypisanych do grupy." }, "editGroupCollectionsDesc": { "message": "Przyznaj dostęp do kolekcji, dodając je do tej grupy." @@ -6467,7 +6467,7 @@ "message": "Jeśli zaznaczone, zastąpi to wszystkie inne uprawnienia kolekcji." }, "selectMembers": { - "message": "Wybierz użytkowników" + "message": "Wybierz członków" }, "selectCollections": { "message": "Wybierz kolekcje" @@ -6476,7 +6476,7 @@ "message": "Rola" }, "removeMember": { - "message": "Usuń użytkownika" + "message": "Usuń członka" }, "collection": { "message": "Kolekcja" @@ -6500,7 +6500,7 @@ "message": "Nie dodano kolekcji" }, "noMembersAdded": { - "message": "Nie dodano użytkowników" + "message": "Nie dodano członków" }, "noGroupsAdded": { "message": "Nie dodano grup" @@ -6680,13 +6680,13 @@ "message": "Wysłać kod ponownie" }, "memberColumnHeader": { - "message": "Member" + "message": "Członek" }, "groupSlashMemberColumnHeader": { - "message": "Group/Member" + "message": "Grupa/Członek" }, "selectGroupsAndMembers": { - "message": "Wybierz grupy i użytkowników" + "message": "Wybierz grupy i członków" }, "selectGroups": { "message": "Wybierz grupy" @@ -6695,22 +6695,22 @@ "message": "Uprawnienia ustawione dla użytkownika zastąpią uprawnienia ustawione przez grupę tego użytkownika" }, "noMembersOrGroupsAdded": { - "message": "Nie dodano użytkowników lub grup" + "message": "Nie dodano członków lub grup" }, "deleted": { "message": "Usunięto" }, "memberStatusFilter": { - "message": "Filtr statusu użytkownika" + "message": "Filtr statusu członka" }, "inviteMember": { - "message": "Zaproś użytkownika" + "message": "Zaproś członka" }, "needsConfirmation": { "message": "Wymaga potwierdzenia" }, "memberRole": { - "message": "Rola użytkownika" + "message": "Rola członka" }, "moreFromBitwarden": { "message": "Więcej od Bitwarden" @@ -7105,7 +7105,7 @@ "message": "Zaufane urządzenia" }, "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Po uwierzytelnieniu użytkownicy odszyfrowają dane sejfu przy użyciu klucza zapisanego na ich urządzeniu.", + "message": "Po uwierzytelnieniu członkowie odszyfrowają dane sejfu przy użyciu klucza zapisanego na ich urządzeniu.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" }, "memberDecryptionOptionTdeDescriptionLinkOne": { @@ -7229,7 +7229,7 @@ "message": "Brak hasła głównego" }, "removeMembersWithoutMasterPasswordWarning": { - "message": "Usuwanie użytkowników, którzy nie mają hasła głównego bez ustawienia go, może ograniczyć dostęp do ich pełnych kont." + "message": "Usuwanie członków, którzy nie mają hasła głównego bez ustawienia go, może ograniczyć dostęp do ich pełnych kont." }, "approvedAuthRequest": { "message": "Zatwierdzone urządzenie dla $ID$.", @@ -7262,7 +7262,7 @@ } }, "startYour7DayFreeTrialOfBitwardenSecretsManagerFor": { - "message": "Start your 7-Day free trial of Bitwarden Secrets Manager for $ORG$", + "message": "Rozpocznij 7-dniowy darmowy okres próbny Menedżera Sekretów Bitwarden dla $ORG$", "placeholders": { "org": { "content": "$1", @@ -7508,7 +7508,7 @@ "message": "Przyznaj grupom lub członkom dostęp do tej kolekcji." }, "grantCollectionAccessMembersOnly": { - "message": "Grant members access to this collection." + "message": "Przyznaj członkom dostęp do tej kolekcji." }, "adminCollectionAccess": { "message": "Administratorzy mogą mieć dostęp do kolekcji i zarządzać nimi." @@ -7548,7 +7548,7 @@ "message": "Szczegóły potwierdzenia" }, "smFreeTrialThankYou": { - "message": "Thank you for signing up for Bitwarden Secrets Manager!" + "message": "Dziękujemy za zarejestrowanie się do Menedżera Sekretów Bitwarden!" }, "smFreeTrialConfirmationEmail": { "message": "Wysłaliśmy e-mail weryfikacyjny na adres " @@ -7557,7 +7557,7 @@ "message": "Ta akcja jest nieodwracalna" }, "confirmCollectionEnhancementsDialogContent": { - "message": "Turning on this feature will deprecate the manager role and replace it with a Can manage permission. This will take a few moments. Do not make any organization changes until it is complete. Are you sure you want to proceed?" + "message": "Włączenie tej funkcji spowoduje deprekację roli menedżera i zastąpi ją uprawnieniami do zarządzania uprawnieniami. To zajmie kilka chwil. Nie wprowadzaj żadnych zmian w organizacji dopóki nie zostanie zakończone. Czy na pewno chcesz kontynuować?" }, "sorryToSeeYouGo": { "message": "Przykro nam, że nas opuszczasz! Pomóż ulepszyć Bitwarden udostępniając informacje dlaczego anulujesz.", @@ -7598,7 +7598,7 @@ "message": "Witaj w nowej i ulepszonej aplikacji internetowej. Dowiedz się więcej o tym, co się zmieniło." }, "releaseBlog": { - "message": "Read release blog" + "message": "Przeczytaj blog z informacjami o wydaniu" }, "adminConsole": { "message": "Konsola administratora" @@ -7606,14 +7606,17 @@ "providerPortal": { "message": "Portal dostawcy" }, + "success": { + "message": "Sukces" + }, "viewCollection": { "message": "Zobacz kolekcję" }, "restrictedGroupAccess": { - "message": "You cannot add yourself to groups." + "message": "Nie możesz dodać siebie do grup." }, "restrictedCollectionAccess": { - "message": "You cannot add yourself to collections." + "message": "Nie możesz dodać siebie do kolekcji." }, "assign": { "message": "Przypisz" @@ -7738,42 +7741,42 @@ "description": "The date header used when a subscription is cancelled." }, "machineAccountsCannotCreate": { - "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + "message": "Konta dla maszyn nie mogą być tworzone w zawieszonych organizacjach. Skontaktuj się z właścicielem organizacji, aby uzyskać pomoc." }, "machineAccount": { - "message": "Machine account", + "message": "Konto dla maszyny", "description": "A machine user which can be used to automate processes and access secrets in the system." }, "machineAccounts": { - "message": "Machine accounts", + "message": "Konta dla maszyn", "description": "The title for the section that deals with machine accounts." }, "newMachineAccount": { - "message": "New machine account", + "message": "Nowe konto dla maszyny", "description": "Title for creating a new machine account." }, "machineAccountsNoItemsMessage": { - "message": "Create a new machine account to get started automating secret access.", + "message": "Utwórz nowe konto dla maszyny, aby rozpocząć automatyzację dostępu do sekretu.", "description": "Message to encourage the user to start creating machine accounts." }, "machineAccountsNoItemsTitle": { - "message": "Nothing to show yet", + "message": "Nie ma nic do pokazania", "description": "Title to indicate that there are no machine accounts to display." }, "deleteMachineAccounts": { - "message": "Delete machine accounts", + "message": "Usuń konta maszyn", "description": "Title for the action to delete one or multiple machine accounts." }, "deleteMachineAccount": { - "message": "Delete machine account", + "message": "Usuń konto maszyny", "description": "Title for the action to delete a single machine account." }, "viewMachineAccount": { - "message": "View machine account", + "message": "Zobacz konto maszyny", "description": "Action to view the details of a machine account." }, "deleteMachineAccountDialogMessage": { - "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "message": "Usuwanie konta maszyny $MACHINE_ACCOUNT$ jest trwałe i nieodwracalne.", "placeholders": { "machine_account": { "content": "$1", @@ -7782,10 +7785,10 @@ } }, "deleteMachineAccountsDialogMessage": { - "message": "Deleting machine accounts is permanent and irreversible." + "message": "Usuwanie kont maszyn jest trwałe i nieodwracalne." }, "deleteMachineAccountsConfirmMessage": { - "message": "Delete $COUNT$ machine accounts", + "message": "Usuń $COUNT$ kont maszyn", "placeholders": { "count": { "content": "$1", @@ -7794,60 +7797,60 @@ } }, "deleteMachineAccountToast": { - "message": "Machine account deleted" + "message": "Konto maszyny usunięte" }, "deleteMachineAccountsToast": { - "message": "Machine accounts deleted" + "message": "Konta maszyn usunięte" }, "searchMachineAccounts": { - "message": "Search machine accounts", + "message": "Szukaj kont maszyn", "description": "Placeholder text for searching machine accounts." }, "editMachineAccount": { - "message": "Edit machine account", + "message": "Edytuj konto maszyny", "description": "Title for editing a machine account." }, "machineAccountName": { - "message": "Machine account name", + "message": "Nazwa konta maszyny", "description": "Label for the name of a machine account" }, "machineAccountCreated": { - "message": "Machine account created", + "message": "Konto maszyny utworzone", "description": "Notifies that a new machine account has been created" }, "machineAccountUpdated": { - "message": "Machine account updated", + "message": "Konto maszyny zaktualizowane", "description": "Notifies that a machine account has been updated" }, "projectMachineAccountsDescription": { - "message": "Grant machine accounts access to this project." + "message": "Przyznaj dostęp kontom maszynowym do tego projektu." }, "projectMachineAccountsSelectHint": { - "message": "Type or select machine accounts" + "message": "Wpisz lub wybierz konta maszyn" }, "projectEmptyMachineAccountAccessPolicies": { - "message": "Add machine accounts to grant access" + "message": "Dodaj konta maszyn, aby udzielić dostępu" }, "machineAccountPeopleDescription": { - "message": "Grant groups or people access to this machine account." + "message": "Przyznaj grupom lub ludziom dostęp do tego konta maszyny." }, "machineAccountProjectsDescription": { - "message": "Assign projects to this machine account. " + "message": "Przypisz projekty do tego konta maszyny. " }, "createMachineAccount": { - "message": "Create a machine account" + "message": "Utwórz konto dla maszyny" }, "maPeopleWarningMessage": { - "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + "message": "Usuwanie osób z konta maszyny nie usuwa tokenów dostępu. Dla zapewnienia najlepszej praktyki bezpieczeństwa zaleca się cofnięcie tokenów dostępu stworzonych przez osoby usunięte z konta maszyny." }, "smAccessRemovalWarningMaTitle": { - "message": "Remove access to this machine account" + "message": "Usuń dostęp do tego konta maszyny" }, "smAccessRemovalWarningMaMessage": { - "message": "This action will remove your access to the machine account." + "message": "Ta akcja usunie Twój dostęp do konta maszyny." }, "machineAccountsIncluded": { - "message": "$COUNT$ machine accounts included", + "message": "Uwzględnione konta maszyn: $COUNT$", "placeholders": { "count": { "content": "$1", @@ -7856,7 +7859,7 @@ } }, "additionalMachineAccountCost": { - "message": "$COST$ per month for additional machine accounts", + "message": "$COST$ miesięcznie dla dodatkowych kont dla maszyn", "placeholders": { "cost": { "content": "$1", @@ -7865,10 +7868,10 @@ } }, "additionalMachineAccounts": { - "message": "Additional machine accounts" + "message": "Dodatkowe konta dla maszyn" }, "includedMachineAccounts": { - "message": "Your plan comes with $COUNT$ machine accounts.", + "message": "Twój plan obejmuje $COUNT$ kont dla maszyn.", "placeholders": { "count": { "content": "$1", @@ -7877,7 +7880,7 @@ } }, "addAdditionalMachineAccounts": { - "message": "You can add additional machine accounts for $COST$ per month.", + "message": "Możesz dodać dodatkowe konta dla maszyn za $COST$ miesięcznie.", "placeholders": { "cost": { "content": "$1", @@ -7886,24 +7889,156 @@ } }, "limitMachineAccounts": { - "message": "Limit machine accounts (optional)" + "message": "Limit kont dla maszyn (opcjonalnie)" }, "limitMachineAccountsDesc": { - "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + "message": "Ustaw limit liczby kont dla maszyn. Po osiągnięciu tego limitu nie będziesz mógł tworzyć nowych kont." }, "machineAccountLimit": { - "message": "Machine account limit (optional)" + "message": "Limit konta dla maszyn (opcjonalnie)" }, "maxMachineAccountCost": { - "message": "Max potential machine account cost" + "message": "Maksymalny potencjalny koszt kont dla maszyn" }, "machineAccountAccessUpdated": { - "message": "Machine account access updated" + "message": "Zaktualizowano dostęp do konta dla maszyny" }, - "unassignedItemsBanner": { - "message": "Uwaga: Nieprzypisane elementy w organizacji nie są już widoczne w widoku Wszystkie sejfy na urządzeniach i są teraz dostępne tylko przez Konsolę Administracyjną. Przypisz te elementy do kolekcji z konsoli administracyjnej, aby były one widoczne." + "restrictedGroupAccessDesc": { + "message": "Nie możesz dodać siebie do grupy." }, "unassignedItemsBannerSelfHost": { "message": "Uwaga: 2 maja 2024 r. nieprzypisane elementy w organizacji nie będą już widoczne w widoku Wszystkie sejfy na urządzeniach i będą dostępne tylko przez Konsolę Administracyjną. Przypisz te elementy do kolekcji z konsoli administracyjnej, aby były one widoczne." + }, + "unassignedItemsBannerNotice": { + "message": "Uwaga: Nieprzypisane elementy organizacji nie są już widoczne w widoku Wszystkie sejfy na urządzeniach i są teraz dostępne tylko przez Konsolę Administracyjną." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Uwaga: 16 maja 2024 r. nieprzypisana elementy w organizacji nie będą już widoczne w widoku Wszystkie sejfy na urządzeniach i będą dostępne tylko przez Konsolę Administracyjną." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Przypisz te elementy do kolekcji z", + "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": ", aby były widoczne.", + "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." + }, + "deleteProvider": { + "message": "Usuń dostawcę" + }, + "deleteProviderConfirmation": { + "message": "Usunięcie dostawcy jest trwałe i nieodwracalne. Wprowadź swoje hasło główne, aby potwierdzić usunięcie dostawcy i wszystkich powiązanych danych." + }, + "deleteProviderName": { + "message": "Nie można usunąć $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "Musisz odłączyć wszystkich klientów zanim będziesz mógł usunąć $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Dostawca został usunięty" + }, + "providerDeletedDesc": { + "message": "Dostawca i wszystkie powiązane dane zostały usunięte." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "Poprosiłeś o usunięcie tego dostawcy. Kliknij poniższy przycisk, aby to potwierdzić." + }, + "deleteProviderWarning": { + "message": "Usunięcie dostawcy jest nieodwracalne. Ta czynność nie może zostać cofnięta." + }, + "errorAssigningTargetCollection": { + "message": "Wystąpił błąd podczas przypisywania kolekcji." + }, + "errorAssigningTargetFolder": { + "message": "Wystąpił błąd podczas przypisywania folderu." + }, + "integrationsAndSdks": { + "message": "Integracje i SDK", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integracje" + }, + "integrationsDesc": { + "message": "Automatycznie synchronizuj sekrety z Menedżera Sekretnych Bitwarden do usługi innej firmy." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Użyj SDK Menedżera Sekretów Bitwarden w następujących językach programowania, aby zbudować własne aplikacje." + }, + "setUpGithubActions": { + "message": "Skonfiguruj Github Actions" + }, + "setUpGitlabCICD": { + "message": "Skonfiguruj GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Skonfiguruj Ansible" + }, + "cSharpSDKRepo": { + "message": "Zobacz repozytorium C#" + }, + "cPlusPlusSDKRepo": { + "message": "Zobacz repozytorium C++" + }, + "jsWebAssemblySDKRepo": { + "message": "Zobacz repozytorium JS WebAssembly" + }, + "javaSDKRepo": { + "message": "Zobacz repozytorium Java" + }, + "pythonSDKRepo": { + "message": "Zobacz repozytorium Pythona" + }, + "phpSDKRepo": { + "message": "Zobacz repozytorium php" + }, + "rubySDKRepo": { + "message": "Zobacz repozytorium Ruby" + }, + "goSDKRepo": { + "message": "Zobacz repozytorium Go" + }, + "createNewClientToManageAsProvider": { + "message": "Utwórz nową organizację klienta do zarządzania jako dostawca. Dodatkowe miejsca zostaną odzwierciedlone w następnym cyklu rozliczeniowym." + }, + "selectAPlan": { + "message": "Wybierz plan" + }, + "thirtyFivePercentDiscount": { + "message": "Zniżka 35%" + }, + "monthPerMember": { + "message": "miesięcznie za członka" + }, + "seats": { + "message": "Miejsca" + }, + "addOrganization": { + "message": "Dodaj organizację" + }, + "createdNewClient": { + "message": "Pomyślnie utworzono nowego klienta" + }, + "noAccess": { + "message": "Brak dostępu" + }, + "collectionAdminConsoleManaged": { + "message": "Ta kolekcja jest dostępna tylko z konsoli administracyjnej" } } diff --git a/apps/web/src/locales/pt_BR/messages.json b/apps/web/src/locales/pt_BR/messages.json index 084d4c95d9..4f43c2aad7 100644 --- a/apps/web/src/locales/pt_BR/messages.json +++ b/apps/web/src/locales/pt_BR/messages.json @@ -579,7 +579,7 @@ "message": "Acesso" }, "accessLevel": { - "message": "Access level" + "message": "Nível de acesso" }, "loggedOut": { "message": "Sessão encerrada" @@ -672,7 +672,7 @@ "message": "Criptografia não suportada" }, "enablePasskeyEncryption": { - "message": "Set up encryption" + "message": "Configurar criptografia" }, "usedForEncryption": { "message": "Usado para criptografia" @@ -4951,13 +4951,13 @@ "message": "Crie uma nova organização de cliente que será associada a você como o provedor. Você poderá acessar e gerenciar esta organização." }, "newClient": { - "message": "New client" + "message": "Novo cliente" }, "addExistingOrganization": { "message": "Adicionar Organização Existente" }, "addNewOrganization": { - "message": "Add new organization" + "message": "Adicionar nova organização" }, "myProvider": { "message": "Meu Provedor" @@ -5940,13 +5940,13 @@ } }, "launchDuoAndFollowStepsToFinishLoggingIn": { - "message": "Launch Duo and follow the steps to finish logging in." + "message": "Inicie o Duo e siga os passos para finalizar o login." }, "duoRequiredByOrgForAccount": { - "message": "Duo two-step login is required for your account." + "message": "A autenticação em duas etapas do Duo é necessária para sua conta." }, "launchDuo": { - "message": "Launch Duo" + "message": "Abrir o Duo" }, "turnOn": { "message": "Ligar" @@ -6680,10 +6680,10 @@ "message": "Reenviar código" }, "memberColumnHeader": { - "message": "Member" + "message": "Membro" }, "groupSlashMemberColumnHeader": { - "message": "Group/Member" + "message": "Grupo/Membro" }, "selectGroupsAndMembers": { "message": "Selecione grupos e membros" @@ -7450,7 +7450,7 @@ "message": "Já tem uma conta?" }, "skipToContent": { - "message": "Skip to content" + "message": "Pular para o conteúdo" }, "managePermissionRequired": { "message": "Pelo menos um membro ou grupo deve ter poder gerenciar a permissão." @@ -7502,13 +7502,13 @@ "message": "O acesso à coleção está restrito" }, "readOnlyCollectionAccess": { - "message": "You do not have access to manage this collection." + "message": "Você não tem acesso para gerenciar esta coleção." }, "grantCollectionAccess": { "message": "Grant groups or members access to this collection." }, "grantCollectionAccessMembersOnly": { - "message": "Grant members access to this collection." + "message": "Conceder acesso a essa coleção." }, "adminCollectionAccess": { "message": "Administrators can access and manage collections." @@ -7557,57 +7557,60 @@ "message": "Esta ação é irreversível" }, "confirmCollectionEnhancementsDialogContent": { - "message": "Turning on this feature will deprecate the manager role and replace it with a Can manage permission. This will take a few moments. Do not make any organization changes until it is complete. Are you sure you want to proceed?" + "message": "Ativar este recurso irá depreciar a função de gerente e substituí-lo por uma Permissão de para Gerenciar. Isso levará algum tempo. Não faça nenhuma alteração de organização até que esteja concluída. Tem certeza que deseja continuar?" }, "sorryToSeeYouGo": { - "message": "Sorry to see you go! Help improve Bitwarden by sharing why you're canceling.", + "message": "Lamentamos vê-lo ir! Ajude a melhorar o Bitwarden compartilhando o motivo de você estar cancelando.", "description": "A message shown to users as part of an offboarding survey asking them to provide more information on their subscription cancelation." }, "selectCancellationReason": { - "message": "Select a reason for canceling", + "message": "Selecione o motivo do cancelamento", "description": "Used as a form field label for a select input on the offboarding survey." }, "anyOtherFeedback": { - "message": "Is there any other feedback you'd like to share?", + "message": "Existe algum outro comentário que você gostaria de compartilhar?", "description": "Used as a form field label for a textarea input on the offboarding survey." }, "missingFeatures": { - "message": "Missing features", + "message": "Recursos ausentes", "description": "An option for the offboarding survey shown when a user cancels their subscription." }, "movingToAnotherTool": { - "message": "Moving to another tool", + "message": "Trocando de ferramenta", "description": "An option for the offboarding survey shown when a user cancels their subscription." }, "tooDifficultToUse": { - "message": "Too difficult to use", + "message": "Muito difícil de usar", "description": "An option for the offboarding survey shown when a user cancels their subscription." }, "notUsingEnough": { - "message": "Not using enough", + "message": "Não está usando o suficiente", "description": "An option for the offboarding survey shown when a user cancels their subscription." }, "tooExpensive": { - "message": "Too expensive", + "message": "Muito caro", "description": "An option for the offboarding survey shown when a user cancels their subscription." }, "freeForOneYear": { "message": "Grátis por 1 ano" }, "newWebApp": { - "message": "Welcome to the new and improved web app. Learn more about what’s changed." + "message": "Bem-vindo ao novo e melhorado aplicativo da web. Saiba mais sobre o que mudou." }, "releaseBlog": { - "message": "Read release blog" + "message": "Ler o blog do lançamento" }, "adminConsole": { - "message": "Admin Console" + "message": "Painel de administração" }, "providerPortal": { - "message": "Provider Portal" + "message": "Portal do provedor" + }, + "success": { + "message": "Success" }, "viewCollection": { - "message": "View collection" + "message": "Ver Coleção" }, "restrictedGroupAccess": { "message": "Você não pode se adicionar aos grupos." @@ -7616,28 +7619,28 @@ "message": "Você não pode se adicionar às coleções." }, "assign": { - "message": "Assign" + "message": "Atribuir" }, "assignToCollections": { - "message": "Assign to collections" + "message": "Atribuir à coleções" }, "assignToTheseCollections": { - "message": "Assign to these collections" + "message": "Atribuir a estas coleções" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Selecione as coleções com as quais os itens serão compartilhados. Assim que um item for atualizado em uma coleção, isso será refletido em todas as coleções. Apenas membros da organização com acesso a essas coleções poderão ver os itens." }, "selectCollectionsToAssign": { - "message": "Select collections to assign" + "message": "Selecione as coleções para atribuir" }, "noCollectionsAssigned": { - "message": "No collections have been assigned" + "message": "Nenhuma coleção foi atribuída" }, "successfullyAssignedCollections": { - "message": "Successfully assigned collections" + "message": "Coleções atribuídas com sucesso" }, "bulkCollectionAssignmentWarning": { - "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "message": "Você selecionou $TOTAL_COUNT$ itens. Você não pode atualizar $READONLY_COUNT$ destes itens porque você não tem permissão para edição.", "placeholders": { "total_count": { "content": "$1", @@ -7650,55 +7653,55 @@ } }, "items": { - "message": "Items" + "message": "Itens" }, "assignedSeats": { - "message": "Assigned seats" + "message": "Lugares Atribuídos" }, "assigned": { - "message": "Assigned" + "message": "Atribuído" }, "used": { - "message": "Used" + "message": "Utilizado" }, "remaining": { - "message": "Remaining" + "message": "Restante" }, "unlinkOrganization": { - "message": "Unlink organization" + "message": "Desvincular organização" }, "manageSeats": { - "message": "MANAGE SEATS" + "message": "GERENCIAR LUGARES" }, "manageSeatsDescription": { - "message": "Adjustments to seats will be reflected in the next billing cycle." + "message": "Os ajustes nos lugares serão refletidos no próximo ciclo de faturamento." }, "unassignedSeatsDescription": { - "message": "Unassigned subscription seats" + "message": "Assinatura de assentos desvinculada" }, "purchaseSeatDescription": { - "message": "Additional seats purchased" + "message": "Assentos adicionais comprados" }, "assignedSeatCannotUpdate": { - "message": "Assigned Seats can not be updated. Please contact your organization owner for assistance." + "message": "Os assentos atribuídos não podem ser atualizados. Por favor, entre em contato com o proprietário da sua organização para obter assistência." }, "subscriptionUpdateFailed": { - "message": "Subscription update failed" + "message": "Atualização da assinatura falhou" }, "trial": { - "message": "Trial", + "message": "Avaliação", "description": "A subscription status label." }, "pastDue": { - "message": "Past due", + "message": "Atrasado", "description": "A subscription status label" }, "subscriptionExpired": { - "message": "Subscription expired", + "message": "Assinatura expirada", "description": "The date header used when a subscription is past due." }, "pastDueWarningForChargeAutomatically": { - "message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "message": "Você tem um período de carência de $DAYS$ dias a contar da data de expiração para manter sua assinatura. Por favor, resolva as faturas vencidas até $SUSPENSION_DATE$.", "placeholders": { "days": { "content": "$1", @@ -7712,7 +7715,7 @@ "description": "A warning shown to the user when their subscription is past due and they are charged automatically." }, "pastDueWarningForSendInvoice": { - "message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "message": "Você tem um período de licença de $DAYS$, contados a partir da data de vencimento da sua primeira fatura em aberto para manter a sua assinatura. Por favor, resolva as faturas vencidas até $SUSPENSION_DATE$.", "placeholders": { "days": { "content": "$1", @@ -7726,54 +7729,54 @@ "description": "A warning shown to the user when their subscription is past due and they pay via invoice." }, "unpaidInvoice": { - "message": "Unpaid invoice", + "message": "Fatura não paga", "description": "The header of a warning box shown to a user whose subscription is unpaid." }, "toReactivateYourSubscription": { - "message": "To reactivate your subscription, please resolve the past due invoices.", + "message": "Para reativar sua assinatura, por favor resolva as faturas vencidas.", "description": "The body of a warning box shown to a user whose subscription is unpaid." }, "cancellationDate": { - "message": "Cancellation date", + "message": "Data de cancelamento", "description": "The date header used when a subscription is cancelled." }, "machineAccountsCannotCreate": { - "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + "message": "Contas de máquina não podem ser criadas em organizações suspensas. Entre em contato com o proprietário da organização para obter assistência." }, "machineAccount": { - "message": "Machine account", + "message": "Conta de máquina", "description": "A machine user which can be used to automate processes and access secrets in the system." }, "machineAccounts": { - "message": "Machine accounts", + "message": "Contas de máquina", "description": "The title for the section that deals with machine accounts." }, "newMachineAccount": { - "message": "New machine account", + "message": "Nova conta de máquina", "description": "Title for creating a new machine account." }, "machineAccountsNoItemsMessage": { - "message": "Create a new machine account to get started automating secret access.", + "message": "Crie uma nova conta de máquina para começar a automatizar o acesso secreto.", "description": "Message to encourage the user to start creating machine accounts." }, "machineAccountsNoItemsTitle": { - "message": "Nothing to show yet", + "message": "Ainda não há nada a ser exibido", "description": "Title to indicate that there are no machine accounts to display." }, "deleteMachineAccounts": { - "message": "Delete machine accounts", + "message": "Excluir contas de máquina", "description": "Title for the action to delete one or multiple machine accounts." }, "deleteMachineAccount": { - "message": "Delete machine account", + "message": "Excluir conta de máquina", "description": "Title for the action to delete a single machine account." }, "viewMachineAccount": { - "message": "View machine account", + "message": "Ver conta de máquina", "description": "Action to view the details of a machine account." }, "deleteMachineAccountDialogMessage": { - "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "message": "A exclusão da conta de máquina $MACHINE_ACCOUNT$ é permanente e irreversível.", "placeholders": { "machine_account": { "content": "$1", @@ -7782,10 +7785,10 @@ } }, "deleteMachineAccountsDialogMessage": { - "message": "Deleting machine accounts is permanent and irreversible." + "message": "Excluir contas de máquina é permanente e irreversível." }, "deleteMachineAccountsConfirmMessage": { - "message": "Delete $COUNT$ machine accounts", + "message": "Excluir $COUNT$ contas de máquina", "placeholders": { "count": { "content": "$1", @@ -7794,60 +7797,60 @@ } }, "deleteMachineAccountToast": { - "message": "Machine account deleted" + "message": "Conta de máquina excluída" }, "deleteMachineAccountsToast": { - "message": "Machine accounts deleted" + "message": "Contas de máquina excluídas" }, "searchMachineAccounts": { - "message": "Search machine accounts", + "message": "Pesquisar contas de máquina", "description": "Placeholder text for searching machine accounts." }, "editMachineAccount": { - "message": "Edit machine account", + "message": "Editar conta da máquina", "description": "Title for editing a machine account." }, "machineAccountName": { - "message": "Machine account name", + "message": "Nome da conta de máquina", "description": "Label for the name of a machine account" }, "machineAccountCreated": { - "message": "Machine account created", + "message": "Conta de máquina criada", "description": "Notifies that a new machine account has been created" }, "machineAccountUpdated": { - "message": "Machine account updated", + "message": "Conta de máquina atualizada", "description": "Notifies that a machine account has been updated" }, "projectMachineAccountsDescription": { - "message": "Grant machine accounts access to this project." + "message": "Conceder acesso a contas de máquina a este projeto." }, "projectMachineAccountsSelectHint": { - "message": "Type or select machine accounts" + "message": "Digite ou selecione contas de máquina" }, "projectEmptyMachineAccountAccessPolicies": { - "message": "Add machine accounts to grant access" + "message": "Adicione contas de máquina para conceder acesso" }, "machineAccountPeopleDescription": { - "message": "Grant groups or people access to this machine account." + "message": "Conceder acesso de grupos ou pessoas a esta conta de máquina." }, "machineAccountProjectsDescription": { - "message": "Assign projects to this machine account. " + "message": "Atribuir projetos a esta conta de máquina. " }, "createMachineAccount": { - "message": "Create a machine account" + "message": "Criar uma conta de máquina" }, "maPeopleWarningMessage": { - "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + "message": "Remover pessoas de uma conta de máquina não remove os tokens de acesso que elas criaram. Por melhores práticas de segurança, é recomendado revogar os tokens de acesso criados por pessoas que foram removidas de uma conta de máquina." }, "smAccessRemovalWarningMaTitle": { - "message": "Remove access to this machine account" + "message": "Remover acesso a esta conta de máquina" }, "smAccessRemovalWarningMaMessage": { - "message": "This action will remove your access to the machine account." + "message": "Esta ação irá remover seu acesso à conta de máquina." }, "machineAccountsIncluded": { - "message": "$COUNT$ machine accounts included", + "message": "$COUNT$ contas de serviço incluídas", "placeholders": { "count": { "content": "$1", @@ -7856,7 +7859,7 @@ } }, "additionalMachineAccountCost": { - "message": "$COST$ per month for additional machine accounts", + "message": "$COST$ mensal para contas de máquina adicionais", "placeholders": { "cost": { "content": "$1", @@ -7865,10 +7868,10 @@ } }, "additionalMachineAccounts": { - "message": "Additional machine accounts" + "message": "Contas de máquina adicionais" }, "includedMachineAccounts": { - "message": "Your plan comes with $COUNT$ machine accounts.", + "message": "Seu plano vem com $COUNT$ contas de máquina.", "placeholders": { "count": { "content": "$1", @@ -7877,7 +7880,7 @@ } }, "addAdditionalMachineAccounts": { - "message": "You can add additional machine accounts for $COST$ per month.", + "message": "Você pode adicionar contas de máquina extras por $COST$ mensais.", "placeholders": { "cost": { "content": "$1", @@ -7886,24 +7889,156 @@ } }, "limitMachineAccounts": { - "message": "Limit machine accounts (optional)" + "message": "Limitar contas de máquina (opcional)" }, "limitMachineAccountsDesc": { - "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + "message": "Defina um limite para suas contas de máquina. Quando este limite for atingido, você não poderá criar novas contas de máquina." }, "machineAccountLimit": { - "message": "Machine account limit (optional)" + "message": "Limite de conta de máquina (opcional)" }, "maxMachineAccountCost": { - "message": "Max potential machine account cost" + "message": "Custo máximo de conta de máquina" }, "machineAccountAccessUpdated": { - "message": "Machine account access updated" + "message": "Acesso a conta de máquina atualizado" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "Você não pode adicionar você mesmo a um grupo." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Aviso: Itens de organização não atribuídos não estão mais visíveis na sua tela Todos os Cofres através dos dispositivos e agora só são acessíveis por meio do Console de Administração." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Aviso: Em 16 de maio, 2024, itens da organização que não foram atribuídos não estarão mais visíveis em sua visualização de Todos os Cofres dos dispositivos e só serão acessíveis por meio do painel de administração." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Atribua estes itens a uma coleção da", + "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": "para torná-los visíveis.", + "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." + }, + "deleteProvider": { + "message": "Excluir Provedor" + }, + "deleteProviderConfirmation": { + "message": "A exclusão de um provedor é permanente e irreversível. Digite sua senha mestra para confirmar a exclusão do provedor e de todos os dados associados." + }, + "deleteProviderName": { + "message": "Não é possível excluir $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "Você deve desvincular todos os clientes antes de excluir $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provedor excluído" + }, + "providerDeletedDesc": { + "message": "O provedor e todos os dados associados foram excluídos." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "Você pediu para excluir este Provedor. Clique no botão abaixo para confirmar." + }, + "deleteProviderWarning": { + "message": "A exclusão do seu provedor é permanente. Não pode ser desfeita." + }, + "errorAssigningTargetCollection": { + "message": "Erro ao atribuir coleção de destino." + }, + "errorAssigningTargetFolder": { + "message": "Erro ao atribuir pasta de destino." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Crie uma nova organização de cliente para gerenciar como um Provedor. Posições adicionais serão refletidas no próximo ciclo de faturamento." + }, + "selectAPlan": { + "message": "Selecione um plano" + }, + "thirtyFivePercentDiscount": { + "message": "35% de Desconto" + }, + "monthPerMember": { + "message": "mês por membro" + }, + "seats": { + "message": "Lugares" + }, + "addOrganization": { + "message": "Adicionar Organização" + }, + "createdNewClient": { + "message": "Novo cliente criado com sucesso" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/pt_PT/messages.json b/apps/web/src/locales/pt_PT/messages.json index cf1ad10d9b..c2f2fbeee3 100644 --- a/apps/web/src/locales/pt_PT/messages.json +++ b/apps/web/src/locales/pt_PT/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Portal do fornecedor" }, + "success": { + "message": "Com sucesso" + }, "viewCollection": { "message": "Ver coleção" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Acesso à conta automática atualizado" }, - "unassignedItemsBanner": { - "message": "Aviso: Os itens da organização não atribuídos já não são visíveis na sua vista Todos os cofres em todos os dispositivos e agora só estã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." + "restrictedGroupAccessDesc": { + "message": "Não se pode adicionar a si próprio a um grupo." }, "unassignedItemsBannerSelfHost": { "message": "Aviso: A 2 de maio de 2024, os itens da organização não atribuídos deixarão de estar visíveis na vista Todos os cofres em todos os dispositivos 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." + }, + "unassignedItemsBannerNotice": { + "message": "Aviso: Os itens da organização não atribuídos já não são visíveis na vista Todos os cofres em todos os dispositivos e agora só estão acessíveis através da Consola de administração." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Aviso: A 16 de maio de 2024, os itens da organização não atribuídos deixarão de estar visíveis na vista Todos os cofres em todos os dispositivos e só estarão acessíveis através da Consola de administração." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Atribua estes itens a uma coleção a partir da", + "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": "para os tornar visíveis.", + "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." + }, + "deleteProvider": { + "message": "Eliminar fornecedor" + }, + "deleteProviderConfirmation": { + "message": "A eliminação de um fornecedor é permanente e irreversível. Introduza a sua palavra-passe mestra para confirmar a eliminação do fornecedor e de todos os dados associados." + }, + "deleteProviderName": { + "message": "Não é possível eliminar $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "É necessário desvincular todos os clientes antes de poder eliminar $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Fornecedor eliminado" + }, + "providerDeletedDesc": { + "message": "O fornecedor e todos os dados associados foram eliminados." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "Pediu para eliminar este fornecedor. Clique no botão abaixo para confirmar." + }, + "deleteProviderWarning": { + "message": "A eliminação do seu fornecedor é permanente. Não pode ser anulada." + }, + "errorAssigningTargetCollection": { + "message": "Erro ao atribuir a coleção de destino." + }, + "errorAssigningTargetFolder": { + "message": "Erro ao atribuir a pasta de destino." + }, + "integrationsAndSdks": { + "message": "Integrações e SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrações" + }, + "integrationsDesc": { + "message": "Sincronize automaticamente segredos do Gestor de Segredos do Bitwarden para um serviço de terceiros." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Utilize o SDK do Gestor de Segredos do Bitwarden nas seguintes linguagens de programação para criar as suas próprias aplicações." + }, + "setUpGithubActions": { + "message": "Configurar ações do Github" + }, + "setUpGitlabCICD": { + "message": "Configurar o GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Configurar o Ansible" + }, + "cSharpSDKRepo": { + "message": "Ver repositório de C#" + }, + "cPlusPlusSDKRepo": { + "message": "Ver repositório de C++" + }, + "jsWebAssemblySDKRepo": { + "message": "Ver repositório de JS WebAssembly" + }, + "javaSDKRepo": { + "message": "Ver repositório de Java" + }, + "pythonSDKRepo": { + "message": "Ver repositório de Python" + }, + "phpSDKRepo": { + "message": "Ver repositório de php" + }, + "rubySDKRepo": { + "message": "Ver repositório de Ruby" + }, + "goSDKRepo": { + "message": "Ver repositório de Go" + }, + "createNewClientToManageAsProvider": { + "message": "Crie uma nova organização de clientes para gerir como Fornecedor. Os lugares adicionais serão refletidos na próxima faturação." + }, + "selectAPlan": { + "message": "Selecionar um plano" + }, + "thirtyFivePercentDiscount": { + "message": "Desconto de 35%" + }, + "monthPerMember": { + "message": "mês por membro" + }, + "seats": { + "message": "Lugares" + }, + "addOrganization": { + "message": "Adicionar organização" + }, + "createdNewClient": { + "message": "Novo cliente criado com sucesso" + }, + "noAccess": { + "message": "Sem acesso" + }, + "collectionAdminConsoleManaged": { + "message": "Esta coleção só é acessível a partir da Consola de administração" } } diff --git a/apps/web/src/locales/ro/messages.json b/apps/web/src/locales/ro/messages.json index ba5badd908..1a47866ebb 100644 --- a/apps/web/src/locales/ro/messages.json +++ b/apps/web/src/locales/ro/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices 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." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/ru/messages.json b/apps/web/src/locales/ru/messages.json index eeb89acf55..85ba7d5785 100644 --- a/apps/web/src/locales/ru/messages.json +++ b/apps/web/src/locales/ru/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Портал провайдера" }, + "success": { + "message": "Успешно" + }, "viewCollection": { "message": "Посмотреть коллекцию" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Доступ аккаунта компьютера обновлен" }, - "unassignedItemsBanner": { - "message": "Обратите внимание: неприсвоенные элементы организации больше не видны в представлении \"Все хранилища\" на всех устройствах и теперь доступны только через консоль администратора. Назначьте эти элементы коллекции в консоли администратора, чтобы сделать их видимыми." + "restrictedGroupAccessDesc": { + "message": "Нельзя добавить самого себя в группу." }, "unassignedItemsBannerSelfHost": { "message": "Уведомление: 2 мая 2024 года неприсвоенные элементы организации больше не будут отображаться в представлении \"Все хранилища\" на всех устройствах и будут доступны только через консоль администратора. Назначьте эти элементы коллекции в консоли администратора, чтобы сделать их видимыми." + }, + "unassignedItemsBannerNotice": { + "message": "Уведомление: Неприсвоенные элементы организации больше не отображаются в представлении \"Все хранилища\" на всех устройствах и теперь доступны только через консоль администратора." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Уведомление: с 16 мая 2024 года неназначенные элементы организации больше не будут отображаться в представлении \"Все хранилища\" на всех устройствах и будут доступны только через консоль администратора." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Назначьте эти элементы в коллекцию из", + "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": "чтобы сделать их видимыми.", + "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." + }, + "deleteProvider": { + "message": "Удалить провайдера" + }, + "deleteProviderConfirmation": { + "message": "Удаление провайдера является постоянным и необратимым. Введите свой мастер-пароль, чтобы подтвердить удаление провайдера и всех связанных с ним данных." + }, + "deleteProviderName": { + "message": "Невозможно удалить $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "Перед удалением $ID$ необходимо отвязать всех клиентов", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Провайдер удален" + }, + "providerDeletedDesc": { + "message": "Провайдер и все связанные с ним данные были удалены." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "Вы запросили удаление этого провайдера. Воспользуйтесь кнопкой ниже для подтверждения." + }, + "deleteProviderWarning": { + "message": "Удаление вашего провайдера необратимо. Это нельзя отменить." + }, + "errorAssigningTargetCollection": { + "message": "Ошибка при назначении целевой коллекции." + }, + "errorAssigningTargetFolder": { + "message": "Ошибка при назначении целевой папки." + }, + "integrationsAndSdks": { + "message": "Интеграции и SDK", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Интеграции" + }, + "integrationsDesc": { + "message": "Автоматическая синхронизация секретов из Bitwarden Secrets Manager со сторонним сервисом." + }, + "sdks": { + "message": "SDK" + }, + "sdksDesc": { + "message": "Используйте SDK Bitwarden Secrets Manager на следующих языках программирования для создания собственных приложений." + }, + "setUpGithubActions": { + "message": "Настроить Github Actions" + }, + "setUpGitlabCICD": { + "message": "Настроить GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Настроить Ansible" + }, + "cSharpSDKRepo": { + "message": "Просмотр репозитория C#" + }, + "cPlusPlusSDKRepo": { + "message": "Просмотр репозитория C++" + }, + "jsWebAssemblySDKRepo": { + "message": "Просмотр репозитория JS WebAssembly" + }, + "javaSDKRepo": { + "message": "Просмотр репозитория Java" + }, + "pythonSDKRepo": { + "message": "Просмотр репозитория Python" + }, + "phpSDKRepo": { + "message": "Просмотр репозитория php" + }, + "rubySDKRepo": { + "message": "Просмотр репозитория Ruby" + }, + "goSDKRepo": { + "message": "Просмотр репозитория Go" + }, + "createNewClientToManageAsProvider": { + "message": "Создайте новую клиентскую организацию для управления ею в качестве провайдера. Дополнительные места будут отражены в следующем биллинговом цикле." + }, + "selectAPlan": { + "message": "Выберите план" + }, + "thirtyFivePercentDiscount": { + "message": "Скидка 35%" + }, + "monthPerMember": { + "message": "в месяц за пользователя" + }, + "seats": { + "message": "Места" + }, + "addOrganization": { + "message": "Добавить организацию" + }, + "createdNewClient": { + "message": "Новый клиент успешно создан" + }, + "noAccess": { + "message": "Нет доступа" + }, + "collectionAdminConsoleManaged": { + "message": "Эта коллекция доступна только из консоли администратора" } } diff --git a/apps/web/src/locales/si/messages.json b/apps/web/src/locales/si/messages.json index 0faa92c08d..e2c1e42427 100644 --- a/apps/web/src/locales/si/messages.json +++ b/apps/web/src/locales/si/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices 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." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/sk/messages.json b/apps/web/src/locales/sk/messages.json index 67f5abdc7d..234a4670b9 100644 --- a/apps/web/src/locales/sk/messages.json +++ b/apps/web/src/locales/sk/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Portál poskytovateľa" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "Pozrieť zbierku" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Upozornenie: Nepriradené položky organizácie už nie sú viditeľné v zobrazení Všetky trezory a sú prístupné iba cez Správcovskú konzolu." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Upozornenie: 16. mája 2024 nepriradené položky organizácie už nebudú viditeľné v zobrazení Všetky trezory a budú prístupné iba cez Správcovskú konzolu." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Priradiť tieto položky do zbierky zo", + "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": ", aby boli viditeľné.", + "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." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Chyba pri priraďovaní cieľovej kolekcie." + }, + "errorAssigningTargetFolder": { + "message": "Chyba pri priraďovaní cieľového priečinka." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/sl/messages.json b/apps/web/src/locales/sl/messages.json index 6a55868df2..c209ef05bd 100644 --- a/apps/web/src/locales/sl/messages.json +++ b/apps/web/src/locales/sl/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices 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." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/sr/messages.json b/apps/web/src/locales/sr/messages.json index 9683c587b7..a59b6a8855 100644 --- a/apps/web/src/locales/sr/messages.json +++ b/apps/web/src/locales/sr/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Портал провајдера" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "Преглед колекције" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Приступ налога машине ажуриран" }, - "unassignedItemsBanner": { - "message": "Напомена: Недодељене ставке организације више нису видљиве у вашем приказу Сви сефови на свим уређајима и сада су доступне само преко Админ конзоле. Доделите ове ставке колекцији са Админ конзолом да бисте их учинили видљивим." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { - "message": "Обавештење: 2. маја 2024. недодељене ставке организације више неће бити видљиве у вашем приказу Сви сефови на свим уређајима и биће им доступне само преко Админ конзоле. Доделите ове ставке колекцији са Админ конзолом да бисте их учинили видљивим." + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices 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." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/sr_CS/messages.json b/apps/web/src/locales/sr_CS/messages.json index 15770437ce..1ff89501f3 100644 --- a/apps/web/src/locales/sr_CS/messages.json +++ b/apps/web/src/locales/sr_CS/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices 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." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/sv/messages.json b/apps/web/src/locales/sv/messages.json index 57321d3376..389f1682a8 100644 --- a/apps/web/src/locales/sv/messages.json +++ b/apps/web/src/locales/sv/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "Visa samling" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "Du kan inte lägga till dig själv i en grupp." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices 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." + }, + "deleteProvider": { + "message": "Radera leverantör" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/te/messages.json b/apps/web/src/locales/te/messages.json index 781c9000cb..ed04c3a3ef 100644 --- a/apps/web/src/locales/te/messages.json +++ b/apps/web/src/locales/te/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices 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." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/th/messages.json b/apps/web/src/locales/th/messages.json index 1ecd45a9e4..ec114aad90 100644 --- a/apps/web/src/locales/th/messages.json +++ b/apps/web/src/locales/th/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices 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." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/tr/messages.json b/apps/web/src/locales/tr/messages.json index 65ef93eef7..9177321cb5 100644 --- a/apps/web/src/locales/tr/messages.json +++ b/apps/web/src/locales/tr/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Sağlayıcı Portalı" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices 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." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/uk/messages.json b/apps/web/src/locales/uk/messages.json index 1baaa5c67b..ecb31b6da0 100644 --- a/apps/web/src/locales/uk/messages.json +++ b/apps/web/src/locales/uk/messages.json @@ -7601,11 +7601,14 @@ "message": "Читати блог випусків" }, "adminConsole": { - "message": "Консоль адміністратора" + "message": "консолі адміністратора," }, "providerPortal": { "message": "Портал провайдера" }, + "success": { + "message": "Успішно" + }, "viewCollection": { "message": "Переглянути збірку" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Доступ до машинного облікового запису оновлено" }, - "unassignedItemsBanner": { - "message": "Увага: непризначені елементи організації більше не видимі у поданні \"Усі сховища\" на різних пристроях і тепер доступні лише в консолі адміністратора. Щоб зробити їх видимими, призначте ці елементи збірці в консолі адміністратора." + "restrictedGroupAccessDesc": { + "message": "Ви не можете додати себе до групи." }, "unassignedItemsBannerSelfHost": { "message": "Сповіщення: 2 травня 2024 року, непризначені елементи організації більше не будуть видимі на ваших пристроях у поданні \"Усі сховища\", і будуть доступні лише через консоль адміністратора. Щоб зробити їх видимими, призначте ці елементи збірці в консолі адміністратора." + }, + "unassignedItemsBannerNotice": { + "message": "Примітка: непризначені елементи організації більше не видимі на ваших пристроях у поданні \"Усі сховища\", і тепер доступні лише через консоль адміністратора." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Примітка: 16 травня 2024 року непризначені елементи організації більше не будуть видимі на ваших пристроях у поданні \"Усі сховища\", і будуть доступні лише через консоль адміністратора." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Призначте ці елементи збірці в", + "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": "щоб зробити їх видимими.", + "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." + }, + "deleteProvider": { + "message": "Видалити провайдера" + }, + "deleteProviderConfirmation": { + "message": "Видалення провайдера є остаточною і незворотною дією. Введіть свій головний пароль, щоб підтвердити видалення провайдера і всі пов'язані з ним дані." + }, + "deleteProviderName": { + "message": "Неможливо видалити $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "Перш ніж видалити $ID$, ви повинні від'єднати всіх клієнтів", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Провайдера видалено" + }, + "providerDeletedDesc": { + "message": "Провайдера і всі пов'язані дані було видалено." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "Ви відправили запит на видалення цього провайдера. Натисніть кнопку внизу для підтвердження." + }, + "deleteProviderWarning": { + "message": "Видалення провайдера є незворотною дією. Це не можна буде скасувати." + }, + "errorAssigningTargetCollection": { + "message": "Помилка призначення цільової збірки." + }, + "errorAssigningTargetFolder": { + "message": "Помилка призначення цільової теки." + }, + "integrationsAndSdks": { + "message": "Інтеграції та SDK", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Інтеграції" + }, + "integrationsDesc": { + "message": "Автоматична синхронізація секретів між менеджером секретів Bitwarden і стороннім сервісом." + }, + "sdks": { + "message": "SDK" + }, + "sdksDesc": { + "message": "Використовуйте SDK менеджера секретів Bitwarden із зазначеними мовами програмування для створення власних програм." + }, + "setUpGithubActions": { + "message": "Налаштувати дії для Github" + }, + "setUpGitlabCICD": { + "message": "Налаштувати GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Налаштувати Ansible" + }, + "cSharpSDKRepo": { + "message": "Перегляд репозиторію C#" + }, + "cPlusPlusSDKRepo": { + "message": "Перегляд репозиторію C++" + }, + "jsWebAssemblySDKRepo": { + "message": "Перегляд репозиторію JS WebAssembly" + }, + "javaSDKRepo": { + "message": "Перегляд репозиторію Java" + }, + "pythonSDKRepo": { + "message": "Перегляд репозиторію Python" + }, + "phpSDKRepo": { + "message": "Перегляд репозиторію php" + }, + "rubySDKRepo": { + "message": "Перегляд репозиторію Ruby" + }, + "goSDKRepo": { + "message": "Перегляд репозиторію Go" + }, + "createNewClientToManageAsProvider": { + "message": "Створіть нову організацію клієнта, щоб керувати нею як провайдер. Додаткові місця будуть відображені в наступному платіжному циклі." + }, + "selectAPlan": { + "message": "Оберіть тарифний план" + }, + "thirtyFivePercentDiscount": { + "message": "Знижка 35%" + }, + "monthPerMember": { + "message": "на місяць за учасника" + }, + "seats": { + "message": "Місця" + }, + "addOrganization": { + "message": "Додати організацію" + }, + "createdNewClient": { + "message": "Нового клієнта успішно створено" + }, + "noAccess": { + "message": "Немає доступу" + }, + "collectionAdminConsoleManaged": { + "message": "Ця збірка доступна тільки з консолі адміністратора" } } diff --git a/apps/web/src/locales/vi/messages.json b/apps/web/src/locales/vi/messages.json index ffbd72ce9d..0696e226e0 100644 --- a/apps/web/src/locales/vi/messages.json +++ b/apps/web/src/locales/vi/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices 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." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/zh_CN/messages.json b/apps/web/src/locales/zh_CN/messages.json index e584785a5a..f6093d9bc7 100644 --- a/apps/web/src/locales/zh_CN/messages.json +++ b/apps/web/src/locales/zh_CN/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "提供商门户" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "查看集合" }, @@ -7698,7 +7701,7 @@ "description": "The date header used when a subscription is past due." }, "pastDueWarningForChargeAutomatically": { - "message": "从您的订阅到期之日起,您有 $DAYS$ 天的宽限期来保留您的订购。请在 $SUSPENSION_DATE$ 之前处理逾期未支付的账单。", + "message": "从您的订阅到期之日起,您有 $DAYS$ 天的宽限期来保留您的订阅。请在 $SUSPENSION_DATE$ 之前处理逾期未支付的账单。", "placeholders": { "days": { "content": "$1", @@ -7712,7 +7715,7 @@ "description": "A warning shown to the user when their subscription is past due and they are charged automatically." }, "pastDueWarningForSendInvoice": { - "message": "从第一笔未支付的账单到期之日起,您有 $DAYS$ 天的宽限期来保留您的订购。请在 $SUSPENSION_DATE$ 之前处理逾期未支付的账单。", + "message": "从第一笔未支付的账单到期之日起,您有 $DAYS$ 天的宽限期来保留您的订阅。请在 $SUSPENSION_DATE$ 之前处理逾期未支付的账单。", "placeholders": { "days": { "content": "$1", @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "机器账户访问权限已更新" }, - "unassignedItemsBanner": { - "message": "注意:未分配的组织项目在您所有设备的「所有密码库」视图中不再可见,现在只能通过管理控制台访问。通过管理控制台将这些项目分配给集合以使其可见。" + "restrictedGroupAccessDesc": { + "message": "您不能将自己添加到群组。" }, "unassignedItemsBannerSelfHost": { - "message": "注意:从 2024 年 5 月 2 日起,未分配的组织项目在您所有设备的「所有密码库」视图中将不再可见,只能通过管理控制台访问。通过管理控制台将这些项目分配给集合以使其可见。" + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices 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." + }, + "deleteProvider": { + "message": "删除提供商" + }, + "deleteProviderConfirmation": { + "message": "删除提供商是永久性操作,无法撤销!输入您的主密码以确认删除提供商及所有关联的数据。" + }, + "deleteProviderName": { + "message": "无法删除 $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "提供商已删除" + }, + "providerDeletedDesc": { + "message": "提供商和所有关联数据已被删除。" + }, + "deleteProviderRecoverConfirmDesc": { + "message": "您已请求删除此提供商。请使用下面的按钮确认。" + }, + "deleteProviderWarning": { + "message": "删除您的提供商是永久性操作,无法撤销!" + }, + "errorAssigningTargetCollection": { + "message": "分配目标集合时出错。" + }, + "errorAssigningTargetFolder": { + "message": "分配目标文件夹时出错。" + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } diff --git a/apps/web/src/locales/zh_TW/messages.json b/apps/web/src/locales/zh_TW/messages.json index 89bb26cacd..1bc56d2157 100644 --- a/apps/web/src/locales/zh_TW/messages.json +++ b/apps/web/src/locales/zh_TW/messages.json @@ -7606,6 +7606,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7903,142 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices 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." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDesc": { + "message": "You must unlink all clients before you can delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" } } From 2916fc7404c18a6ca3914b1546c63d0b6859c8b4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 19 Apr 2024 10:42:33 +0000 Subject: [PATCH 225/351] Autosync the updated translations (#8825) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/desktop/src/locales/af/messages.json | 22 ++++++- apps/desktop/src/locales/ar/messages.json | 22 ++++++- apps/desktop/src/locales/az/messages.json | 66 +++++++++++++------- apps/desktop/src/locales/be/messages.json | 22 ++++++- apps/desktop/src/locales/bg/messages.json | 22 ++++++- apps/desktop/src/locales/bn/messages.json | 22 ++++++- apps/desktop/src/locales/bs/messages.json | 22 ++++++- apps/desktop/src/locales/ca/messages.json | 22 ++++++- apps/desktop/src/locales/cs/messages.json | 22 ++++++- apps/desktop/src/locales/cy/messages.json | 22 ++++++- apps/desktop/src/locales/da/messages.json | 22 ++++++- apps/desktop/src/locales/de/messages.json | 22 ++++++- apps/desktop/src/locales/el/messages.json | 22 ++++++- apps/desktop/src/locales/en_GB/messages.json | 22 ++++++- apps/desktop/src/locales/en_IN/messages.json | 22 ++++++- apps/desktop/src/locales/eo/messages.json | 22 ++++++- apps/desktop/src/locales/es/messages.json | 22 ++++++- apps/desktop/src/locales/et/messages.json | 22 ++++++- apps/desktop/src/locales/eu/messages.json | 22 ++++++- apps/desktop/src/locales/fa/messages.json | 22 ++++++- apps/desktop/src/locales/fi/messages.json | 22 ++++++- apps/desktop/src/locales/fil/messages.json | 22 ++++++- apps/desktop/src/locales/fr/messages.json | 22 ++++++- apps/desktop/src/locales/gl/messages.json | 22 ++++++- apps/desktop/src/locales/he/messages.json | 22 ++++++- apps/desktop/src/locales/hi/messages.json | 22 ++++++- apps/desktop/src/locales/hr/messages.json | 22 ++++++- apps/desktop/src/locales/hu/messages.json | 22 ++++++- apps/desktop/src/locales/id/messages.json | 22 ++++++- apps/desktop/src/locales/it/messages.json | 22 ++++++- apps/desktop/src/locales/ja/messages.json | 22 ++++++- apps/desktop/src/locales/ka/messages.json | 22 ++++++- apps/desktop/src/locales/km/messages.json | 22 ++++++- apps/desktop/src/locales/kn/messages.json | 22 ++++++- apps/desktop/src/locales/ko/messages.json | 22 ++++++- apps/desktop/src/locales/lt/messages.json | 22 ++++++- apps/desktop/src/locales/lv/messages.json | 22 ++++++- apps/desktop/src/locales/me/messages.json | 22 ++++++- apps/desktop/src/locales/ml/messages.json | 22 ++++++- apps/desktop/src/locales/mr/messages.json | 22 ++++++- apps/desktop/src/locales/my/messages.json | 22 ++++++- apps/desktop/src/locales/nb/messages.json | 22 ++++++- apps/desktop/src/locales/ne/messages.json | 22 ++++++- apps/desktop/src/locales/nl/messages.json | 22 ++++++- apps/desktop/src/locales/nn/messages.json | 22 ++++++- apps/desktop/src/locales/or/messages.json | 22 ++++++- apps/desktop/src/locales/pl/messages.json | 22 ++++++- apps/desktop/src/locales/pt_BR/messages.json | 22 ++++++- apps/desktop/src/locales/pt_PT/messages.json | 22 ++++++- apps/desktop/src/locales/ro/messages.json | 22 ++++++- apps/desktop/src/locales/ru/messages.json | 22 ++++++- apps/desktop/src/locales/si/messages.json | 22 ++++++- apps/desktop/src/locales/sk/messages.json | 24 ++++++- apps/desktop/src/locales/sl/messages.json | 22 ++++++- apps/desktop/src/locales/sr/messages.json | 22 ++++++- apps/desktop/src/locales/sv/messages.json | 22 ++++++- apps/desktop/src/locales/te/messages.json | 22 ++++++- apps/desktop/src/locales/th/messages.json | 22 ++++++- apps/desktop/src/locales/tr/messages.json | 22 ++++++- apps/desktop/src/locales/uk/messages.json | 22 ++++++- apps/desktop/src/locales/vi/messages.json | 22 ++++++- apps/desktop/src/locales/zh_CN/messages.json | 22 ++++++- apps/desktop/src/locales/zh_TW/messages.json | 22 ++++++- 63 files changed, 1283 insertions(+), 149 deletions(-) diff --git a/apps/desktop/src/locales/af/messages.json b/apps/desktop/src/locales/af/messages.json index ee32c045c9..97067b788a 100644 --- a/apps/desktop/src/locales/af/messages.json +++ b/apps/desktop/src/locales/af/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Verander Hoofwagwoord" }, - "changeMasterPasswordConfirmation": { - "message": "U kan u hoofwagwoord op die bitwarden.com-webkluis verander. Wil u die webwerf nou besoek?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Vingerafdrukfrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Blaaierintegrasie word nie ondersteun nie" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Ongelukkig word blaaierintegrasie tans slegs in die weergawe vir die Mac-toepwinkel ondersteun." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/ar/messages.json b/apps/desktop/src/locales/ar/messages.json index 104a9f7780..2d25269fff 100644 --- a/apps/desktop/src/locales/ar/messages.json +++ b/apps/desktop/src/locales/ar/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "تغيير كلمة المرور الرئيسية" }, - "changeMasterPasswordConfirmation": { - "message": "يمكنك تغيير كلمة المرور الرئيسية من خزنة الويب في bitwarden.com. هل تريد زيارة الموقع الآن؟" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "عبارة بصمة الإصبع", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "تكامل المتصفح غير مدعوم" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "للأسف، لا يتم دعم تكامل المتصفح إلا في إصدار متجر تطبيقات ماك في الوقت الحالي." }, @@ -2688,6 +2697,9 @@ "message": "تنسيقات مشتركة", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/az/messages.json b/apps/desktop/src/locales/az/messages.json index f404c7f95a..1ecd18eee7 100644 --- a/apps/desktop/src/locales/az/messages.json +++ b/apps/desktop/src/locales/az/messages.json @@ -717,7 +717,7 @@ "message": "Bildiriş server URL-si" }, "iconsUrl": { - "message": "Nişan server URL-si" + "message": "İkon server URL-si" }, "environmentSaved": { "message": "Mühit URL-ləri saxlanıldı." @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Ana parolu dəyişdir" }, - "changeMasterPasswordConfirmation": { - "message": "Ana parolunuzu bitwarden.com veb anbarında dəyişdirə bilərsiniz. İndi saytı ziyarət etmək istəyirsiniz?" + "continueToWebApp": { + "message": "Veb tətbiqlə davam edilsin?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Ana parolunuzu Bitwarden veb tətbiqində dəyişdirə bilərsiniz." }, "fingerprintPhrase": { "message": "Barmaq izi ifadəsi", @@ -920,52 +923,52 @@ "description": "Clipboard is the operating system thing where you copy/paste data to on your device." }, "enableFavicon": { - "message": "Veb sayt nişanlarını göstər" + "message": "Veb sayt ikonlarını göstər" }, "faviconDesc": { "message": "Hər girişin yanında tanına bilən bir təsvir göstər." }, "enableMinToTray": { - "message": "Bildiriş nişanına kiçildin" + "message": "Bildiriş sahəsi ikonuna kiçilt" }, "enableMinToTrayDesc": { - "message": "Pəncərə kiçildiləndə, bildiriş sahəsində bir nişan göstər." + "message": "Pəncərə kiçildiləndə, bunun əvəzinə bildiriş sahəsində bir ikon göstər." }, "enableMinToMenuBar": { - "message": "Menyu sətrinə kiçilt" + "message": "Menyu çubuğuna kiçilt" }, "enableMinToMenuBarDesc": { - "message": "Pəncərəni kiçildəndə, menyu sətrində bir nişan göstər." + "message": "Pəncərəni kiçildəndə, bunun əvəzinə menyu çubuğunda bir ikon göstər." }, "enableCloseToTray": { - "message": "Bildiriş nişanına bağla" + "message": "Bildiriş ikonu üçün bağla" }, "enableCloseToTrayDesc": { - "message": "Pəncərə bağlananda, bildiriş sahəsində bir nişan göstər." + "message": "Pəncərə bağladılanda, bunun əvəzinə bildiriş sahəsində bir ikon göstər." }, "enableCloseToMenuBar": { - "message": "Menyu sətrini bağla" + "message": "Menyu çubuğunda bağla" }, "enableCloseToMenuBarDesc": { - "message": "Pəncərəni bağlananda, menyu sətrində bir nişan göstər." + "message": "Pəncərəni bağladılanda, bunun əvəzinə menyu çubuğunda bir ikon göstər." }, "enableTray": { - "message": "Bildiriş sahəsi nişanını fəallaşdır" + "message": "Bildiriş sahəsi ikonunu göstər" }, "enableTrayDesc": { - "message": "Bildiriş sahəsində həmişə bir nişan göstər." + "message": "Bildiriş sahəsində həmişə bir ikon göstər." }, "startToTray": { - "message": "Bildiriş sahəsi nişanı kimi başlat" + "message": "Bildiriş sahəsi ikonu kimi başlat" }, "startToTrayDesc": { - "message": "Tətbiq ilk başladılanda, yalnız bildiriş sahəsi nişanı görünsün." + "message": "Tətbiq ilk başladılanda, sistem bildiriş sahəsində yalnız ikon olaraq görünsün." }, "startToMenuBar": { - "message": "Menyu sətrini başlat" + "message": "Menyu çubuğunda başlat" }, "startToMenuBarDesc": { - "message": "Tətbiq ilk başladılanda, menyu sətri sadəcə nişan kimi görünsün." + "message": "Tətbiq ilk başladılanda, menyu çubuğunda yalnız ikon olaraq görünsün." }, "openAtLogin": { "message": "Giriş ediləndə avtomatik başlat" @@ -977,7 +980,7 @@ "message": "\"Dock\"da həmişə göstər" }, "alwaysShowDockDesc": { - "message": "Menyu sətrinə kiçildiləndə belə Bitwarden nişanını \"Dock\"da göstər." + "message": "Menyu çubuğuna kiçildiləndə belə Bitwarden ikonunu Yuvada göstər." }, "confirmTrayTitle": { "message": "Bildiriş sahəsi nişanını ləğv et" @@ -1450,16 +1453,16 @@ "message": "Hesabınız bağlandı və bütün əlaqəli datalar silindi." }, "preferences": { - "message": "Tercihlər" + "message": "Tərcihlər" }, "enableMenuBar": { - "message": "Menyu sətri nişanını fəallaşdır" + "message": "Menyu çubuğu ikonunu göstər" }, "enableMenuBarDesc": { - "message": "Menyu sətrində həmişə bir nişan göstər." + "message": "Menyu çubuğunda həmişə bir ikon göstər." }, "hideToMenuBar": { - "message": "Menyu sətrini gizlət" + "message": "Menyu çubuğunda gizlət" }, "selectOneCollection": { "message": "Ən azı bir kolleksiya seçməlisiniz." @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Brauzer inteqrasiyası dəstəklənmir" }, + "browserIntegrationErrorTitle": { + "message": "Brauzer inteqrasiyasını fəallaşdırma xətası" + }, + "browserIntegrationErrorDesc": { + "message": "Brauzer inteqrasiyasını fəallaşdırarkən bir xəta baş verdi." + }, "browserIntegrationMasOnlyDesc": { "message": "Təəssüf ki, brauzer inteqrasiyası indilik yalnız Mac App Store versiyasında dəstəklənir." }, @@ -2021,7 +2030,7 @@ "message": "Eyni vaxtda 5-dən çox hesaba giriş edilə bilməz." }, "accountPreferences": { - "message": "Tercihlər" + "message": "Tərcihlər" }, "appPreferences": { "message": "Tətbiq ayarları (bütün hesablar)" @@ -2688,6 +2697,9 @@ "message": "Ortaq formatlar", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Uğurlu" + }, "troubleshooting": { "message": "Problemlərin aradan qaldırılması" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Parol silindi" + }, + "errorAssigningTargetCollection": { + "message": "Hədəf kolleksiyaya təyin etmə xətası." + }, + "errorAssigningTargetFolder": { + "message": "Hədəf qovluğa təyin etmə xətası." } } diff --git a/apps/desktop/src/locales/be/messages.json b/apps/desktop/src/locales/be/messages.json index 2bc33f2b28..e0133e5a74 100644 --- a/apps/desktop/src/locales/be/messages.json +++ b/apps/desktop/src/locales/be/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Змяніць асноўны пароль" }, - "changeMasterPasswordConfirmation": { - "message": "Вы можаце змяніць свой асноўны пароль у вэб-сховішчы на bitwarden.com. Перайсці на вэб-сайт зараз?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Фраза адбітка пальца", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Інтэграцыя з браўзерам не падтрымліваецца" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "На жаль, інтэграцыя з браўзерам зараз падтрымліваецца толькі ў версіі для Mac App Store." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/bg/messages.json b/apps/desktop/src/locales/bg/messages.json index 9f6d5bdd36..20f47d4bcd 100644 --- a/apps/desktop/src/locales/bg/messages.json +++ b/apps/desktop/src/locales/bg/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Промяна на главната парола" }, - "changeMasterPasswordConfirmation": { - "message": "Главната парола на трезор може да се промени чрез сайта bitwarden.com. Искате ли да го посетите?" + "continueToWebApp": { + "message": "Продължаване към уеб приложението?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Може да промените главната си парола в уеб приложението на Битуорден." }, "fingerprintPhrase": { "message": "Уникална фраза", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Интеграцията с браузър не се поддържа" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "За жалост в момента интеграцията с браузър не се поддържа във версията за магазина на Mac." }, @@ -2688,6 +2697,9 @@ "message": "Често използвани формати", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Отстраняване на проблеми" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Секретният ключ е премахнат" + }, + "errorAssigningTargetCollection": { + "message": "Грешка при задаването на целева колекция." + }, + "errorAssigningTargetFolder": { + "message": "Грешка при задаването на целева папка." } } diff --git a/apps/desktop/src/locales/bn/messages.json b/apps/desktop/src/locales/bn/messages.json index 22893aadab..626734ebff 100644 --- a/apps/desktop/src/locales/bn/messages.json +++ b/apps/desktop/src/locales/bn/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "মূল পাসওয়ার্ড পরিবর্তন" }, - "changeMasterPasswordConfirmation": { - "message": "আপনি bitwarden.com ওয়েব ভল্ট থেকে মূল পাসওয়ার্ডটি পরিবর্তন করতে পারেন। আপনি কি এখনই ওয়েবসাইটটি দেখতে চান?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "ফিঙ্গারপ্রিন্ট ফ্রেজ", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/bs/messages.json b/apps/desktop/src/locales/bs/messages.json index 6adb5bbce3..9d5685cca9 100644 --- a/apps/desktop/src/locales/bs/messages.json +++ b/apps/desktop/src/locales/bs/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Promijenite glavnu lozinku" }, - "changeMasterPasswordConfirmation": { - "message": "Možete da promjenite svoju glavnu lozinku na bitwarden.com web trezoru. Da li želite da posjetite web stranicu sada?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Jedinstvena fraza", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Nažalost, za sada je integracija sa preglednikom podržana samo u Mac App Store verziji aplikacije." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/ca/messages.json b/apps/desktop/src/locales/ca/messages.json index c76111b53c..d8c0f32948 100644 --- a/apps/desktop/src/locales/ca/messages.json +++ b/apps/desktop/src/locales/ca/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Canvia la contrasenya mestra" }, - "changeMasterPasswordConfirmation": { - "message": "Podeu canviar la contrasenya mestra a la caixa forta web de bitwarden.com. Voleu visitar el lloc web ara?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Frase d'empremta digital", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "La integració en el navegador no és compatible" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Malauradament, la integració del navegador només és compatible amb la versió de Mac App Store." }, @@ -2688,6 +2697,9 @@ "message": "Formats comuns", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Resolució de problemes" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Clau de pas suprimida" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/cs/messages.json b/apps/desktop/src/locales/cs/messages.json index e7ba56e81c..e68fe8fffc 100644 --- a/apps/desktop/src/locales/cs/messages.json +++ b/apps/desktop/src/locales/cs/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Změnit hlavní heslo" }, - "changeMasterPasswordConfirmation": { - "message": "Hlavní heslo si můžete změnit na webové stránce bitwarden.com. Chcete tuto stránku nyní otevřít?" + "continueToWebApp": { + "message": "Pokračovat do webové aplikace?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Hlavní heslo můžete změnit ve webové aplikaci Bitwardenu." }, "fingerprintPhrase": { "message": "Fráze otisku prstu", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Integrace prohlížeče není podporována" }, + "browserIntegrationErrorTitle": { + "message": "Chyba při povolování integrace prohlížeče" + }, + "browserIntegrationErrorDesc": { + "message": "Vyskytla se chyba při povolování integrace prohlížeče." + }, "browserIntegrationMasOnlyDesc": { "message": "Integrace prohlížeče je podporována jen ve verzi pro Mac App Store." }, @@ -2688,6 +2697,9 @@ "message": "Společné formáty", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Úspěch" + }, "troubleshooting": { "message": "Řešení problémů" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Přístupový klíč byl odebrán" + }, + "errorAssigningTargetCollection": { + "message": "Chyba při přiřazování cílové kolekce." + }, + "errorAssigningTargetFolder": { + "message": "Chyba při přiřazování cílové složky." } } diff --git a/apps/desktop/src/locales/cy/messages.json b/apps/desktop/src/locales/cy/messages.json index e87b805d0b..62f2e608bb 100644 --- a/apps/desktop/src/locales/cy/messages.json +++ b/apps/desktop/src/locales/cy/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", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/da/messages.json b/apps/desktop/src/locales/da/messages.json index 1f994cf8eb..0e578a6f66 100644 --- a/apps/desktop/src/locales/da/messages.json +++ b/apps/desktop/src/locales/da/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Skift hovedadgangskode" }, - "changeMasterPasswordConfirmation": { - "message": "Man kan ændre sin hovedadgangskode via bitwarden.com web-boksen. Besøg webstedet nu?" + "continueToWebApp": { + "message": "Fortsæt til web-app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Hovedadgangskoden kan ændres via Bitwarden web-appen." }, "fingerprintPhrase": { "message": "Fingeraftrykssætning", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browserintegration understøttes ikke" }, + "browserIntegrationErrorTitle": { + "message": "Fejl ved aktivering af webbrowserintegration" + }, + "browserIntegrationErrorDesc": { + "message": "En fejl opstod under aktivering af webbrowserintegration." + }, "browserIntegrationMasOnlyDesc": { "message": "Desværre understøttes browserintegration indtil videre kun i Mac App Store-versionen." }, @@ -2688,6 +2697,9 @@ "message": "Almindelige formater", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Gennemført" + }, "troubleshooting": { "message": "Fejlfinding" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Adgangsnøgle fjernet" + }, + "errorAssigningTargetCollection": { + "message": "Fejl ved tildeling af målsamling." + }, + "errorAssigningTargetFolder": { + "message": "Fejl ved tildeling af målmappe." } } diff --git a/apps/desktop/src/locales/de/messages.json b/apps/desktop/src/locales/de/messages.json index 428cfd6a27..bba1ccec15 100644 --- a/apps/desktop/src/locales/de/messages.json +++ b/apps/desktop/src/locales/de/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Master-Passwort ändern" }, - "changeMasterPasswordConfirmation": { - "message": "Du kannst dein Master-Passwort im bitwarden.com-Web-Tresor ändern. Möchtest du die Seite jetzt öffnen?" + "continueToWebApp": { + "message": "Weiter zur Web-App?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Du kannst dein Master-Passwort in der Bitwarden Web-App ändern." }, "fingerprintPhrase": { "message": "Fingerabdruck-Phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser-Integration wird nicht unterstützt" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Leider wird die Browser-Integration derzeit nur in der Mac App Store Version unterstützt." }, @@ -2688,6 +2697,9 @@ "message": "Gängigste Formate", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Problembehandlung" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey entfernt" + }, + "errorAssigningTargetCollection": { + "message": "Fehler beim Zuweisen der Ziel-Sammlung." + }, + "errorAssigningTargetFolder": { + "message": "Fehler beim Zuweisen des Ziel-Ordners." } } diff --git a/apps/desktop/src/locales/el/messages.json b/apps/desktop/src/locales/el/messages.json index 63b1f21c2e..87360c33ce 100644 --- a/apps/desktop/src/locales/el/messages.json +++ b/apps/desktop/src/locales/el/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Αλλαγή Κύριου Κωδικού" }, - "changeMasterPasswordConfirmation": { - "message": "Μπορείτε να αλλάξετε τον κύριο κωδικό στο bitwarden.com. Θέλετε να επισκεφθείτε την ιστοσελίδα τώρα;" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Φράση Δακτυλικών Αποτυπωμάτων", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Η ενσωμάτωση του περιηγητή δεν υποστηρίζεται" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Δυστυχώς η ενσωμάτωση του προγράμματος περιήγησης υποστηρίζεται μόνο στην έκδοση Mac App Store για τώρα." }, @@ -2688,6 +2697,9 @@ "message": "Κοινές μορφές", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Αντιμετώπιση Προβλημάτων" }, @@ -2702,5 +2714,11 @@ }, "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 53958bca57..5c8c32b7c1 100644 --- a/apps/desktop/src/locales/en_GB/messages.json +++ b/apps/desktop/src/locales/en_GB/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", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/en_IN/messages.json b/apps/desktop/src/locales/en_IN/messages.json index f6011c301f..abfa0b1c0d 100644 --- a/apps/desktop/src/locales/en_IN/messages.json +++ b/apps/desktop/src/locales/en_IN/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", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/eo/messages.json b/apps/desktop/src/locales/eo/messages.json index 772eb70985..427f08f805 100644 --- a/apps/desktop/src/locales/eo/messages.json +++ b/apps/desktop/src/locales/eo/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", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/es/messages.json b/apps/desktop/src/locales/es/messages.json index e3dcd0dc4c..f7df93bdd7 100644 --- a/apps/desktop/src/locales/es/messages.json +++ b/apps/desktop/src/locales/es/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Cambiar contraseña maestra" }, - "changeMasterPasswordConfirmation": { - "message": "Puedes cambiar tu contraseña maestra en la caja fuerte web de bitwarden.com. ¿Quieres visitar ahora el sitio web?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Frase de huella digital", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "La integración con el navegador no está soportada" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Por desgracia la integración del navegador sólo está soportada por ahora en la versión de la Mac App Store." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/et/messages.json b/apps/desktop/src/locales/et/messages.json index 2b54df2a91..02cd737baa 100644 --- a/apps/desktop/src/locales/et/messages.json +++ b/apps/desktop/src/locales/et/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Muuda ülemparooli" }, - "changeMasterPasswordConfirmation": { - "message": "Saad oma ülemparooli muuta bitwarden.com veebihoidlas. Soovid seda kohe teha?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Sõrmejälje fraas", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Brauseri integratsioon ei ole toetatud" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Paraku on brauseri integratsioon hetkel toetatud ainult Mac App Store'i versioonis." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/eu/messages.json b/apps/desktop/src/locales/eu/messages.json index d66d5265e1..2067b2dcc2 100644 --- a/apps/desktop/src/locales/eu/messages.json +++ b/apps/desktop/src/locales/eu/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Aldatu pasahitz nagusia" }, - "changeMasterPasswordConfirmation": { - "message": "Zure pasahitz nagusia alda dezakezu bitwarden.com webgunean. Orain joan nahi duzu webgunera?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Hatz-marka digitalaren esaldia", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Ez da nabigatzailearen integrazioa onartzen" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Zoritxarrez, Mac App Storeren bertsioan soilik onartzen da oraingoz nabigatzailearen integrazioa." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/fa/messages.json b/apps/desktop/src/locales/fa/messages.json index c62bb99b2d..ef34f8222a 100644 --- a/apps/desktop/src/locales/fa/messages.json +++ b/apps/desktop/src/locales/fa/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "تغییر کلمه عبور اصلی" }, - "changeMasterPasswordConfirmation": { - "message": "شما می‌توانید کلمه عبور اصلی خود را در bitwarden.com تغییر دهید. آیا می‌خواهید از سایت بازدید کنید؟" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "عبارت اثر انگشت", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "ادغام مرورگر پشتیبانی نمی‌شود" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "متأسفانه در حال حاضر ادغام مرورگر فقط در نسخه Mac App Store پشتیبانی می‌شود." }, @@ -2688,6 +2697,9 @@ "message": "فرمت‌های رایج", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/fi/messages.json b/apps/desktop/src/locales/fi/messages.json index f74136aedc..03059f7ef3 100644 --- a/apps/desktop/src/locales/fi/messages.json +++ b/apps/desktop/src/locales/fi/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Vaihda pääsalasana" }, - "changeMasterPasswordConfirmation": { - "message": "Voit vaihtaa pääsalasanasi bitwarden.com-verkkoholvissa. Haluatko käydä sivustolla nyt?" + "continueToWebApp": { + "message": "Avataanko verkkosovellus?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Voit vaihtaa pääsalasanasi Bitwardenin verkkosovelluksessa." }, "fingerprintPhrase": { "message": "Tunnistelauseke", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Selainintegraatiota ei tueta" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Valitettavasti selainintegraatiota tuetaan toistaiseksi vain Mac App Store -versiossa." }, @@ -2688,6 +2697,9 @@ "message": "Yleiset muodot", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Vianetsintä" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Suojausavain poistettiin" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/fil/messages.json b/apps/desktop/src/locales/fil/messages.json index 170559fc64..d28a4b568c 100644 --- a/apps/desktop/src/locales/fil/messages.json +++ b/apps/desktop/src/locales/fil/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Palitan ang master password" }, - "changeMasterPasswordConfirmation": { - "message": "Maaari mong palitan ang iyong master password sa bitwarden.com web vault. Gusto mo bang bisitahin ang website ngayon?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Hulmabig ng Hilik ng Dako", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Hindi suportado ang pagsasama ng browser" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Sa kasamaang palad ang pagsasama ng browser ay suportado lamang sa bersyon ng Mac App Store para sa ngayon." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/fr/messages.json b/apps/desktop/src/locales/fr/messages.json index 20353d2d86..86550b736f 100644 --- a/apps/desktop/src/locales/fr/messages.json +++ b/apps/desktop/src/locales/fr/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Changer le mot de passe principal" }, - "changeMasterPasswordConfirmation": { - "message": "Vous pouvez changer votre mot de passe principal depuis le coffre web de bitwarden.com. Voulez-vous visiter le site web maintenant ?" + "continueToWebApp": { + "message": "Poursuivre vers l'application web ?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Vous pouvez modifier votre mot de passe principal sur l'application web de Bitwarden." }, "fingerprintPhrase": { "message": "Phrase d'empreinte", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Intégration dans le navigateur non supportée" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Malheureusement l'intégration avec le navigateur est uniquement supportée dans la version Mac App Store pour le moment." }, @@ -2688,6 +2697,9 @@ "message": "Formats communs", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Résolution de problèmes" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Clé d'identification (passkey) retirée" + }, + "errorAssigningTargetCollection": { + "message": "Erreur lors de l'assignation de la collection cible." + }, + "errorAssigningTargetFolder": { + "message": "Erreur lors de l'assignation du dossier cible." } } diff --git a/apps/desktop/src/locales/gl/messages.json b/apps/desktop/src/locales/gl/messages.json index f96260c005..889a2beeee 100644 --- a/apps/desktop/src/locales/gl/messages.json +++ b/apps/desktop/src/locales/gl/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", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/he/messages.json b/apps/desktop/src/locales/he/messages.json index cc5a0f011d..3b155ffdf3 100644 --- a/apps/desktop/src/locales/he/messages.json +++ b/apps/desktop/src/locales/he/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "החלף סיסמה ראשית" }, - "changeMasterPasswordConfirmation": { - "message": "באפשרותך לשנות את הסיסמה הראשית שלך דרך הכספת באתר bitwarden.com. האם ברצונך לפתוח את האתר כעת?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "סיסמת טביעת אצבע", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "שילוב הדפדפן אינו נתמך" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "למצער, אינטגרצייה עם הדפדפן בשלב זה נתמכת רק על ידי גרסת חנות האפליקציות של מקינטוש." }, @@ -2688,6 +2697,9 @@ "message": "תסדירים נפוצים", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/hi/messages.json b/apps/desktop/src/locales/hi/messages.json index 57ef1d32ef..af28c66681 100644 --- a/apps/desktop/src/locales/hi/messages.json +++ b/apps/desktop/src/locales/hi/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", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/hr/messages.json b/apps/desktop/src/locales/hr/messages.json index 1e501cee78..01983d5891 100644 --- a/apps/desktop/src/locales/hr/messages.json +++ b/apps/desktop/src/locales/hr/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Promjeni glavnu lozinku" }, - "changeMasterPasswordConfirmation": { - "message": "Svoju glavnu lozinku možeš promijeniti na web trezoru. Želiš li sada posjetiti bitwarden.com?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Jedinstvena fraza", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Integracija preglednika nije podržana" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Nažalost, za sada je integracija s preglednikom podržana samo u Mac App Store verziji aplikacije." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/hu/messages.json b/apps/desktop/src/locales/hu/messages.json index 0b443c9a6b..ecf77e2f34 100644 --- a/apps/desktop/src/locales/hu/messages.json +++ b/apps/desktop/src/locales/hu/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Mesterjelszó módosítása" }, - "changeMasterPasswordConfirmation": { - "message": "A mesterjelszó megváltoztatható a bitwarden.com webes széfben. Szeretnénk felkeresni a webhelyet mos?" + "continueToWebApp": { + "message": "Tovább a webes alkalmazáshoz?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "A mesterjelszó a Bitwarden webalkalmazásban módosítható." }, "fingerprintPhrase": { "message": "Azonosítókifejezés", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "A böngésző integráció nem támogatott." }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Sajnos a böngésző integrációt egyelőre csak a Mac App Store verzió támogatja." }, @@ -2688,6 +2697,9 @@ "message": "Általános formátumok", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Hibaelhárítás" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Hiba történt a célgyűjtemény hozzárendelése során." + }, + "errorAssigningTargetFolder": { + "message": "Hiba történt a célmappa hozzárendelése során." } } diff --git a/apps/desktop/src/locales/id/messages.json b/apps/desktop/src/locales/id/messages.json index 008b6b369a..cd36126b05 100644 --- a/apps/desktop/src/locales/id/messages.json +++ b/apps/desktop/src/locales/id/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Ubah Kata Sandi Utama" }, - "changeMasterPasswordConfirmation": { - "message": "Anda dapat mengubah kata sandi utama Anda di brankas web bitwarden.com. Anda ingin mengunjungi situs web sekarang?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Frase Fingerprint", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Integrasi browser tidak didukung" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Sayangnya integrasi browser hanya didukung di versi Mac App Store untuk saat ini." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/it/messages.json b/apps/desktop/src/locales/it/messages.json index a3a6f771fe..0eeea259e2 100644 --- a/apps/desktop/src/locales/it/messages.json +++ b/apps/desktop/src/locales/it/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Cambia password principale" }, - "changeMasterPasswordConfirmation": { - "message": "Puoi cambiare la tua password principale sulla cassaforte online di bitwarden.com. Vuoi visitare ora il sito?" + "continueToWebApp": { + "message": "Passa al sito web?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Puoi modificare la tua password principale sul sito web di Bitwarden." }, "fingerprintPhrase": { "message": "Frase impronta", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "L'integrazione del browser non è supportata" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Purtroppo l'integrazione del browser è supportata solo nella versione nell'App Store per ora." }, @@ -2688,6 +2697,9 @@ "message": "Formati comuni", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Risoluzione problemi" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey rimossa" + }, + "errorAssigningTargetCollection": { + "message": "Errore nell'assegnazione della raccolta di destinazione." + }, + "errorAssigningTargetFolder": { + "message": "Errore nell'assegnazione della cartella di destinazione." } } diff --git a/apps/desktop/src/locales/ja/messages.json b/apps/desktop/src/locales/ja/messages.json index f0a95d4e35..ab6c0be95f 100644 --- a/apps/desktop/src/locales/ja/messages.json +++ b/apps/desktop/src/locales/ja/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "マスターパスワードの変更" }, - "changeMasterPasswordConfirmation": { - "message": "マスターパスワードは bitwarden.com ウェブ保管庫で変更できます。ウェブサイトを開きますか?" + "continueToWebApp": { + "message": "ウェブアプリに進みますか?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Bitwarden ウェブアプリでマスターパスワードを変更できます。" }, "fingerprintPhrase": { "message": "パスフレーズ", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "ブラウザー統合はサポートされていません" }, + "browserIntegrationErrorTitle": { + "message": "ブラウザー連携を有効にする際にエラーが発生しました" + }, + "browserIntegrationErrorDesc": { + "message": "ブラウザー統合の有効化中にエラーが発生しました。" + }, "browserIntegrationMasOnlyDesc": { "message": "残念ながら、ブラウザ統合は、Mac App Storeのバージョンでのみサポートされています。" }, @@ -2688,6 +2697,9 @@ "message": "一般的な形式", "description": "Label indicating the most common import formats" }, + "success": { + "message": "成功" + }, "troubleshooting": { "message": "トラブルシューティング" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "パスキーを削除しました" + }, + "errorAssigningTargetCollection": { + "message": "ターゲットコレクションの割り当てに失敗しました。" + }, + "errorAssigningTargetFolder": { + "message": "ターゲットフォルダーの割り当てに失敗しました。" } } diff --git a/apps/desktop/src/locales/ka/messages.json b/apps/desktop/src/locales/ka/messages.json index f96260c005..889a2beeee 100644 --- a/apps/desktop/src/locales/ka/messages.json +++ b/apps/desktop/src/locales/ka/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", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/km/messages.json b/apps/desktop/src/locales/km/messages.json index f96260c005..889a2beeee 100644 --- a/apps/desktop/src/locales/km/messages.json +++ b/apps/desktop/src/locales/km/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", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/kn/messages.json b/apps/desktop/src/locales/kn/messages.json index 281d64cbc2..eb0cbcf6be 100644 --- a/apps/desktop/src/locales/kn/messages.json +++ b/apps/desktop/src/locales/kn/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "ಮಾಸ್ಟರ್ ಪಾಸ್ವರ್ಡ್ ಬದಲಾಯಿಸಿ" }, - "changeMasterPasswordConfirmation": { - "message": "ನಿಮ್ಮ ಮಾಸ್ಟರ್ ಪಾಸ್‌ವರ್ಡ್ ಅನ್ನು ನೀವು bitwarden.com ವೆಬ್ ವಾಲ್ಟ್‌ನಲ್ಲಿ ಬದಲಾಯಿಸಬಹುದು. ನೀವು ಈಗ ವೆಬ್‌ಸೈಟ್‌ಗೆ ಭೇಟಿ ನೀಡಲು ಬಯಸುವಿರಾ?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "ಫಿಂಗರ್ಪ್ರಿಂಟ್ ಫ್ರೇಸ್", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "ದುರದೃಷ್ಟವಶಾತ್ ಬ್ರೌಸರ್ ಏಕೀಕರಣವನ್ನು ಇದೀಗ ಮ್ಯಾಕ್ ಆಪ್ ಸ್ಟೋರ್ ಆವೃತ್ತಿಯಲ್ಲಿ ಮಾತ್ರ ಬೆಂಬಲಿಸಲಾಗುತ್ತದೆ." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/ko/messages.json b/apps/desktop/src/locales/ko/messages.json index a09d53b1dd..8e50ade96c 100644 --- a/apps/desktop/src/locales/ko/messages.json +++ b/apps/desktop/src/locales/ko/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "마스터 비밀번호 변경" }, - "changeMasterPasswordConfirmation": { - "message": "bitwarden.com 웹 보관함에서 마스터 비밀번호를 바꿀 수 있습니다. 지금 웹 사이트를 방문하시겠습니까?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "지문 구절", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "브라우저와 연결이 지원되지 않음" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "브라우저와 연결은 현재 Mac App Store 버전에서만 지원됩니다." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/lt/messages.json b/apps/desktop/src/locales/lt/messages.json index dbc2e13d1c..e9de697005 100644 --- a/apps/desktop/src/locales/lt/messages.json +++ b/apps/desktop/src/locales/lt/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Keisti pagrindinį slaptažodį" }, - "changeMasterPasswordConfirmation": { - "message": "Pagrindinį slaptažodį galite pakeisti bitwarden.com žiniatinklio saugykloje. Ar norite dabar apsilankyti svetainėje?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Piršto antspaudo frazė", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Naršyklės integravimas nepalaikomas" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Deja, bet naršyklės integravimas kol kas palaikomas tik Mac App Store versijoje." }, @@ -2688,6 +2697,9 @@ "message": "Dažni formatai", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/lv/messages.json b/apps/desktop/src/locales/lv/messages.json index 0a3501dded..0227a8c524 100644 --- a/apps/desktop/src/locales/lv/messages.json +++ b/apps/desktop/src/locales/lv/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Mainīt galveno paroli" }, - "changeMasterPasswordConfirmation": { - "message": "Galveno paroli ir iespējams mainīt bitwarden.com tīmekļa glabātavā. Vai tagad apmeklēt tīmekļvietni?" + "continueToWebApp": { + "message": "Pāriet uz tīmekļa lietotni?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Savu galveno paroli var mainīt Bitwarden tīmekļa lietotnē." }, "fingerprintPhrase": { "message": "Atpazīšanas vārdkopa", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Sasaistīšana ar pārlūku nav atbalstīta" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Diemžēl sasaistīšāna ar pārlūku pagaidām ir nodrošināta tikai Mac App Store laidienā." }, @@ -2688,6 +2697,9 @@ "message": "Izplatīti veidoli", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Sarežģījumu novēršana" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Piekļuves atslēga noņemta" + }, + "errorAssigningTargetCollection": { + "message": "Kļūda mērķa krājuma piešķiršanā." + }, + "errorAssigningTargetFolder": { + "message": "Kļūda mērķa mapes piešķiršanā." } } diff --git a/apps/desktop/src/locales/me/messages.json b/apps/desktop/src/locales/me/messages.json index d5e3bddf8e..1f49961b46 100644 --- a/apps/desktop/src/locales/me/messages.json +++ b/apps/desktop/src/locales/me/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Promjena glavne lozinke" }, - "changeMasterPasswordConfirmation": { - "message": "Možete promijeniti svoju glavnu lozinku u trezoru na internet strani bitwarden.com. Da li želite da posjetite internet lokaciju sada?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fraza računa", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/ml/messages.json b/apps/desktop/src/locales/ml/messages.json index 6b1137d232..96811b9dba 100644 --- a/apps/desktop/src/locales/ml/messages.json +++ b/apps/desktop/src/locales/ml/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "പ്രാഥമിക പാസ്‌വേഡ് മാറ്റുക" }, - "changeMasterPasswordConfirmation": { - "message": "തങ്ങൾക്കു Bitwarden വെബ് വാൾട്ടിൽ പ്രാഥമിക പാസ്‌വേഡ് മാറ്റാൻ സാധിക്കും.വെബ്സൈറ്റ് ഇപ്പോൾ സന്ദർശിക്കാൻ ആഗ്രഹിക്കുന്നുവോ?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "ഫിംഗർപ്രിന്റ് ഫ്രേസ്‌", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/mr/messages.json b/apps/desktop/src/locales/mr/messages.json index f96260c005..889a2beeee 100644 --- a/apps/desktop/src/locales/mr/messages.json +++ b/apps/desktop/src/locales/mr/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", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/my/messages.json b/apps/desktop/src/locales/my/messages.json index 2626e93c24..0ee0db69ef 100644 --- a/apps/desktop/src/locales/my/messages.json +++ b/apps/desktop/src/locales/my/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", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/nb/messages.json b/apps/desktop/src/locales/nb/messages.json index 8e8e2e2cbb..7bf132bdac 100644 --- a/apps/desktop/src/locales/nb/messages.json +++ b/apps/desktop/src/locales/nb/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Endre hovedpassordet" }, - "changeMasterPasswordConfirmation": { - "message": "Du kan endre superpassordet ditt på bitwarden.net-netthvelvet. Vil du besøke det nettstedet nå?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingeravtrykksfrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Nettleserintegrasjon støttes ikke" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Nettleserintegrasjon støttes dessverre bare i Mac App Store-versjonen for øyeblikket." }, @@ -2688,6 +2697,9 @@ "message": "Vanlige formater", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/ne/messages.json b/apps/desktop/src/locales/ne/messages.json index e7d586023e..13e1466805 100644 --- a/apps/desktop/src/locales/ne/messages.json +++ b/apps/desktop/src/locales/ne/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", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/nl/messages.json b/apps/desktop/src/locales/nl/messages.json index 9c4ba78036..b5f2a413d6 100644 --- a/apps/desktop/src/locales/nl/messages.json +++ b/apps/desktop/src/locales/nl/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Hoofdwachtwoord wijzigen" }, - "changeMasterPasswordConfirmation": { - "message": "Je kunt je hoofdwachtwoord wijzigen in de kluis op bitwarden.com. Wil je de website nu bezoeken?" + "continueToWebApp": { + "message": "Doorgaan naar web-app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Je kunt je hoofdwachtwoord wijzigen in de Bitwarden-webapp." }, "fingerprintPhrase": { "message": "Vingerafdrukzin", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browserintegratie niet ondersteund" }, + "browserIntegrationErrorTitle": { + "message": "Fout bij inschakelen van de browserintegratie" + }, + "browserIntegrationErrorDesc": { + "message": "Er is iets misgegaan bij het tijdens het inschakelen van de browserintegratie." + }, "browserIntegrationMasOnlyDesc": { "message": "Helaas wordt browserintegratie momenteel alleen ondersteund in de Mac App Store-versie." }, @@ -2688,6 +2697,9 @@ "message": "Veelvoorkomende formaten", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Probleemoplossing" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey verwijderd" + }, + "errorAssigningTargetCollection": { + "message": "Fout bij toewijzen doelverzameling." + }, + "errorAssigningTargetFolder": { + "message": "Fout bij toewijzen doelmap." } } diff --git a/apps/desktop/src/locales/nn/messages.json b/apps/desktop/src/locales/nn/messages.json index ea55378f5b..35e7173d74 100644 --- a/apps/desktop/src/locales/nn/messages.json +++ b/apps/desktop/src/locales/nn/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Endre hovudpassord" }, - "changeMasterPasswordConfirmation": { - "message": "Du kan endre hovudpassordet ditt i Bitwarden sin nettkvelv. Vil du gå til nettstaden no?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/or/messages.json b/apps/desktop/src/locales/or/messages.json index c6c0c2fb0c..cd83d2ea69 100644 --- a/apps/desktop/src/locales/or/messages.json +++ b/apps/desktop/src/locales/or/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", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/pl/messages.json b/apps/desktop/src/locales/pl/messages.json index a0626a6c90..250c557309 100644 --- a/apps/desktop/src/locales/pl/messages.json +++ b/apps/desktop/src/locales/pl/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Zmień hasło główne" }, - "changeMasterPasswordConfirmation": { - "message": "Hasło główne możesz zmienić na stronie sejfu bitwarden.com. Czy chcesz przejść do tej strony?" + "continueToWebApp": { + "message": "Kontynuować do aplikacji internetowej?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Możesz zmienić swoje hasło główne w aplikacji internetowej Bitwarden." }, "fingerprintPhrase": { "message": "Unikalny identyfikator konta", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Połączenie z przeglądarką nie jest obsługiwane" }, + "browserIntegrationErrorTitle": { + "message": "Błąd podczas włączania integracji z przeglądarką" + }, + "browserIntegrationErrorDesc": { + "message": "Wystąpił błąd podczas włączania integracji z przeglądarką." + }, "browserIntegrationMasOnlyDesc": { "message": "Połączenie z przeglądarką jest obsługiwane tylko z wersją aplikacji ze sklepu Mac App Store." }, @@ -2688,6 +2697,9 @@ "message": "Popularne formaty", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Sukces" + }, "troubleshooting": { "message": "Rozwiązywanie problemów" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey został usunięty" + }, + "errorAssigningTargetCollection": { + "message": "Wystąpił błąd podczas przypisywania kolekcji." + }, + "errorAssigningTargetFolder": { + "message": "Wystąpił błąd podczas przypisywania folderu." } } diff --git a/apps/desktop/src/locales/pt_BR/messages.json b/apps/desktop/src/locales/pt_BR/messages.json index c8f8316e6d..9a79ad665e 100644 --- a/apps/desktop/src/locales/pt_BR/messages.json +++ b/apps/desktop/src/locales/pt_BR/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Alterar Senha Mestra" }, - "changeMasterPasswordConfirmation": { - "message": "Você pode alterar a sua senha mestra no cofre web em bitwarden.com. Você deseja visitar o site agora?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Frase biométrica", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Integração com o navegador não suportado" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Infelizmente, por ora, a integração do navegador só é suportada na versão da Mac App Store." }, @@ -2688,6 +2697,9 @@ "message": "Formatos comuns", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/pt_PT/messages.json b/apps/desktop/src/locales/pt_PT/messages.json index 5fbd7636d1..14f0ec5d2f 100644 --- a/apps/desktop/src/locales/pt_PT/messages.json +++ b/apps/desktop/src/locales/pt_PT/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Alterar palavra-passe mestra" }, - "changeMasterPasswordConfirmation": { - "message": "Pode alterar o seu endereço de e-mail no cofre do site bitwarden.com. Deseja visitar o site agora?" + "continueToWebApp": { + "message": "Continuar para a aplicação Web?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Pode alterar a sua palavra-passe mestra na aplicação Web Bitwarden." }, "fingerprintPhrase": { "message": "Frase de impressão digital", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Integração com o navegador não suportada" }, + "browserIntegrationErrorTitle": { + "message": "Erro ao ativar a integração do navegador" + }, + "browserIntegrationErrorDesc": { + "message": "Ocorreu um erro ao ativar a integração do navegador." + }, "browserIntegrationMasOnlyDesc": { "message": "Infelizmente, a integração do navegador só é suportada na versão da Mac App Store por enquanto." }, @@ -2688,6 +2697,9 @@ "message": "Formatos comuns", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Com sucesso" + }, "troubleshooting": { "message": "Resolução de problemas" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Chave de acesso removida" + }, + "errorAssigningTargetCollection": { + "message": "Erro ao atribuir a coleção de destino." + }, + "errorAssigningTargetFolder": { + "message": "Erro ao atribuir a pasta de destino." } } diff --git a/apps/desktop/src/locales/ro/messages.json b/apps/desktop/src/locales/ro/messages.json index 7af8f7ec16..978f57eb9b 100644 --- a/apps/desktop/src/locales/ro/messages.json +++ b/apps/desktop/src/locales/ro/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Schimbare parolă principală" }, - "changeMasterPasswordConfirmation": { - "message": "Puteți modifica parola principală pe saitul web bitwarden.com. Doriți să vizitați saitul acum?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Frază amprentă", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Integrarea browserului nu este acceptată" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Din păcate, integrarea browserului este acceptată numai în versiunea Mac App Store pentru moment." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/ru/messages.json b/apps/desktop/src/locales/ru/messages.json index 0e28c2cf90..c9b3b95b39 100644 --- a/apps/desktop/src/locales/ru/messages.json +++ b/apps/desktop/src/locales/ru/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Изменить мастер-пароль" }, - "changeMasterPasswordConfirmation": { - "message": "Вы можете изменить свой мастер-пароль на bitwarden.com. Перейти на сайт сейчас?" + "continueToWebApp": { + "message": "Перейти к веб-приложению?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Изменить мастер-пароль можно в веб-приложении Bitwarden." }, "fingerprintPhrase": { "message": "Фраза отпечатка", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Интеграция с браузером не поддерживается" }, + "browserIntegrationErrorTitle": { + "message": "Ошибка при включении интеграции с браузером" + }, + "browserIntegrationErrorDesc": { + "message": "Произошла ошибка при включении интеграции с браузером." + }, "browserIntegrationMasOnlyDesc": { "message": "К сожалению, интеграция браузера пока поддерживается только в версии Mac App Store." }, @@ -2688,6 +2697,9 @@ "message": "Основные форматы", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Успешно" + }, "troubleshooting": { "message": "Устранение проблем" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey удален" + }, + "errorAssigningTargetCollection": { + "message": "Ошибка при назначении целевой коллекции." + }, + "errorAssigningTargetFolder": { + "message": "Ошибка при назначении целевой папки." } } diff --git a/apps/desktop/src/locales/si/messages.json b/apps/desktop/src/locales/si/messages.json index 60e8aea93c..3d43997144 100644 --- a/apps/desktop/src/locales/si/messages.json +++ b/apps/desktop/src/locales/si/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", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/sk/messages.json b/apps/desktop/src/locales/sk/messages.json index 240b883254..6499486b9d 100644 --- a/apps/desktop/src/locales/sk/messages.json +++ b/apps/desktop/src/locales/sk/messages.json @@ -445,7 +445,7 @@ "message": "Vyhnúť sa zameniteľným znakom" }, "searchCollection": { - "message": "Search collection" + "message": "Vyhľadať zbierku" }, "searchFolder": { "message": "Search folder" @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Zmeniť hlavné heslo" }, - "changeMasterPasswordConfirmation": { - "message": "Svoje hlavné heslo môžete zmeniť vo webovom trezore bitwarden.com. Chcete teraz navštíviť túto stránku?" + "continueToWebApp": { + "message": "Pokračovať vo webovej aplikácii?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Hlavné heslo si môžete zmeniť vo webovej aplikácii Bitwarden." }, "fingerprintPhrase": { "message": "Fráza odtlačku", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Integrácia v prehliadači nie je podporovaná" }, + "browserIntegrationErrorTitle": { + "message": "Chyba pri povoľovaní integrácie v prehliadači" + }, + "browserIntegrationErrorDesc": { + "message": "Pri povoľovaní integrácie v prehliadači sa vyskytla chyba." + }, "browserIntegrationMasOnlyDesc": { "message": "Bohužiaľ, integrácia v prehliadači je zatiaľ podporovaná iba vo verzii Mac App Store." }, @@ -2688,6 +2697,9 @@ "message": "Bežné formáty", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Úspech" + }, "troubleshooting": { "message": "Riešenie problémov" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Prístupový kľúč bol odstránený" + }, + "errorAssigningTargetCollection": { + "message": "Chyba pri priraďovaní cieľovej kolekcie." + }, + "errorAssigningTargetFolder": { + "message": "Chyba pri priraďovaní cieľového priečinka." } } diff --git a/apps/desktop/src/locales/sl/messages.json b/apps/desktop/src/locales/sl/messages.json index 528274cf29..8cb06dcf0c 100644 --- a/apps/desktop/src/locales/sl/messages.json +++ b/apps/desktop/src/locales/sl/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Spremeni glavno geslo" }, - "changeMasterPasswordConfirmation": { - "message": "Svoje glavno geslo lahko spremenite v bitwarden.com spletnem trezorju. Želite obiskati spletno stran zdaj?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Identifikacijsko geslo", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/sr/messages.json b/apps/desktop/src/locales/sr/messages.json index 77b5f7221d..37c5dfa382 100644 --- a/apps/desktop/src/locales/sr/messages.json +++ b/apps/desktop/src/locales/sr/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Промени главну лозинку" }, - "changeMasterPasswordConfirmation": { - "message": "Можете променити главну лозинку у Вашем сефу на bitwarden.com. Да ли желите да посетите веб страницу сада?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Сигурносна Фраза Сефа", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Интеграција са претраживачем није подржана" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Нажалост, интеграција прегледача за сада је подржана само у верзији Mac App Store." }, @@ -2688,6 +2697,9 @@ "message": "Уобичајени формати", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Решавање проблема" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Приступачни кључ је уклоњен" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/sv/messages.json b/apps/desktop/src/locales/sv/messages.json index c07f7efef3..bd21c0f328 100644 --- a/apps/desktop/src/locales/sv/messages.json +++ b/apps/desktop/src/locales/sv/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Ändra huvudlösenord" }, - "changeMasterPasswordConfirmation": { - "message": "Du kan ändra ditt huvudlösenord i Bitwardens webbvalv. Vill du besöka webbplatsen nu?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingeravtrycksfras", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Webbläsarintegration stöds inte" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Tyvärr stöds webbläsarintegration för tillfället endast i versionen från Mac App Store." }, @@ -2688,6 +2697,9 @@ "message": "Vanliga format", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Felsökning" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Nyckel borttagen" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/te/messages.json b/apps/desktop/src/locales/te/messages.json index f96260c005..889a2beeee 100644 --- a/apps/desktop/src/locales/te/messages.json +++ b/apps/desktop/src/locales/te/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", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/th/messages.json b/apps/desktop/src/locales/th/messages.json index efbfc86b33..f1cd5351f7 100644 --- a/apps/desktop/src/locales/th/messages.json +++ b/apps/desktop/src/locales/th/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "เปลี่ยนรหัสผ่านหลัก" }, - "changeMasterPasswordConfirmation": { - "message": "คุณสามารถเปลี่ยนรหัสผ่านหลักของคุณได้ที่เว็บ bitwarden.com คุณต้องการไปที่เว็บไซต์ตอนนี้ไหม?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint Phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/tr/messages.json b/apps/desktop/src/locales/tr/messages.json index 36d62ed2a5..3e7229c41b 100644 --- a/apps/desktop/src/locales/tr/messages.json +++ b/apps/desktop/src/locales/tr/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Ana parolayı değiştir" }, - "changeMasterPasswordConfirmation": { - "message": "Ana parolanızı bitwarden.com web kasası üzerinden değiştirebilirsiniz. Siteyi şimdi ziyaret etmek ister misiniz?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Parmak izi ifadesi", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Tarayıcı entegrasyonu desteklenmiyor" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Ne yazık ki tarayıcı entegrasyonu şu anda sadece Mac App Store sürümünde destekleniyor." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Sorun giderme" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/uk/messages.json b/apps/desktop/src/locales/uk/messages.json index 377fd23b0b..9ee7652093 100644 --- a/apps/desktop/src/locales/uk/messages.json +++ b/apps/desktop/src/locales/uk/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Змінити головний пароль" }, - "changeMasterPasswordConfirmation": { - "message": "Ви можете змінити головний пароль в сховищі на bitwarden.com. Хочете перейти на вебсайт зараз?" + "continueToWebApp": { + "message": "Продовжити у вебпрограмі?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Ви можете змінити головний пароль у вебпрограмі Bitwarden." }, "fingerprintPhrase": { "message": "Фраза відбитка", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Інтеграція з браузером не підтримується" }, + "browserIntegrationErrorTitle": { + "message": "Помилка увімкнення інтеграції з браузером" + }, + "browserIntegrationErrorDesc": { + "message": "Під час увімкнення інтеграції з браузером сталася помилка." + }, "browserIntegrationMasOnlyDesc": { "message": "На жаль, зараз інтеграція з браузером підтримується лише у версії для Mac з App Store." }, @@ -2688,6 +2697,9 @@ "message": "Поширені формати", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Успішно" + }, "troubleshooting": { "message": "Усунення проблем" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Ключ доступу вилучено" + }, + "errorAssigningTargetCollection": { + "message": "Помилка призначення цільової збірки." + }, + "errorAssigningTargetFolder": { + "message": "Помилка призначення цільової теки." } } diff --git a/apps/desktop/src/locales/vi/messages.json b/apps/desktop/src/locales/vi/messages.json index 138773d40a..0c0e6f6df7 100644 --- a/apps/desktop/src/locales/vi/messages.json +++ b/apps/desktop/src/locales/vi/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Thay đổi mật khẩu chính" }, - "changeMasterPasswordConfirmation": { - "message": "Bạn có thể thay đổi mật khẩu chính trong kho bitwarden nền web. Bạn có muốn truy cập trang web bây giờ?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint Phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Không hỗ trợ tích hợp trình duyệt" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Rất tiếc, tính năng tích hợp trình duyệt hiện chỉ được hỗ trợ trong phiên bản App Store trên Mac." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/zh_CN/messages.json b/apps/desktop/src/locales/zh_CN/messages.json index 8725fa0f21..3819cb967c 100644 --- a/apps/desktop/src/locales/zh_CN/messages.json +++ b/apps/desktop/src/locales/zh_CN/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "修改主密码" }, - "changeMasterPasswordConfirmation": { - "message": "您可以在 bitwarden.com 网页密码库修改您的主密码。现在要访问吗?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "指纹短语", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "不支持浏览器集成" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "很遗憾,目前仅 Mac App Store 版本支持浏览器集成。" }, @@ -2688,6 +2697,9 @@ "message": "常规格式", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "故障排除" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "通行密钥已移除" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/zh_TW/messages.json b/apps/desktop/src/locales/zh_TW/messages.json index c93f236976..5f768b0a43 100644 --- a/apps/desktop/src/locales/zh_TW/messages.json +++ b/apps/desktop/src/locales/zh_TW/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "變更主密碼" }, - "changeMasterPasswordConfirmation": { - "message": "您可以在 bitwarden.com 網頁版密碼庫變更主密碼。現在要前往嗎?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "指紋短語", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "不支援瀏覽器整合" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "很遺憾,目前僅 Mac App Store 版本支援瀏覽器整合功能。" }, @@ -2688,6 +2697,9 @@ "message": "常見格式", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "疑難排解" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "金鑰已移除" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } From c1c6afb0f48e9cc8852448a0826263810f7a5cae Mon Sep 17 00:00:00 2001 From: Robyn MacCallum <robyntmaccallum@gmail.com> Date: Fri, 19 Apr 2024 09:45:09 -0400 Subject: [PATCH 226/351] [PM-7562] Add DuckDuckGo back to State Service (#8791) * Add ddg back to state service * Remove getters --- .../desktop/src/app/accounts/settings.component.ts | 5 +++++ .../src/platform/abstractions/state.service.ts | 7 +++++++ .../src/platform/models/domain/global-state.ts | 1 + libs/common/src/platform/services/state.service.ts | 14 ++++++++++++++ 4 files changed, 27 insertions(+) diff --git a/apps/desktop/src/app/accounts/settings.component.ts b/apps/desktop/src/app/accounts/settings.component.ts index 06533e18fc..f3958d7c87 100644 --- a/apps/desktop/src/app/accounts/settings.component.ts +++ b/apps/desktop/src/app/accounts/settings.component.ts @@ -660,6 +660,11 @@ export class SettingsComponent implements OnInit { this.form.value.enableDuckDuckGoBrowserIntegration, ); + // Adding to cover users on a previous version of DDG + await this.stateService.setEnableDuckDuckGoBrowserIntegration( + this.form.value.enableDuckDuckGoBrowserIntegration, + ); + if (!this.form.value.enableBrowserIntegration) { await this.stateService.setDuckDuckGoSharedKey(null); } diff --git a/libs/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index 2348c8844a..27026ac0ea 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -74,6 +74,13 @@ export abstract class StateService<T extends Account = Account> { * Used when Lock with MP on Restart is enabled */ setPinKeyEncryptedUserKeyEphemeral: (value: EncString, options?: StorageOptions) => Promise<void>; + /** + * @deprecated For backwards compatible purposes only, use DesktopAutofillSettingsService + */ + setEnableDuckDuckGoBrowserIntegration: ( + value: boolean, + options?: StorageOptions, + ) => Promise<void>; /** * @deprecated For migration purposes only, use getUserKeyMasterKey instead */ diff --git a/libs/common/src/platform/models/domain/global-state.ts b/libs/common/src/platform/models/domain/global-state.ts index 703a998d1c..cd7cf7d174 100644 --- a/libs/common/src/platform/models/domain/global-state.ts +++ b/libs/common/src/platform/models/domain/global-state.ts @@ -4,4 +4,5 @@ export class GlobalState { vaultTimeoutAction?: string; enableBrowserIntegration?: boolean; enableBrowserIntegrationFingerprint?: boolean; + enableDuckDuckGoBrowserIntegration?: boolean; } diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index 9edc9ed1e3..1fb2d71670 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -571,6 +571,20 @@ export class StateService< ); } + async setEnableDuckDuckGoBrowserIntegration( + value: boolean, + options?: StorageOptions, + ): Promise<void> { + const globals = await this.getGlobals( + this.reconcileOptions(options, await this.defaultOnDiskOptions()), + ); + globals.enableDuckDuckGoBrowserIntegration = value; + await this.saveGlobals( + globals, + this.reconcileOptions(options, await this.defaultOnDiskOptions()), + ); + } + /** * @deprecated Use UserKey instead */ From 6f2bed63a63ed3b6ddf49066bc8c7b000be44688 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Gon=C3=A7alves?= <cgoncalves@bitwarden.com> Date: Fri, 19 Apr 2024 14:53:34 +0100 Subject: [PATCH 227/351] [PM-7569] Fix ciphers view update race on desktop (#8821) * PM-7569 Wait for the update before allow reading ciphers$ * PM-7569 Remove commented line --- .../src/vault/services/cipher.service.ts | 39 +++++++++++++++++-- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 4a13196c9c..0b44636ea6 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -1,4 +1,4 @@ -import { Observable, firstValueFrom, map } from "rxjs"; +import { Observable, firstValueFrom, map, share, skipWhile, switchMap } from "rxjs"; import { SemVer } from "semver"; import { ApiService } from "../../abstractions/api.service"; @@ -21,7 +21,13 @@ 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 { + ActiveUserState, + CIPHERS_MEMORY, + DeriveDefinition, + DerivedState, + 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"; @@ -71,10 +77,14 @@ export class CipherService implements CipherServiceAbstraction { private sortedCiphersCache: SortedCiphersCache = new SortedCiphersCache( this.sortCiphersByLastUsed, ); + private ciphersExpectingUpdate: DerivedState<boolean>; localData$: Observable<Record<CipherId, LocalData>>; ciphers$: Observable<Record<CipherId, CipherData>>; cipherViews$: Observable<Record<CipherId, CipherView>>; + viewFor$(id: CipherId) { + return this.cipherViews$.pipe(map((views) => views[id])); + } addEditCipherInfo$: Observable<AddEditCipherInfo>; private localDataState: ActiveUserState<Record<CipherId, LocalData>>; @@ -99,10 +109,29 @@ export class CipherService implements CipherServiceAbstraction { this.encryptedCiphersState = this.stateProvider.getActive(ENCRYPTED_CIPHERS); this.decryptedCiphersState = this.stateProvider.getActive(DECRYPTED_CIPHERS); this.addEditCipherInfoState = this.stateProvider.getActive(ADD_EDIT_CIPHER_INFO_KEY); + this.ciphersExpectingUpdate = this.stateProvider.getDerived( + this.encryptedCiphersState.state$, + new DeriveDefinition(CIPHERS_MEMORY, "ciphersExpectingUpdate", { + derive: (_: Record<CipherId, CipherData>) => false, + deserializer: (value) => value, + }), + {}, + ); 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 ?? {})); + // First wait for ciphersExpectingUpdate to be false before emitting ciphers + this.ciphers$ = this.ciphersExpectingUpdate.state$.pipe( + skipWhile((expectingUpdate) => expectingUpdate), + switchMap(() => this.encryptedCiphersState.state$), + map((ciphers) => ciphers ?? {}), + ); + this.cipherViews$ = this.decryptedCiphersState.state$.pipe( + map((views) => views ?? {}), + + share({ + resetOnRefCountZero: true, + }), + ); this.addEditCipherInfo$ = this.addEditCipherInfoState.state$; } @@ -807,6 +836,8 @@ export class CipherService implements CipherServiceAbstraction { private async updateEncryptedCipherState( update: (current: Record<CipherId, CipherData>) => Record<CipherId, CipherData>, ): Promise<Record<CipherId, CipherData>> { + // Store that we should wait for an update to return any ciphers + await this.ciphersExpectingUpdate.forceValue(true); await this.clearDecryptedCiphersState(); const [, updatedCiphers] = await this.encryptedCiphersState.update((current) => { const result = update(current ?? {}); From 8cb16fb406b735e4fb97bcfe4dc46b00c0a913d5 Mon Sep 17 00:00:00 2001 From: Joseph Flinn <58369717+joseph-flinn@users.noreply.github.com> Date: Fri, 19 Apr 2024 07:02:48 -0700 Subject: [PATCH 228/351] Make extension copy updates for Marketing (#8822) --- apps/browser/src/_locales/en/messages.json | 4 +- apps/browser/store/locales/en/copy.resx | 103 ++++++++++++-------- apps/browser/store/windows/AppxManifest.xml | 8 +- 3 files changed, 66 insertions(+), 49 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 8c81088fc5..7e6e333689 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Free Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "A secure and free password manager for all of your devices.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/store/locales/en/copy.resx b/apps/browser/store/locales/en/copy.resx index 191198691d..df8d63835c 100644 --- a/apps/browser/store/locales/en/copy.resx +++ b/apps/browser/store/locales/en/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,41 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden – Free Password Manager</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>A secure and free password manager for all of your devices</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS &amp; WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -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. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose 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. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 50 languages. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>A secure and free password manager for all of your devices</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Sync and access your vault from multiple devices</value> diff --git a/apps/browser/store/windows/AppxManifest.xml b/apps/browser/store/windows/AppxManifest.xml index f57b3db988..df02ea085c 100644 --- a/apps/browser/store/windows/AppxManifest.xml +++ b/apps/browser/store/windows/AppxManifest.xml @@ -11,7 +11,7 @@ Version="0.0.0.0"/> <Properties> - <DisplayName>Bitwarden Extension - Free Password Manager</DisplayName> + <DisplayName>Bitwarden Password Manager</DisplayName> <PublisherDisplayName>8bit Solutions LLC</PublisherDisplayName> <Logo>Assets/icon_50.png</Logo> </Properties> @@ -30,10 +30,10 @@ <Application Id="App"> <uap:VisualElements AppListEntry="none" - DisplayName="Bitwarden Extension - Free Password Manager" + DisplayName="Bitwarden Password Manager" Square150x150Logo="Assets/icon_150.png" Square44x44Logo="Assets/icon_44.png" - Description="A secure and free password manager for all of your devices." + Description="At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information." BackgroundColor="white"> </uap:VisualElements> <Extensions> @@ -41,7 +41,7 @@ <uap3:AppExtension Name="com.microsoft.edge.extension" Id="EdgeExtension" PublicFolder="Extension" - DisplayName="Bitwarden Extension - Free Password Manager"> + DisplayName="Bitwarden Password Manager"> </uap3:AppExtension> </uap3:Extension> </Extensions> From 6cafb1d28fb5dd1fe4e524c6f2f203e3fa23b2a0 Mon Sep 17 00:00:00 2001 From: Merissa Weinstein <merissa.k.weinstein@gmail.com> Date: Fri, 19 Apr 2024 09:09:58 -0500 Subject: [PATCH 229/351] [PM-2870] [PM-2865] Accessibility updates: add labels to buttons & form checkboxes (#8358) * organization-options: add area-labels to links * vault-cipher-row: add aria-label to input checkbox * vault-collection-row: add aria-label to collection item * add internationalizatino to org options menu * add internationlization to checkbox aria-labels for vault and collection items * organization-options-component: remove added aria-lables to buttons inside of toggle --------- Co-authored-by: Merissa Weinstein <merissaweinstein@merissas-mbp-2.lan> --- .../vault-items/vault-cipher-row.component.html | 1 + .../vault-items/vault-collection-row.component.html | 1 + .../components/organization-options.component.html | 7 ++++++- apps/web/src/locales/en/messages.json | 9 +++++++++ 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html b/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html index 5ddabf0557..ae22d89f7f 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html +++ b/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html @@ -6,6 +6,7 @@ [disabled]="disabled" [checked]="checked" (change)="$event ? this.checkedToggled.next() : null" + [attr.aria-label]="'vaultItemSelect' | i18n" /> </td> <td bitCell [ngClass]="RowHeightClass" class="tw-min-w-fit"> diff --git a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.html b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.html index d333f92d5c..d03b6dcc38 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.html +++ b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.html @@ -7,6 +7,7 @@ [disabled]="disabled" [checked]="checked" (change)="$event ? this.checkedToggled.next() : null" + [attr.aria-label]="'collectionItemSelect' | i18n" /> </td> <td bitCell [ngClass]="RowHeightClass" class="tw-min-w-fit"> diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.html b/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.html index f4fb2cc040..0b94b6e2be 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.html +++ b/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.html @@ -1,5 +1,10 @@ <ng-container *ngIf="!hideMenu"> - <button type="button" [bitMenuTriggerFor]="optionsMenu" class="filter-options-icon"> + <button + type="button" + [bitMenuTriggerFor]="optionsMenu" + class="filter-options-icon" + [attr.aria-label]="'organizationOptionsMenu' | i18n" + > <i class="bwi bwi-ellipsis-v" aria-hidden="true"></i> </button> <bit-menu class="filter-organization-options" #optionsMenu> diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 845816562b..63069a83de 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } From fffef95c5ef0ef3f2a9112eb3e2f49d5412362a0 Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Fri, 19 Apr 2024 11:20:13 -0400 Subject: [PATCH 230/351] Auth/PM-7235 - Refactor AuthService.getAuthStatus, deprecate everBeenUnlocked, and handle initialization of auto user key on client init (#8590) * PM-7235 - AuthSvc - Refactor getAuthStatus to simply use the cryptoService.hasUserKey check to determine the user's auth status. * PM-7235 - CryptoSvc - getUserKey - remove setUserKey side effect if auto key is stored. Will move to app init * PM-7235 - For each client init service, add setUserKeyInMemoryIfAutoUserKeySet logic * PM-7235 - CryptoSvc tests - remove uncessary test. * PM-7235 - Create UserKeyInitService and inject into all init services with new listening logic to support acct switching. * PM-7235 - UserKeyInitSvc - minor refactor of setUserKeyInMemoryIfAutoUserKeySet * PM-7235 - Add test suite for UserKeyInitService * PM-7235 - Remove everBeenUnlocked as it is no longer needed * PM-7235 - Fix tests * PM-7235 - UserKeyInitSvc - per PR feedback, add error handling to protect observable stream from being cancelled in case of an error * PM-7235 - Fix tests * Update libs/common/src/platform/services/user-key-init.service.ts Co-authored-by: Matt Gibson <mgibson@bitwarden.com> * Update libs/common/src/platform/services/user-key-init.service.ts Co-authored-by: Matt Gibson <mgibson@bitwarden.com> * PM-7235 - AuthSvc - Per PR review, for getAuthStatus, only check user key existence in memory. * PM-7235 - remove not useful test per PR feedback. * PM-7235 - Per PR feedback, update cryptoService.hasUserKey to only check memory for the user key. * PM-7235 - Per PR feedback, move user key init service listener to main.background instead of init service * PM-7235 - UserKeyInitSvc tests - fix tests to plass --------- Co-authored-by: Matt Gibson <mgibson@bitwarden.com> --- .../browser/src/background/main.background.ts | 12 ++ .../src/popup/services/init.service.ts | 1 - apps/desktop/src/app/services/init.service.ts | 4 + apps/web/src/app/core/init.service.ts | 3 + .../src/auth/components/lock.component.ts | 1 - .../src/services/jslib-services.module.ts | 7 + libs/common/src/auth/services/auth.service.ts | 29 +--- .../platform/abstractions/state.service.ts | 2 - .../src/platform/models/domain/account.ts | 1 - .../platform/services/crypto.service.spec.ts | 27 +-- .../src/platform/services/crypto.service.ts | 20 +-- .../src/platform/services/state.service.ts | 18 -- .../services/user-key-init.service.spec.ts | 162 ++++++++++++++++++ .../services/user-key-init.service.ts | 57 ++++++ .../vault-timeout-settings.service.ts | 1 - .../vault-timeout.service.spec.ts | 1 - .../vault-timeout/vault-timeout.service.ts | 1 - 17 files changed, 253 insertions(+), 94 deletions(-) create mode 100644 libs/common/src/platform/services/user-key-init.service.spec.ts create mode 100644 libs/common/src/platform/services/user-key-init.service.ts diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 325a7f1943..8432c398b7 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -110,6 +110,7 @@ import { MigrationBuilderService } from "@bitwarden/common/platform/services/mig import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider"; import { SystemService } from "@bitwarden/common/platform/services/system.service"; +import { UserKeyInitService } from "@bitwarden/common/platform/services/user-key-init.service"; import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service"; import { ActiveUserStateProvider, @@ -325,6 +326,7 @@ export default class MainBackground { stateEventRunnerService: StateEventRunnerService; ssoLoginService: SsoLoginServiceAbstraction; billingAccountProfileStateService: BillingAccountProfileStateService; + userKeyInitService: UserKeyInitService; scriptInjectorService: BrowserScriptInjectorService; onUpdatedRan: boolean; @@ -1046,6 +1048,12 @@ export default class MainBackground { ); } } + + this.userKeyInitService = new UserKeyInitService( + this.accountService, + this.cryptoService, + this.logService, + ); } async bootstrap() { @@ -1053,6 +1061,10 @@ export default class MainBackground { await this.stateService.init({ runMigrations: !this.isPrivateMode }); + // This is here instead of in in the InitService b/c we don't plan for + // side effects to run in the Browser InitService. + this.userKeyInitService.listenForActiveUserChangesToSetUserKey(); + await (this.i18nService as I18nService).init(); await (this.eventUploadService as EventUploadService).init(true); this.twoFactorService.init(); diff --git a/apps/browser/src/popup/services/init.service.ts b/apps/browser/src/popup/services/init.service.ts index 4036ace31f..c9e6d66c2a 100644 --- a/apps/browser/src/popup/services/init.service.ts +++ b/apps/browser/src/popup/services/init.service.ts @@ -9,7 +9,6 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { BrowserApi } from "../../platform/browser/browser-api"; import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; import { BrowserStateService as StateServiceAbstraction } from "../../platform/services/abstractions/browser-state.service"; - @Injectable() export class InitService { constructor( diff --git a/apps/desktop/src/app/services/init.service.ts b/apps/desktop/src/app/services/init.service.ts index d1a83d468c..ae2e1ba97c 100644 --- a/apps/desktop/src/app/services/init.service.ts +++ b/apps/desktop/src/app/services/init.service.ts @@ -12,6 +12,7 @@ import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platfor import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; +import { UserKeyInitService } from "@bitwarden/common/platform/services/user-key-init.service"; import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service"; import { VaultTimeoutService } from "@bitwarden/common/services/vault-timeout/vault-timeout.service"; import { SyncService as SyncServiceAbstraction } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; @@ -35,6 +36,7 @@ export class InitService { private nativeMessagingService: NativeMessagingService, private themingService: AbstractThemingService, private encryptService: EncryptService, + private userKeyInitService: UserKeyInitService, @Inject(DOCUMENT) private document: Document, ) {} @@ -42,6 +44,8 @@ export class InitService { return async () => { this.nativeMessagingService.init(); await this.stateService.init({ runMigrations: false }); // Desktop will run them in main process + this.userKeyInitService.listenForActiveUserChangesToSetUserKey(); + // 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.syncService.fullSync(true); diff --git a/apps/web/src/app/core/init.service.ts b/apps/web/src/app/core/init.service.ts index d5576d3bf7..dab6ed5e3d 100644 --- a/apps/web/src/app/core/init.service.ts +++ b/apps/web/src/app/core/init.service.ts @@ -11,6 +11,7 @@ import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt. import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; +import { UserKeyInitService } from "@bitwarden/common/platform/services/user-key-init.service"; import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service"; import { VaultTimeoutService } from "@bitwarden/common/services/vault-timeout/vault-timeout.service"; @@ -27,12 +28,14 @@ export class InitService { private cryptoService: CryptoServiceAbstraction, private themingService: AbstractThemingService, private encryptService: EncryptService, + private userKeyInitService: UserKeyInitService, @Inject(DOCUMENT) private document: Document, ) {} init() { return async () => { await this.stateService.init(); + this.userKeyInitService.listenForActiveUserChangesToSetUserKey(); setTimeout(() => this.notificationsService.init(), 3000); await this.vaultTimeoutService.init(true); diff --git a/libs/angular/src/auth/components/lock.component.ts b/libs/angular/src/auth/components/lock.component.ts index 6602a917c9..9c2ed55357 100644 --- a/libs/angular/src/auth/components/lock.component.ts +++ b/libs/angular/src/auth/components/lock.component.ts @@ -283,7 +283,6 @@ export class LockComponent implements OnInit, OnDestroy { } private async doContinue(evaluatePasswordAfterUnlock: boolean) { - await this.stateService.setEverBeenUnlocked(true); await this.biometricStateService.resetUserPromptCancelled(); this.messagingService.send("unlocked"); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 859103474d..b0d84e7c3b 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -52,6 +52,7 @@ import { ProviderApiService } from "@bitwarden/common/admin-console/services/pro import { ProviderService } from "@bitwarden/common/admin-console/services/provider.service"; import { AccountApiService as AccountApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/account-api.service"; import { + AccountService, AccountService as AccountServiceAbstraction, InternalAccountService, } from "@bitwarden/common/auth/abstractions/account.service"; @@ -154,6 +155,7 @@ import { MigrationRunner } from "@bitwarden/common/platform/services/migration-r import { NoopNotificationsService } from "@bitwarden/common/platform/services/noop-notifications.service"; import { StateService } from "@bitwarden/common/platform/services/state.service"; import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider"; +import { UserKeyInitService } from "@bitwarden/common/platform/services/user-key-init.service"; import { ValidationService } from "@bitwarden/common/platform/services/validation.service"; import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service"; import { @@ -1115,6 +1117,11 @@ const safeProviders: SafeProvider[] = [ useClass: DefaultOrganizationManagementPreferencesService, deps: [StateProvider], }), + safeProvider({ + provide: UserKeyInitService, + useClass: UserKeyInitService, + deps: [AccountService, CryptoServiceAbstraction, LogService], + }), safeProvider({ provide: ErrorHandler, useClass: LoggingErrorHandler, diff --git a/libs/common/src/auth/services/auth.service.ts b/libs/common/src/auth/services/auth.service.ts index 7a29d313e7..c9e711b4cc 100644 --- a/libs/common/src/auth/services/auth.service.ts +++ b/libs/common/src/auth/services/auth.service.ts @@ -12,7 +12,6 @@ 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"; @@ -91,31 +90,11 @@ export class AuthService implements AuthServiceAbstraction { return AuthenticationStatus.LoggedOut; } - // If we don't have a user key in memory, we're locked - if (!(await this.cryptoService.hasUserKeyInMemory(userId))) { - // Check if the user has vault timeout set to never and verify that - // they've never unlocked their vault - const neverLock = - (await this.cryptoService.hasUserKeyStored(KeySuffixOptions.Auto, userId)) && - !(await this.stateService.getEverBeenUnlocked({ userId: userId })); + // Note: since we aggresively set the auto user key to memory if it exists on app init (see InitService) + // we only need to check if the user key is in memory. + const hasUserKey = await this.cryptoService.hasUserKeyInMemory(userId as UserId); - if (neverLock) { - // Attempt to get the key from storage and set it in memory - const userKey = await this.cryptoService.getUserKeyFromStorage( - KeySuffixOptions.Auto, - userId, - ); - await this.cryptoService.setUserKey(userKey, userId); - } - } - - // We do another check here in case setting the auto key failed - const hasKeyInMemory = await this.cryptoService.hasUserKeyInMemory(userId); - if (!hasKeyInMemory) { - return AuthenticationStatus.Locked; - } - - return AuthenticationStatus.Unlocked; + return hasUserKey ? AuthenticationStatus.Unlocked : AuthenticationStatus.Locked; } logOut(callback: () => void) { diff --git a/libs/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index 27026ac0ea..051604f0ae 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -148,8 +148,6 @@ export abstract class StateService<T extends Account = Account> { * @deprecated For migration purposes only, use setEncryptedUserKeyPin instead */ setEncryptedPinProtected: (value: string, options?: StorageOptions) => Promise<void>; - getEverBeenUnlocked: (options?: StorageOptions) => Promise<boolean>; - setEverBeenUnlocked: (value: boolean, options?: StorageOptions) => Promise<void>; getIsAuthenticated: (options?: StorageOptions) => Promise<boolean>; getKdfConfig: (options?: StorageOptions) => Promise<KdfConfig>; setKdfConfig: (kdfConfig: KdfConfig, options?: StorageOptions) => Promise<void>; diff --git a/libs/common/src/platform/models/domain/account.ts b/libs/common/src/platform/models/domain/account.ts index ae7780ada4..5a9a764696 100644 --- a/libs/common/src/platform/models/domain/account.ts +++ b/libs/common/src/platform/models/domain/account.ts @@ -126,7 +126,6 @@ export class AccountProfile { name?: string; email?: string; emailVerified?: boolean; - everBeenUnlocked?: boolean; lastSync?: string; userId?: string; kdfIterations?: number; diff --git a/libs/common/src/platform/services/crypto.service.spec.ts b/libs/common/src/platform/services/crypto.service.spec.ts index 16e6d4aa63..2f68cf2ce7 100644 --- a/libs/common/src/platform/services/crypto.service.spec.ts +++ b/libs/common/src/platform/services/crypto.service.spec.ts @@ -91,21 +91,7 @@ describe("cryptoService", () => { expect(userKey).toEqual(mockUserKey); }); - it("sets from the Auto key if the User Key if not set", async () => { - const autoKeyB64 = - "IT5cA1i5Hncd953pb00E58D2FqJX+fWTj4AvoI67qkGHSQPgulAqKv+LaKRAo9Bg0xzP9Nw00wk4TqjMmGSM+g=="; - stateService.getUserKeyAutoUnlock.mockResolvedValue(autoKeyB64); - const setKeySpy = jest.spyOn(cryptoService, "setUserKey"); - - const userKey = await cryptoService.getUserKey(mockUserId); - - expect(setKeySpy).toHaveBeenCalledWith(expect.any(SymmetricCryptoKey), mockUserId); - expect(setKeySpy).toHaveBeenCalledTimes(1); - - expect(userKey.keyB64).toEqual(autoKeyB64); - }); - - it("returns nullish if there is no auto key and the user key is not set", async () => { + it("returns nullish if the user key is not set", async () => { const userKey = await cryptoService.getUserKey(mockUserId); expect(userKey).toBeFalsy(); @@ -147,17 +133,6 @@ describe("cryptoService", () => { }, ); - describe("hasUserKey", () => { - it.each([true, false])( - "returns %s when the user key is not in memory, but the auto key is set", - async (hasKey) => { - stateProvider.singleUser.getFake(mockUserId, USER_KEY).nextState(null); - cryptoService.hasUserKeyStored = jest.fn().mockResolvedValue(hasKey); - expect(await cryptoService.hasUserKey(mockUserId)).toBe(hasKey); - }, - ); - }); - describe("getUserKeyWithLegacySupport", () => { let mockUserKey: UserKey; let mockMasterKey: MasterKey; diff --git a/libs/common/src/platform/services/crypto.service.ts b/libs/common/src/platform/services/crypto.service.ts index c091b6a5a9..3cd443c073 100644 --- a/libs/common/src/platform/services/crypto.service.ts +++ b/libs/common/src/platform/services/crypto.service.ts @@ -164,19 +164,8 @@ export class CryptoService implements CryptoServiceAbstraction { } async getUserKey(userId?: UserId): Promise<UserKey> { - let userKey = await firstValueFrom(this.stateProvider.getUserState$(USER_KEY, userId)); - if (userKey) { - return userKey; - } - - // If the user has set their vault timeout to 'Never', we can load the user key from storage - if (await this.hasUserKeyStored(KeySuffixOptions.Auto, userId)) { - userKey = await this.getKeyFromStorage(KeySuffixOptions.Auto, userId); - if (userKey) { - await this.setUserKey(userKey, userId); - return userKey; - } - } + const userKey = await firstValueFrom(this.stateProvider.getUserState$(USER_KEY, userId)); + return userKey; } async isLegacyUser(masterKey?: MasterKey, userId?: UserId): Promise<boolean> { @@ -217,10 +206,7 @@ export class CryptoService implements CryptoServiceAbstraction { if (userId == null) { return false; } - return ( - (await this.hasUserKeyInMemory(userId)) || - (await this.hasUserKeyStored(KeySuffixOptions.Auto, userId)) - ); + return await this.hasUserKeyInMemory(userId); } async hasUserKeyInMemory(userId?: UserId): Promise<boolean> { diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index 1fb2d71670..f660cd7a34 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -634,24 +634,6 @@ export class StateService< ); } - async getEverBeenUnlocked(options?: StorageOptions): Promise<boolean> { - return ( - (await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))) - ?.profile?.everBeenUnlocked ?? false - ); - } - - async setEverBeenUnlocked(value: boolean, options?: StorageOptions): Promise<void> { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - account.profile.everBeenUnlocked = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - } - async getIsAuthenticated(options?: StorageOptions): Promise<boolean> { return ( (await this.tokenService.getAccessToken(options?.userId as UserId)) != null && diff --git a/libs/common/src/platform/services/user-key-init.service.spec.ts b/libs/common/src/platform/services/user-key-init.service.spec.ts new file mode 100644 index 0000000000..567320ded6 --- /dev/null +++ b/libs/common/src/platform/services/user-key-init.service.spec.ts @@ -0,0 +1,162 @@ +import { mock } from "jest-mock-extended"; + +import { FakeAccountService, mockAccountServiceWith } from "../../../spec"; +import { CsprngArray } from "../../types/csprng"; +import { UserId } from "../../types/guid"; +import { UserKey } from "../../types/key"; +import { LogService } from "../abstractions/log.service"; +import { KeySuffixOptions } from "../enums"; +import { Utils } from "../misc/utils"; +import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; + +import { CryptoService } from "./crypto.service"; +import { UserKeyInitService } from "./user-key-init.service"; + +describe("UserKeyInitService", () => { + let userKeyInitService: UserKeyInitService; + + const mockUserId = Utils.newGuid() as UserId; + + const accountService: FakeAccountService = mockAccountServiceWith(mockUserId); + + const cryptoService = mock<CryptoService>(); + const logService = mock<LogService>(); + + beforeEach(() => { + userKeyInitService = new UserKeyInitService(accountService, cryptoService, logService); + }); + + describe("listenForActiveUserChangesToSetUserKey()", () => { + it("calls setUserKeyInMemoryIfAutoUserKeySet if there is an active user", () => { + // Arrange + accountService.activeAccountSubject.next({ + id: mockUserId, + name: "name", + email: "email", + }); + + (userKeyInitService as any).setUserKeyInMemoryIfAutoUserKeySet = jest.fn(); + + // Act + + const subscription = userKeyInitService.listenForActiveUserChangesToSetUserKey(); + + // Assert + + expect(subscription).not.toBeFalsy(); + + expect((userKeyInitService as any).setUserKeyInMemoryIfAutoUserKeySet).toHaveBeenCalledWith( + mockUserId, + ); + }); + + it("calls setUserKeyInMemoryIfAutoUserKeySet if there is an active user and tracks subsequent emissions", () => { + // Arrange + accountService.activeAccountSubject.next({ + id: mockUserId, + name: "name", + email: "email", + }); + + const mockUser2Id = Utils.newGuid() as UserId; + + jest + .spyOn(userKeyInitService as any, "setUserKeyInMemoryIfAutoUserKeySet") + .mockImplementation(() => Promise.resolve()); + + // Act + + const subscription = userKeyInitService.listenForActiveUserChangesToSetUserKey(); + + accountService.activeAccountSubject.next({ + id: mockUser2Id, + name: "name", + email: "email", + }); + + // Assert + + expect(subscription).not.toBeFalsy(); + + expect((userKeyInitService as any).setUserKeyInMemoryIfAutoUserKeySet).toHaveBeenCalledTimes( + 2, + ); + + expect( + (userKeyInitService as any).setUserKeyInMemoryIfAutoUserKeySet, + ).toHaveBeenNthCalledWith(1, mockUserId); + expect( + (userKeyInitService as any).setUserKeyInMemoryIfAutoUserKeySet, + ).toHaveBeenNthCalledWith(2, mockUser2Id); + + subscription.unsubscribe(); + }); + + it("does not call setUserKeyInMemoryIfAutoUserKeySet if there is not an active user", () => { + // Arrange + accountService.activeAccountSubject.next(null); + + (userKeyInitService as any).setUserKeyInMemoryIfAutoUserKeySet = jest.fn(); + + // Act + + const subscription = userKeyInitService.listenForActiveUserChangesToSetUserKey(); + + // Assert + + expect(subscription).not.toBeFalsy(); + + expect( + (userKeyInitService as any).setUserKeyInMemoryIfAutoUserKeySet, + ).not.toHaveBeenCalledWith(mockUserId); + }); + }); + + describe("setUserKeyInMemoryIfAutoUserKeySet", () => { + it("does nothing if the userId is null", async () => { + // Act + await (userKeyInitService as any).setUserKeyInMemoryIfAutoUserKeySet(null); + + // Assert + expect(cryptoService.getUserKeyFromStorage).not.toHaveBeenCalled(); + expect(cryptoService.setUserKey).not.toHaveBeenCalled(); + }); + + it("does nothing if the autoUserKey is null", async () => { + // Arrange + const userId = mockUserId; + + cryptoService.getUserKeyFromStorage.mockResolvedValue(null); + + // Act + await (userKeyInitService as any).setUserKeyInMemoryIfAutoUserKeySet(userId); + + // Assert + expect(cryptoService.getUserKeyFromStorage).toHaveBeenCalledWith( + KeySuffixOptions.Auto, + userId, + ); + expect(cryptoService.setUserKey).not.toHaveBeenCalled(); + }); + + it("sets the user key in memory if the autoUserKey is not null", async () => { + // Arrange + const userId = mockUserId; + + const mockRandomBytes = new Uint8Array(64) as CsprngArray; + const mockAutoUserKey: UserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey; + + cryptoService.getUserKeyFromStorage.mockResolvedValue(mockAutoUserKey); + + // Act + await (userKeyInitService as any).setUserKeyInMemoryIfAutoUserKeySet(userId); + + // Assert + expect(cryptoService.getUserKeyFromStorage).toHaveBeenCalledWith( + KeySuffixOptions.Auto, + userId, + ); + expect(cryptoService.setUserKey).toHaveBeenCalledWith(mockAutoUserKey, userId); + }); + }); +}); diff --git a/libs/common/src/platform/services/user-key-init.service.ts b/libs/common/src/platform/services/user-key-init.service.ts new file mode 100644 index 0000000000..1f6aacce8f --- /dev/null +++ b/libs/common/src/platform/services/user-key-init.service.ts @@ -0,0 +1,57 @@ +import { EMPTY, Subscription, catchError, filter, from, switchMap } from "rxjs"; + +import { AccountService } from "../../auth/abstractions/account.service"; +import { UserId } from "../../types/guid"; +import { CryptoService } from "../abstractions/crypto.service"; +import { LogService } from "../abstractions/log.service"; +import { KeySuffixOptions } from "../enums"; + +// TODO: this is a half measure improvement which allows us to reduce some side effects today (cryptoService.getUserKey setting user key in memory if auto key exists) +// but ideally, in the future, we would be able to put this logic into the cryptoService +// after the vault timeout settings service is transitioned to state provider so that +// the getUserKey logic can simply go to the correct location based on the vault timeout settings +// similar to the TokenService (it would either go to secure storage for the auto user key or memory for the user key) + +export class UserKeyInitService { + constructor( + private accountService: AccountService, + private cryptoService: CryptoService, + private logService: LogService, + ) {} + + // Note: must listen for changes to support account switching + listenForActiveUserChangesToSetUserKey(): Subscription { + return this.accountService.activeAccount$ + .pipe( + filter((activeAccount) => activeAccount != null), + switchMap((activeAccount) => + from(this.setUserKeyInMemoryIfAutoUserKeySet(activeAccount?.id)).pipe( + catchError((err: unknown) => { + this.logService.warning( + `setUserKeyInMemoryIfAutoUserKeySet failed with error: ${err}`, + ); + // Returning EMPTY to protect observable chain from cancellation in case of error + return EMPTY; + }), + ), + ), + ) + .subscribe(); + } + + private async setUserKeyInMemoryIfAutoUserKeySet(userId: UserId) { + if (userId == null) { + return; + } + + const autoUserKey = await this.cryptoService.getUserKeyFromStorage( + KeySuffixOptions.Auto, + userId, + ); + if (autoUserKey == null) { + return; + } + + await this.cryptoService.setUserKey(autoUserKey, userId); + } +} 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 a8afc63297..4fac3be9c9 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 @@ -172,7 +172,6 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA } async clear(userId?: string): Promise<void> { - await this.stateService.setEverBeenUnlocked(false, { userId: userId }); await this.cryptoService.clearPinKeys(userId); } 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 243b644dd8..5344093a25 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 @@ -174,7 +174,6 @@ describe("VaultTimeoutService", () => { // This does NOT assert all the things that the lock process does expect(stateService.getIsAuthenticated).toHaveBeenCalledWith({ userId: userId }); expect(vaultTimeoutSettingsService.availableVaultTimeoutActions$).toHaveBeenCalledWith(userId); - expect(stateService.setEverBeenUnlocked).toHaveBeenCalledWith(true, { userId: userId }); expect(stateService.setUserKeyAutoUnlock).toHaveBeenCalledWith(null, { userId: userId }); expect(masterPasswordService.mock.clearMasterKey).toHaveBeenCalledWith(userId); expect(cipherService.clearCache).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 35faf0fcee..8baf6c04c4 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout.service.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout.service.ts @@ -98,7 +98,6 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { 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 }); From 1e67014158267477adb8cb563be49189cb7624be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=A8=20Audrey=20=E2=9C=A8?= <ajensen@bitwarden.com> Date: Fri, 19 Apr 2024 13:12:17 -0400 Subject: [PATCH 231/351] fix update loop when overwriting state from buffer (#8834) --- .../state/buffered-key-definition.ts | 6 +- .../generator/state/buffered-state.spec.ts | 26 +++--- .../tools/generator/state/buffered-state.ts | 87 ++++++++----------- 3 files changed, 57 insertions(+), 62 deletions(-) diff --git a/libs/common/src/tools/generator/state/buffered-key-definition.ts b/libs/common/src/tools/generator/state/buffered-key-definition.ts index 5457410f80..1f11280839 100644 --- a/libs/common/src/tools/generator/state/buffered-key-definition.ts +++ b/libs/common/src/tools/generator/state/buffered-key-definition.ts @@ -87,9 +87,13 @@ export class BufferedKeyDefinition<Input, Output = Input, Dependency = true> { } /** Checks whether the input type can be converted to the output type. - * @returns `true` if the definition is valid, otherwise `false`. + * @returns `true` if the definition is defined and valid, otherwise `false`. */ isValid(input: Input, dependency: Dependency) { + if (input === null) { + return Promise.resolve(false); + } + const isValid = this.options?.isValid; if (isValid) { return isValid(input, dependency); diff --git a/libs/common/src/tools/generator/state/buffered-state.spec.ts b/libs/common/src/tools/generator/state/buffered-state.spec.ts index 7f9722d384..46e132c1bd 100644 --- a/libs/common/src/tools/generator/state/buffered-state.spec.ts +++ b/libs/common/src/tools/generator/state/buffered-state.spec.ts @@ -75,14 +75,16 @@ describe("BufferedState", () => { 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 initialValue = { foo: true, bar: false }; + await outputState.update(() => initialValue); 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$); + const result = await trackEmissions(bufferedState.state$); + await awaitAsync(); - expect(result).toEqual(bufferedValue); + expect(result).toEqual([initialValue, bufferedValue]); }); // also important for data migrations @@ -131,14 +133,16 @@ describe("BufferedState", () => { }); const provider = new FakeStateProvider(accountService); const outputState = provider.getUser(SomeUser, SOME_KEY); - await outputState.update(() => ({ foo: true, bar: false })); + const initialValue = { foo: true, bar: false }; + await outputState.update(() => initialValue); 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$); + const result = await trackEmissions(bufferedState.state$); + await awaitAsync(); - expect(result).toEqual(bufferedValue); + expect(result).toEqual([initialValue, bufferedValue]); }); it("reads from the output state when shouldOverwrite returns a falsy value", async () => { @@ -274,7 +278,7 @@ describe("BufferedState", () => { await bufferedState.buffer(bufferedValue); await awaitAsync(); - expect(result).toEqual([firstValue, firstValue]); + expect(result).toEqual([firstValue]); }); it("replaces the output state when its dependency becomes true", async () => { @@ -296,7 +300,7 @@ describe("BufferedState", () => { dependency.next(true); await awaitAsync(); - expect(result).toEqual([firstValue, firstValue, bufferedValue]); + expect(result).toEqual([firstValue, bufferedValue]); }); it.each([[null], [undefined]])("ignores `%p`", async (bufferedValue) => { @@ -325,11 +329,13 @@ describe("BufferedState", () => { await outputState.update(() => firstValue); const bufferedState = new BufferedState(provider, bufferedKey, outputState); - const result = trackEmissions(bufferedState.state$); + const stateResult = trackEmissions(bufferedState.state$); await bufferedState.buffer({ foo: true, bar: true }); await awaitAsync(); + const bufferedResult = await firstValueFrom(bufferedState.bufferedState$); - expect(result).toEqual([firstValue, firstValue]); + expect(stateResult).toEqual([firstValue]); + expect(bufferedResult).toBeNull(); }); it("overwrites the output when isValid returns true", async () => { diff --git a/libs/common/src/tools/generator/state/buffered-state.ts b/libs/common/src/tools/generator/state/buffered-state.ts index 42b14b815c..bb4de645e9 100644 --- a/libs/common/src/tools/generator/state/buffered-state.ts +++ b/libs/common/src/tools/generator/state/buffered-state.ts @@ -1,4 +1,4 @@ -import { Observable, combineLatest, concatMap, filter, map, of } from "rxjs"; +import { Observable, combineLatest, concatMap, filter, map, of, concat, merge } from "rxjs"; import { StateProvider, @@ -33,68 +33,53 @@ export class BufferedState<Input, Output, Dependency> implements SingleUserState private output: SingleUserState<Output>, dependency$: Observable<Dependency> = null, ) { - this.bufferState = provider.getUser(output.userId, key.toKeyDefinition()); + this.bufferedState = 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; + // overwrite the output value + const hasValue$ = concat(of(null), this.bufferedState.state$).pipe( + map((buffer) => (buffer ?? null) !== null), + ); + const overwriteDependency$ = (dependency$ ?? of(true as unknown as Dependency)).pipe( + map((dependency) => [key.shouldOverwrite(dependency), dependency] as const), + ); + const overwrite$ = combineLatest([hasValue$, overwriteDependency$]).pipe( + concatMap(async ([hasValue, [shouldOverwrite, dependency]]) => { + if (hasValue && shouldOverwrite) { + await this.overwriteOutput(dependency); } - - return [true, output] as const; + return [false, null] as const; }), - filter(([updated]) => updated), + ); + + // drive overwrites only when there's a subscription; + // the output state determines when emissions occur + const output$ = this.output.state$.pipe(map((output) => [true, output] as const)); + this.state$ = merge(overwrite$, output$).pipe( + filter(([emit]) => emit), map(([, output]) => output), ); this.combinedState$ = this.state$.pipe(map((state) => [this.output.userId, state])); - this.bufferState$ = this.bufferState.state$; + this.bufferedState$ = this.bufferedState.state$; } - private bufferState: SingleUserState<Input>; + private bufferedState: SingleUserState<Input>; - 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; - }, + private async overwriteOutput(dependency: Dependency) { + // take the latest value from the buffer + let buffered: Input; + await this.bufferedState.update((state) => { + buffered = state ?? null; + return null; }); - // bail if this update lost the race with the last update - if (input === null) { - return; + // update the output state + const isValid = await this.key.isValid(buffered, dependency); + if (isValid) { + const output = await this.key.map(buffered, dependency); + await this.output.update(() => output); } - - // 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} */ @@ -119,14 +104,14 @@ export class BufferedState<Input, Output, Dependency> implements SingleUserState async buffer(value: Input): Promise<void> { const normalized = value ?? null; if (normalized !== null) { - await this.bufferState.update(() => normalized); + await this.bufferedState.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<Input>; + readonly bufferedState$: Observable<Input>; /** Updates the output state. * @param configureState a callback that returns an updated output From ddee74fdee4d79541bd4404a72ab25efa5cb51d2 Mon Sep 17 00:00:00 2001 From: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com> Date: Fri, 19 Apr 2024 13:15:30 -0400 Subject: [PATCH 232/351] Removed 2023 plans for view for grandfathered 2020 providers (#8804) --- .../organizations/organization-plans.component.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) 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 30691ce87d..645b1f29ac 100644 --- a/apps/web/src/app/billing/organizations/organization-plans.component.ts +++ b/apps/web/src/app/billing/organizations/organization-plans.component.ts @@ -48,11 +48,7 @@ interface OnSuccessArgs { organizationId: string; } -const AllowedLegacyPlanTypes = [ - PlanType.TeamsMonthly2023, - PlanType.TeamsAnnually2023, - PlanType.EnterpriseAnnually2023, - PlanType.EnterpriseMonthly2023, +const Allowed2020PlansForLegacyProviders = [ PlanType.TeamsMonthly2020, PlanType.TeamsAnnually2020, PlanType.EnterpriseAnnually2020, @@ -283,7 +279,8 @@ 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() && AllowedLegacyPlanTypes.includes(plan.type))), + (this.isProviderQualifiedFor2020Plan() && + Allowed2020PlansForLegacyProviders.includes(plan.type))), ); result.sort((planA, planB) => planA.displaySortOrder - planB.displaySortOrder); @@ -298,7 +295,8 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { (plan) => plan.product === selectedProductType && ((!this.isProviderQualifiedFor2020Plan() && this.planIsEnabled(plan)) || - (this.isProviderQualifiedFor2020Plan() && AllowedLegacyPlanTypes.includes(plan.type))), + (this.isProviderQualifiedFor2020Plan() && + Allowed2020PlansForLegacyProviders.includes(plan.type))), ) || []; result.sort((planA, planB) => planA.displaySortOrder - planB.displaySortOrder); From d55d240b1886c7a6b26e65a09468e8c3fe7f6d8f Mon Sep 17 00:00:00 2001 From: Matt Gibson <mgibson@bitwarden.com> Date: Fri, 19 Apr 2024 13:23:42 -0400 Subject: [PATCH 233/351] Update host permission to all urls (#8831) Discussions on this permission here: https://github.com/bitwarden/clients/pull/5985 --- apps/browser/src/manifest.v3.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index 6c58b405f4..cdd0869fc5 100644 --- a/apps/browser/src/manifest.v3.json +++ b/apps/browser/src/manifest.v3.json @@ -64,7 +64,7 @@ "offscreen" ], "optional_permissions": ["nativeMessaging", "privacy"], - "host_permissions": ["*://*/*"], + "host_permissions": ["<all_urls>"], "content_security_policy": { "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'", "sandbox": "sandbox allow-scripts; script-src 'self'" From c1bbf675e2545173704be7e6cd1d6a3b57dc985e Mon Sep 17 00:00:00 2001 From: Joseph Flinn <58369717+joseph-flinn@users.noreply.github.com> Date: Fri, 19 Apr 2024 10:29:50 -0700 Subject: [PATCH 234/351] Update number of translations and give credit to our translators (#8835) --- apps/browser/store/locales/en/copy.resx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/browser/store/locales/en/copy.resx b/apps/browser/store/locales/en/copy.resx index df8d63835c..82e4eb1d88 100644 --- a/apps/browser/store/locales/en/copy.resx +++ b/apps/browser/store/locales/en/copy.resx @@ -159,7 +159,7 @@ Built-in Generator Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. Global Translations -Bitwarden translations exist for more than 50 languages. +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. From 9a4279c8bbb50a9c68503621d0b61811afdc2716 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 19 Apr 2024 17:41:41 +0000 Subject: [PATCH 235/351] Autosync the updated translations (#8836) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/desktop/src/locales/af/messages.json | 3 --- apps/desktop/src/locales/ar/messages.json | 3 --- apps/desktop/src/locales/az/messages.json | 3 --- apps/desktop/src/locales/be/messages.json | 3 --- apps/desktop/src/locales/bg/messages.json | 7 ++----- apps/desktop/src/locales/bn/messages.json | 3 --- apps/desktop/src/locales/bs/messages.json | 3 --- apps/desktop/src/locales/ca/messages.json | 3 --- apps/desktop/src/locales/cs/messages.json | 3 --- apps/desktop/src/locales/cy/messages.json | 3 --- apps/desktop/src/locales/da/messages.json | 3 --- apps/desktop/src/locales/de/messages.json | 3 --- apps/desktop/src/locales/el/messages.json | 3 --- apps/desktop/src/locales/en_GB/messages.json | 3 --- apps/desktop/src/locales/en_IN/messages.json | 3 --- apps/desktop/src/locales/eo/messages.json | 3 --- apps/desktop/src/locales/es/messages.json | 3 --- apps/desktop/src/locales/et/messages.json | 3 --- apps/desktop/src/locales/eu/messages.json | 3 --- apps/desktop/src/locales/fa/messages.json | 3 --- apps/desktop/src/locales/fi/messages.json | 11 ++++------- apps/desktop/src/locales/fil/messages.json | 3 --- apps/desktop/src/locales/fr/messages.json | 3 --- apps/desktop/src/locales/gl/messages.json | 3 --- apps/desktop/src/locales/he/messages.json | 3 --- apps/desktop/src/locales/hi/messages.json | 3 --- apps/desktop/src/locales/hr/messages.json | 3 --- apps/desktop/src/locales/hu/messages.json | 7 ++----- apps/desktop/src/locales/id/messages.json | 13 +++++-------- apps/desktop/src/locales/it/messages.json | 7 ++----- apps/desktop/src/locales/ja/messages.json | 3 --- apps/desktop/src/locales/ka/messages.json | 3 --- apps/desktop/src/locales/km/messages.json | 3 --- apps/desktop/src/locales/kn/messages.json | 3 --- apps/desktop/src/locales/ko/messages.json | 3 --- apps/desktop/src/locales/lt/messages.json | 3 --- apps/desktop/src/locales/lv/messages.json | 7 ++----- apps/desktop/src/locales/me/messages.json | 3 --- apps/desktop/src/locales/ml/messages.json | 3 --- apps/desktop/src/locales/mr/messages.json | 3 --- apps/desktop/src/locales/my/messages.json | 3 --- apps/desktop/src/locales/nb/messages.json | 3 --- apps/desktop/src/locales/ne/messages.json | 3 --- apps/desktop/src/locales/nl/messages.json | 3 --- apps/desktop/src/locales/nn/messages.json | 3 --- apps/desktop/src/locales/or/messages.json | 3 --- apps/desktop/src/locales/pl/messages.json | 3 --- apps/desktop/src/locales/pt_BR/messages.json | 3 --- apps/desktop/src/locales/pt_PT/messages.json | 3 --- apps/desktop/src/locales/ro/messages.json | 3 --- apps/desktop/src/locales/ru/messages.json | 3 --- apps/desktop/src/locales/si/messages.json | 3 --- apps/desktop/src/locales/sk/messages.json | 3 --- apps/desktop/src/locales/sl/messages.json | 3 --- apps/desktop/src/locales/sr/messages.json | 3 --- apps/desktop/src/locales/sv/messages.json | 3 --- apps/desktop/src/locales/te/messages.json | 3 --- apps/desktop/src/locales/th/messages.json | 3 --- apps/desktop/src/locales/tr/messages.json | 3 --- apps/desktop/src/locales/uk/messages.json | 3 --- apps/desktop/src/locales/vi/messages.json | 3 --- apps/desktop/src/locales/zh_CN/messages.json | 11 ++++------- apps/desktop/src/locales/zh_TW/messages.json | 3 --- 63 files changed, 21 insertions(+), 210 deletions(-) diff --git a/apps/desktop/src/locales/af/messages.json b/apps/desktop/src/locales/af/messages.json index 97067b788a..afdfc90d76 100644 --- a/apps/desktop/src/locales/af/messages.json +++ b/apps/desktop/src/locales/af/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/ar/messages.json b/apps/desktop/src/locales/ar/messages.json index 2d25269fff..7869b0894b 100644 --- a/apps/desktop/src/locales/ar/messages.json +++ b/apps/desktop/src/locales/ar/messages.json @@ -2697,9 +2697,6 @@ "message": "تنسيقات مشتركة", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/az/messages.json b/apps/desktop/src/locales/az/messages.json index 1ecd18eee7..d4cea4f06e 100644 --- a/apps/desktop/src/locales/az/messages.json +++ b/apps/desktop/src/locales/az/messages.json @@ -2697,9 +2697,6 @@ "message": "Ortaq formatlar", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Uğurlu" - }, "troubleshooting": { "message": "Problemlərin aradan qaldırılması" }, diff --git a/apps/desktop/src/locales/be/messages.json b/apps/desktop/src/locales/be/messages.json index e0133e5a74..53e3ec2d12 100644 --- a/apps/desktop/src/locales/be/messages.json +++ b/apps/desktop/src/locales/be/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/bg/messages.json b/apps/desktop/src/locales/bg/messages.json index 20f47d4bcd..f4886c420f 100644 --- a/apps/desktop/src/locales/bg/messages.json +++ b/apps/desktop/src/locales/bg/messages.json @@ -1633,10 +1633,10 @@ "message": "Интеграцията с браузър не се поддържа" }, "browserIntegrationErrorTitle": { - "message": "Error enabling browser integration" + "message": "Грешка при включването на интеграцията с браузъра" }, "browserIntegrationErrorDesc": { - "message": "An error has occurred while enabling browser integration." + "message": "Възникна грешка при включването на интеграцията с браузъра." }, "browserIntegrationMasOnlyDesc": { "message": "За жалост в момента интеграцията с браузър не се поддържа във версията за магазина на Mac." @@ -2697,9 +2697,6 @@ "message": "Често използвани формати", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Отстраняване на проблеми" }, diff --git a/apps/desktop/src/locales/bn/messages.json b/apps/desktop/src/locales/bn/messages.json index 626734ebff..abd2c1cfae 100644 --- a/apps/desktop/src/locales/bn/messages.json +++ b/apps/desktop/src/locales/bn/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/bs/messages.json b/apps/desktop/src/locales/bs/messages.json index 9d5685cca9..825bd6344e 100644 --- a/apps/desktop/src/locales/bs/messages.json +++ b/apps/desktop/src/locales/bs/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/ca/messages.json b/apps/desktop/src/locales/ca/messages.json index d8c0f32948..6c48d6cb0b 100644 --- a/apps/desktop/src/locales/ca/messages.json +++ b/apps/desktop/src/locales/ca/messages.json @@ -2697,9 +2697,6 @@ "message": "Formats comuns", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Resolució de problemes" }, diff --git a/apps/desktop/src/locales/cs/messages.json b/apps/desktop/src/locales/cs/messages.json index e68fe8fffc..550b10a31c 100644 --- a/apps/desktop/src/locales/cs/messages.json +++ b/apps/desktop/src/locales/cs/messages.json @@ -2697,9 +2697,6 @@ "message": "Společné formáty", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Úspěch" - }, "troubleshooting": { "message": "Řešení problémů" }, diff --git a/apps/desktop/src/locales/cy/messages.json b/apps/desktop/src/locales/cy/messages.json index 62f2e608bb..b1cc9e63d3 100644 --- a/apps/desktop/src/locales/cy/messages.json +++ b/apps/desktop/src/locales/cy/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/da/messages.json b/apps/desktop/src/locales/da/messages.json index 0e578a6f66..f2a84a3c29 100644 --- a/apps/desktop/src/locales/da/messages.json +++ b/apps/desktop/src/locales/da/messages.json @@ -2697,9 +2697,6 @@ "message": "Almindelige formater", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Gennemført" - }, "troubleshooting": { "message": "Fejlfinding" }, diff --git a/apps/desktop/src/locales/de/messages.json b/apps/desktop/src/locales/de/messages.json index bba1ccec15..e5e3945abc 100644 --- a/apps/desktop/src/locales/de/messages.json +++ b/apps/desktop/src/locales/de/messages.json @@ -2697,9 +2697,6 @@ "message": "Gängigste Formate", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Problembehandlung" }, diff --git a/apps/desktop/src/locales/el/messages.json b/apps/desktop/src/locales/el/messages.json index 87360c33ce..41e6a62a2f 100644 --- a/apps/desktop/src/locales/el/messages.json +++ b/apps/desktop/src/locales/el/messages.json @@ -2697,9 +2697,6 @@ "message": "Κοινές μορφές", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Αντιμετώπιση Προβλημάτων" }, diff --git a/apps/desktop/src/locales/en_GB/messages.json b/apps/desktop/src/locales/en_GB/messages.json index 5c8c32b7c1..2658610df3 100644 --- a/apps/desktop/src/locales/en_GB/messages.json +++ b/apps/desktop/src/locales/en_GB/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/en_IN/messages.json b/apps/desktop/src/locales/en_IN/messages.json index abfa0b1c0d..0542da9ddc 100644 --- a/apps/desktop/src/locales/en_IN/messages.json +++ b/apps/desktop/src/locales/en_IN/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/eo/messages.json b/apps/desktop/src/locales/eo/messages.json index 427f08f805..1c4cc4f0be 100644 --- a/apps/desktop/src/locales/eo/messages.json +++ b/apps/desktop/src/locales/eo/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/es/messages.json b/apps/desktop/src/locales/es/messages.json index f7df93bdd7..ec5da44293 100644 --- a/apps/desktop/src/locales/es/messages.json +++ b/apps/desktop/src/locales/es/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/et/messages.json b/apps/desktop/src/locales/et/messages.json index 02cd737baa..3850cc1d85 100644 --- a/apps/desktop/src/locales/et/messages.json +++ b/apps/desktop/src/locales/et/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/eu/messages.json b/apps/desktop/src/locales/eu/messages.json index 2067b2dcc2..b21108b6ad 100644 --- a/apps/desktop/src/locales/eu/messages.json +++ b/apps/desktop/src/locales/eu/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/fa/messages.json b/apps/desktop/src/locales/fa/messages.json index ef34f8222a..08356d410d 100644 --- a/apps/desktop/src/locales/fa/messages.json +++ b/apps/desktop/src/locales/fa/messages.json @@ -2697,9 +2697,6 @@ "message": "فرمت‌های رایج", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/fi/messages.json b/apps/desktop/src/locales/fi/messages.json index 03059f7ef3..fca24c197a 100644 --- a/apps/desktop/src/locales/fi/messages.json +++ b/apps/desktop/src/locales/fi/messages.json @@ -1633,10 +1633,10 @@ "message": "Selainintegraatiota ei tueta" }, "browserIntegrationErrorTitle": { - "message": "Error enabling browser integration" + "message": "Virhe otettaessa selainintegrointia käyttöön" }, "browserIntegrationErrorDesc": { - "message": "An error has occurred while enabling browser integration." + "message": "Otettaessa selainintegraatiota käyttöön tapahtui virhe." }, "browserIntegrationMasOnlyDesc": { "message": "Valitettavasti selainintegraatiota tuetaan toistaiseksi vain Mac App Store -versiossa." @@ -2697,9 +2697,6 @@ "message": "Yleiset muodot", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Vianetsintä" }, @@ -2716,9 +2713,9 @@ "message": "Suojausavain poistettiin" }, "errorAssigningTargetCollection": { - "message": "Error assigning target collection." + "message": "Virhe määritettäessä kohdekokoelmaa." }, "errorAssigningTargetFolder": { - "message": "Error assigning target folder." + "message": "Virhe määritettäessä kohdekansiota." } } diff --git a/apps/desktop/src/locales/fil/messages.json b/apps/desktop/src/locales/fil/messages.json index d28a4b568c..6d5f85fca8 100644 --- a/apps/desktop/src/locales/fil/messages.json +++ b/apps/desktop/src/locales/fil/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/fr/messages.json b/apps/desktop/src/locales/fr/messages.json index 86550b736f..1097624b14 100644 --- a/apps/desktop/src/locales/fr/messages.json +++ b/apps/desktop/src/locales/fr/messages.json @@ -2697,9 +2697,6 @@ "message": "Formats communs", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Résolution de problèmes" }, diff --git a/apps/desktop/src/locales/gl/messages.json b/apps/desktop/src/locales/gl/messages.json index 889a2beeee..90648699c0 100644 --- a/apps/desktop/src/locales/gl/messages.json +++ b/apps/desktop/src/locales/gl/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/he/messages.json b/apps/desktop/src/locales/he/messages.json index 3b155ffdf3..73599c012e 100644 --- a/apps/desktop/src/locales/he/messages.json +++ b/apps/desktop/src/locales/he/messages.json @@ -2697,9 +2697,6 @@ "message": "תסדירים נפוצים", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/hi/messages.json b/apps/desktop/src/locales/hi/messages.json index af28c66681..4f8bb9b4bb 100644 --- a/apps/desktop/src/locales/hi/messages.json +++ b/apps/desktop/src/locales/hi/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/hr/messages.json b/apps/desktop/src/locales/hr/messages.json index 01983d5891..220c8bfab2 100644 --- a/apps/desktop/src/locales/hr/messages.json +++ b/apps/desktop/src/locales/hr/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/hu/messages.json b/apps/desktop/src/locales/hu/messages.json index ecf77e2f34..149d48284e 100644 --- a/apps/desktop/src/locales/hu/messages.json +++ b/apps/desktop/src/locales/hu/messages.json @@ -1633,10 +1633,10 @@ "message": "A böngésző integráció nem támogatott." }, "browserIntegrationErrorTitle": { - "message": "Error enabling browser integration" + "message": "Böngésző integráció engedélyezése" }, "browserIntegrationErrorDesc": { - "message": "An error has occurred while enabling browser integration." + "message": "Hiba történt a böngésző integrációjának engedélyezése közben." }, "browserIntegrationMasOnlyDesc": { "message": "Sajnos a böngésző integrációt egyelőre csak a Mac App Store verzió támogatja." @@ -2697,9 +2697,6 @@ "message": "Általános formátumok", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Hibaelhárítás" }, diff --git a/apps/desktop/src/locales/id/messages.json b/apps/desktop/src/locales/id/messages.json index cd36126b05..3194b0f7d3 100644 --- a/apps/desktop/src/locales/id/messages.json +++ b/apps/desktop/src/locales/id/messages.json @@ -404,7 +404,7 @@ "message": "Panjang" }, "passwordMinLength": { - "message": "Minimum password length" + "message": "Panjang kata sandi minimum" }, "uppercase": { "message": "Huruf Kapital (A-Z)" @@ -545,7 +545,7 @@ "message": "Diperlukan pengetikan ulang kata sandi utama." }, "masterPasswordMinlength": { - "message": "Master password must be at least $VALUE$ characters long.", + "message": "Kata sandi utama minimal harus $VALUE$ karakter.", "description": "The Master Password must be at least a specific number of characters long.", "placeholders": { "value": { @@ -561,7 +561,7 @@ "message": "Akun baru Anda telah dibuat! Sekarang Anda bisa masuk." }, "youSuccessfullyLoggedIn": { - "message": "You successfully logged in" + "message": "Anda berhasil masuk" }, "youMayCloseThisWindow": { "message": "You may close this window" @@ -801,10 +801,10 @@ "message": "Ubah Kata Sandi Utama" }, "continueToWebApp": { - "message": "Continue to web app?" + "message": "Lanjutkan ke aplikasi web?" }, "changeMasterPasswordOnWebConfirmation": { - "message": "You can change your master password on the Bitwarden web app." + "message": "Anda bisa mengganti kata sandi utama Anda di aplikasi web Bitwarden." }, "fingerprintPhrase": { "message": "Frase Fingerprint", @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/it/messages.json b/apps/desktop/src/locales/it/messages.json index 0eeea259e2..08ae2d9da8 100644 --- a/apps/desktop/src/locales/it/messages.json +++ b/apps/desktop/src/locales/it/messages.json @@ -1633,10 +1633,10 @@ "message": "L'integrazione del browser non è supportata" }, "browserIntegrationErrorTitle": { - "message": "Error enabling browser integration" + "message": "Errore durante l'attivazione dell'integrazione del browser" }, "browserIntegrationErrorDesc": { - "message": "An error has occurred while enabling browser integration." + "message": "Si è verificato un errore durante l'attivazione dell'integrazione del browser." }, "browserIntegrationMasOnlyDesc": { "message": "Purtroppo l'integrazione del browser è supportata solo nella versione nell'App Store per ora." @@ -2697,9 +2697,6 @@ "message": "Formati comuni", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Risoluzione problemi" }, diff --git a/apps/desktop/src/locales/ja/messages.json b/apps/desktop/src/locales/ja/messages.json index ab6c0be95f..b07eab20cc 100644 --- a/apps/desktop/src/locales/ja/messages.json +++ b/apps/desktop/src/locales/ja/messages.json @@ -2697,9 +2697,6 @@ "message": "一般的な形式", "description": "Label indicating the most common import formats" }, - "success": { - "message": "成功" - }, "troubleshooting": { "message": "トラブルシューティング" }, diff --git a/apps/desktop/src/locales/ka/messages.json b/apps/desktop/src/locales/ka/messages.json index 889a2beeee..90648699c0 100644 --- a/apps/desktop/src/locales/ka/messages.json +++ b/apps/desktop/src/locales/ka/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/km/messages.json b/apps/desktop/src/locales/km/messages.json index 889a2beeee..90648699c0 100644 --- a/apps/desktop/src/locales/km/messages.json +++ b/apps/desktop/src/locales/km/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/kn/messages.json b/apps/desktop/src/locales/kn/messages.json index eb0cbcf6be..162cba3a75 100644 --- a/apps/desktop/src/locales/kn/messages.json +++ b/apps/desktop/src/locales/kn/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/ko/messages.json b/apps/desktop/src/locales/ko/messages.json index 8e50ade96c..09b1767af0 100644 --- a/apps/desktop/src/locales/ko/messages.json +++ b/apps/desktop/src/locales/ko/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/lt/messages.json b/apps/desktop/src/locales/lt/messages.json index e9de697005..de77aa8fbf 100644 --- a/apps/desktop/src/locales/lt/messages.json +++ b/apps/desktop/src/locales/lt/messages.json @@ -2697,9 +2697,6 @@ "message": "Dažni formatai", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/lv/messages.json b/apps/desktop/src/locales/lv/messages.json index 0227a8c524..521a0afcf2 100644 --- a/apps/desktop/src/locales/lv/messages.json +++ b/apps/desktop/src/locales/lv/messages.json @@ -1633,10 +1633,10 @@ "message": "Sasaistīšana ar pārlūku nav atbalstīta" }, "browserIntegrationErrorTitle": { - "message": "Error enabling browser integration" + "message": "Kļūda pārlūga saistīšanas iespējošanā" }, "browserIntegrationErrorDesc": { - "message": "An error has occurred while enabling browser integration." + "message": "Atgadījās kļūda pārlūka saistīšanas iespējošanas laikā." }, "browserIntegrationMasOnlyDesc": { "message": "Diemžēl sasaistīšāna ar pārlūku pagaidām ir nodrošināta tikai Mac App Store laidienā." @@ -2697,9 +2697,6 @@ "message": "Izplatīti veidoli", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Sarežģījumu novēršana" }, diff --git a/apps/desktop/src/locales/me/messages.json b/apps/desktop/src/locales/me/messages.json index 1f49961b46..ed458379b8 100644 --- a/apps/desktop/src/locales/me/messages.json +++ b/apps/desktop/src/locales/me/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/ml/messages.json b/apps/desktop/src/locales/ml/messages.json index 96811b9dba..b94b9d1b79 100644 --- a/apps/desktop/src/locales/ml/messages.json +++ b/apps/desktop/src/locales/ml/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/mr/messages.json b/apps/desktop/src/locales/mr/messages.json index 889a2beeee..90648699c0 100644 --- a/apps/desktop/src/locales/mr/messages.json +++ b/apps/desktop/src/locales/mr/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/my/messages.json b/apps/desktop/src/locales/my/messages.json index 0ee0db69ef..5142c8e61f 100644 --- a/apps/desktop/src/locales/my/messages.json +++ b/apps/desktop/src/locales/my/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/nb/messages.json b/apps/desktop/src/locales/nb/messages.json index 7bf132bdac..e190cfc236 100644 --- a/apps/desktop/src/locales/nb/messages.json +++ b/apps/desktop/src/locales/nb/messages.json @@ -2697,9 +2697,6 @@ "message": "Vanlige formater", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/ne/messages.json b/apps/desktop/src/locales/ne/messages.json index 13e1466805..bd58a18b0d 100644 --- a/apps/desktop/src/locales/ne/messages.json +++ b/apps/desktop/src/locales/ne/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/nl/messages.json b/apps/desktop/src/locales/nl/messages.json index b5f2a413d6..3ca0730710 100644 --- a/apps/desktop/src/locales/nl/messages.json +++ b/apps/desktop/src/locales/nl/messages.json @@ -2697,9 +2697,6 @@ "message": "Veelvoorkomende formaten", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Probleemoplossing" }, diff --git a/apps/desktop/src/locales/nn/messages.json b/apps/desktop/src/locales/nn/messages.json index 35e7173d74..12e11b32c1 100644 --- a/apps/desktop/src/locales/nn/messages.json +++ b/apps/desktop/src/locales/nn/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/or/messages.json b/apps/desktop/src/locales/or/messages.json index cd83d2ea69..7363558551 100644 --- a/apps/desktop/src/locales/or/messages.json +++ b/apps/desktop/src/locales/or/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/pl/messages.json b/apps/desktop/src/locales/pl/messages.json index 250c557309..df7a158a3a 100644 --- a/apps/desktop/src/locales/pl/messages.json +++ b/apps/desktop/src/locales/pl/messages.json @@ -2697,9 +2697,6 @@ "message": "Popularne formaty", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Sukces" - }, "troubleshooting": { "message": "Rozwiązywanie problemów" }, diff --git a/apps/desktop/src/locales/pt_BR/messages.json b/apps/desktop/src/locales/pt_BR/messages.json index 9a79ad665e..f651e0b060 100644 --- a/apps/desktop/src/locales/pt_BR/messages.json +++ b/apps/desktop/src/locales/pt_BR/messages.json @@ -2697,9 +2697,6 @@ "message": "Formatos comuns", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/pt_PT/messages.json b/apps/desktop/src/locales/pt_PT/messages.json index 14f0ec5d2f..4e58dad2cf 100644 --- a/apps/desktop/src/locales/pt_PT/messages.json +++ b/apps/desktop/src/locales/pt_PT/messages.json @@ -2697,9 +2697,6 @@ "message": "Formatos comuns", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Com sucesso" - }, "troubleshooting": { "message": "Resolução de problemas" }, diff --git a/apps/desktop/src/locales/ro/messages.json b/apps/desktop/src/locales/ro/messages.json index 978f57eb9b..3fe73f28a7 100644 --- a/apps/desktop/src/locales/ro/messages.json +++ b/apps/desktop/src/locales/ro/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/ru/messages.json b/apps/desktop/src/locales/ru/messages.json index c9b3b95b39..cc182812a6 100644 --- a/apps/desktop/src/locales/ru/messages.json +++ b/apps/desktop/src/locales/ru/messages.json @@ -2697,9 +2697,6 @@ "message": "Основные форматы", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Успешно" - }, "troubleshooting": { "message": "Устранение проблем" }, diff --git a/apps/desktop/src/locales/si/messages.json b/apps/desktop/src/locales/si/messages.json index 3d43997144..261ae1c9b8 100644 --- a/apps/desktop/src/locales/si/messages.json +++ b/apps/desktop/src/locales/si/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/sk/messages.json b/apps/desktop/src/locales/sk/messages.json index 6499486b9d..6ef52a83ee 100644 --- a/apps/desktop/src/locales/sk/messages.json +++ b/apps/desktop/src/locales/sk/messages.json @@ -2697,9 +2697,6 @@ "message": "Bežné formáty", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Úspech" - }, "troubleshooting": { "message": "Riešenie problémov" }, diff --git a/apps/desktop/src/locales/sl/messages.json b/apps/desktop/src/locales/sl/messages.json index 8cb06dcf0c..8c9c158c87 100644 --- a/apps/desktop/src/locales/sl/messages.json +++ b/apps/desktop/src/locales/sl/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/sr/messages.json b/apps/desktop/src/locales/sr/messages.json index 37c5dfa382..edafdb55a2 100644 --- a/apps/desktop/src/locales/sr/messages.json +++ b/apps/desktop/src/locales/sr/messages.json @@ -2697,9 +2697,6 @@ "message": "Уобичајени формати", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Решавање проблема" }, diff --git a/apps/desktop/src/locales/sv/messages.json b/apps/desktop/src/locales/sv/messages.json index bd21c0f328..6342424fd4 100644 --- a/apps/desktop/src/locales/sv/messages.json +++ b/apps/desktop/src/locales/sv/messages.json @@ -2697,9 +2697,6 @@ "message": "Vanliga format", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Felsökning" }, diff --git a/apps/desktop/src/locales/te/messages.json b/apps/desktop/src/locales/te/messages.json index 889a2beeee..90648699c0 100644 --- a/apps/desktop/src/locales/te/messages.json +++ b/apps/desktop/src/locales/te/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/th/messages.json b/apps/desktop/src/locales/th/messages.json index f1cd5351f7..5b6a1b9d0b 100644 --- a/apps/desktop/src/locales/th/messages.json +++ b/apps/desktop/src/locales/th/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/tr/messages.json b/apps/desktop/src/locales/tr/messages.json index 3e7229c41b..42a8f207c7 100644 --- a/apps/desktop/src/locales/tr/messages.json +++ b/apps/desktop/src/locales/tr/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Sorun giderme" }, diff --git a/apps/desktop/src/locales/uk/messages.json b/apps/desktop/src/locales/uk/messages.json index 9ee7652093..ac7b7c3243 100644 --- a/apps/desktop/src/locales/uk/messages.json +++ b/apps/desktop/src/locales/uk/messages.json @@ -2697,9 +2697,6 @@ "message": "Поширені формати", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Успішно" - }, "troubleshooting": { "message": "Усунення проблем" }, diff --git a/apps/desktop/src/locales/vi/messages.json b/apps/desktop/src/locales/vi/messages.json index 0c0e6f6df7..aac9995db1 100644 --- a/apps/desktop/src/locales/vi/messages.json +++ b/apps/desktop/src/locales/vi/messages.json @@ -2697,9 +2697,6 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/zh_CN/messages.json b/apps/desktop/src/locales/zh_CN/messages.json index 3819cb967c..0560466cf8 100644 --- a/apps/desktop/src/locales/zh_CN/messages.json +++ b/apps/desktop/src/locales/zh_CN/messages.json @@ -1633,10 +1633,10 @@ "message": "不支持浏览器集成" }, "browserIntegrationErrorTitle": { - "message": "Error enabling browser integration" + "message": "启用浏览器集成时出错" }, "browserIntegrationErrorDesc": { - "message": "An error has occurred while enabling browser integration." + "message": "启用浏览器集成时出错。" }, "browserIntegrationMasOnlyDesc": { "message": "很遗憾,目前仅 Mac App Store 版本支持浏览器集成。" @@ -2697,9 +2697,6 @@ "message": "常规格式", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "故障排除" }, @@ -2716,9 +2713,9 @@ "message": "通行密钥已移除" }, "errorAssigningTargetCollection": { - "message": "Error assigning target collection." + "message": "分配目标集合时出错。" }, "errorAssigningTargetFolder": { - "message": "Error assigning target folder." + "message": "分配目标文件夹时出错。" } } diff --git a/apps/desktop/src/locales/zh_TW/messages.json b/apps/desktop/src/locales/zh_TW/messages.json index 5f768b0a43..9eb12e23cf 100644 --- a/apps/desktop/src/locales/zh_TW/messages.json +++ b/apps/desktop/src/locales/zh_TW/messages.json @@ -2697,9 +2697,6 @@ "message": "常見格式", "description": "Label indicating the most common import formats" }, - "success": { - "message": "Success" - }, "troubleshooting": { "message": "疑難排解" }, From 395ed3f5d464355d448204d944eee51094bdc28c Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Fri, 19 Apr 2024 14:02:40 -0500 Subject: [PATCH 236/351] [PM-7489] Introduce `MessageSender` & `MessageListener` (#8709) * Introduce MessageSender * Update `messageSenderFactory` * Remove Comment * Use BrowserApi * Update Comment * Rename to CommandDefinition * Add More Documentation to MessageSender * Add `EMPTY` helpers and remove NoopMessageSender * Calm Down Logging * Limit Logging On Known Errors * Use `messageStream` Parameter Co-authored-by: Matt Gibson <mgibson@bitwarden.com> * Add eslint rules * Update Error Handling Co-authored-by: Cesar Gonzalez <cesar.a.gonzalezcs@gmail.com> * Delete Lazy Classes In Favor of Observable Factories * Remove Fido Messages --------- Co-authored-by: Matt Gibson <mgibson@bitwarden.com> Co-authored-by: Cesar Gonzalez <cesar.a.gonzalezcs@gmail.com> --- .eslintrc.json | 16 ++ .../browser/src/background/main.background.ts | 54 +++--- .../background/nativeMessaging.background.ts | 2 +- .../src/background/runtime.background.ts | 156 ++++++++++-------- .../message-sender.factory.ts | 17 ++ .../messaging-service.factory.ts | 24 +-- .../messaging/chrome-message.sender.ts | 37 +++++ ...ssaging-private-mode-background.service.ts | 8 - ...er-messaging-private-mode-popup.service.ts | 8 - .../services/browser-messaging.service.ts | 9 - .../utils/from-chrome-runtime-messaging.ts | 26 +++ apps/browser/src/popup/app.component.ts | 145 ++++++++-------- .../src/popup/services/services.module.ts | 81 +++++++-- apps/cli/src/bw.ts | 10 +- .../src/app/services/services.module.ts | 30 +++- apps/desktop/src/main.ts | 18 +- apps/desktop/src/main/power-monitor.main.ts | 5 +- apps/desktop/src/platform/preload.ts | 21 ++- .../electron-renderer-message.sender.ts | 12 ++ .../electron-renderer-messaging.service.ts | 20 --- .../src/platform/utils/from-ipc-messaging.ts | 15 ++ .../electron-main-messaging.service.ts | 17 +- .../trial-billing-step.component.ts | 3 +- .../organization-plans.component.ts | 3 +- .../app/core/broadcaster-messaging.service.ts | 14 -- apps/web/src/app/core/core.module.ts | 7 - .../vault/components/premium-badge.stories.ts | 10 +- .../platform/services/broadcaster.service.ts | 6 - libs/angular/src/services/injection-tokens.ts | 6 +- .../src/services/jslib-services.module.ts | 30 +++- .../abstractions/messaging.service.ts | 6 +- .../src/platform/messaging/helpers.spec.ts | 46 ++++++ libs/common/src/platform/messaging/helpers.ts | 23 +++ libs/common/src/platform/messaging/index.ts | 4 + .../common/src/platform/messaging/internal.ts | 5 + .../messaging/message.listener.spec.ts | 47 ++++++ .../platform/messaging/message.listener.ts | 41 +++++ .../src/platform/messaging/message.sender.ts | 62 +++++++ .../messaging/subject-message.sender.spec.ts | 65 ++++++++ .../messaging/subject-message.sender.ts | 17 ++ libs/common/src/platform/messaging/types.ts | 13 ++ .../platform/services/broadcaster.service.ts | 34 ---- .../services/default-broadcaster.service.ts | 36 ++++ .../services/noop-messaging.service.ts | 7 - 44 files changed, 855 insertions(+), 361 deletions(-) create mode 100644 apps/browser/src/platform/background/service-factories/message-sender.factory.ts create mode 100644 apps/browser/src/platform/messaging/chrome-message.sender.ts delete mode 100644 apps/browser/src/platform/services/browser-messaging-private-mode-background.service.ts delete mode 100644 apps/browser/src/platform/services/browser-messaging-private-mode-popup.service.ts delete mode 100644 apps/browser/src/platform/services/browser-messaging.service.ts create mode 100644 apps/browser/src/platform/utils/from-chrome-runtime-messaging.ts create mode 100644 apps/desktop/src/platform/services/electron-renderer-message.sender.ts delete mode 100644 apps/desktop/src/platform/services/electron-renderer-messaging.service.ts create mode 100644 apps/desktop/src/platform/utils/from-ipc-messaging.ts delete mode 100644 apps/web/src/app/core/broadcaster-messaging.service.ts delete mode 100644 libs/angular/src/platform/services/broadcaster.service.ts create mode 100644 libs/common/src/platform/messaging/helpers.spec.ts create mode 100644 libs/common/src/platform/messaging/helpers.ts create mode 100644 libs/common/src/platform/messaging/index.ts create mode 100644 libs/common/src/platform/messaging/internal.ts create mode 100644 libs/common/src/platform/messaging/message.listener.spec.ts create mode 100644 libs/common/src/platform/messaging/message.listener.ts create mode 100644 libs/common/src/platform/messaging/message.sender.ts create mode 100644 libs/common/src/platform/messaging/subject-message.sender.spec.ts create mode 100644 libs/common/src/platform/messaging/subject-message.sender.ts create mode 100644 libs/common/src/platform/messaging/types.ts delete mode 100644 libs/common/src/platform/services/broadcaster.service.ts create mode 100644 libs/common/src/platform/services/default-broadcaster.service.ts delete mode 100644 libs/common/src/platform/services/noop-messaging.service.ts diff --git a/.eslintrc.json b/.eslintrc.json index 671e7b2fab..61bebbf483 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -246,6 +246,22 @@ } ] } + }, + { + "files": ["**/*.ts"], + "excludedFiles": ["**/platform/**/*.ts"], + "rules": { + "no-restricted-imports": [ + "error", + { + "patterns": [ + "**/platform/**/internal", // General internal pattern + // All features that have been converted to barrel files + "**/platform/messaging/**" + ] + } + ] + } } ] } diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 8432c398b7..f2003f9621 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1,4 +1,4 @@ -import { firstValueFrom } from "rxjs"; +import { Subject, firstValueFrom, merge } from "rxjs"; import { PinCryptoServiceAbstraction, @@ -82,7 +82,6 @@ import { FileUploadService as FileUploadServiceAbstraction } from "@bitwarden/co import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { LogService as LogServiceAbstraction } from "@bitwarden/common/platform/abstractions/log.service"; -import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { AbstractMemoryStorageService, @@ -95,6 +94,9 @@ import { DefaultBiometricStateService, } from "@bitwarden/common/platform/biometrics/biometric-state.service"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; +import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging"; +// eslint-disable-next-line no-restricted-imports -- Used for dependency creation +import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal"; 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"; @@ -208,13 +210,14 @@ import { Account } from "../models/account"; import { BrowserApi } from "../platform/browser/browser-api"; import { flagEnabled } from "../platform/flags"; import { UpdateBadge } from "../platform/listeners/update-badge"; +/* eslint-disable no-restricted-imports */ +import { ChromeMessageSender } from "../platform/messaging/chrome-message.sender"; +/* eslint-enable no-restricted-imports */ import { BrowserStateService as StateServiceAbstraction } from "../platform/services/abstractions/browser-state.service"; import { BrowserCryptoService } from "../platform/services/browser-crypto.service"; import { BrowserEnvironmentService } from "../platform/services/browser-environment.service"; import BrowserLocalStorageService from "../platform/services/browser-local-storage.service"; import BrowserMemoryStorageService from "../platform/services/browser-memory-storage.service"; -import BrowserMessagingPrivateModeBackgroundService from "../platform/services/browser-messaging-private-mode-background.service"; -import BrowserMessagingService from "../platform/services/browser-messaging.service"; import { BrowserScriptInjectorService } from "../platform/services/browser-script-injector.service"; import { DefaultBrowserStateService } from "../platform/services/default-browser-state.service"; import I18nService from "../platform/services/i18n.service"; @@ -223,6 +226,7 @@ import { BackgroundPlatformUtilsService } from "../platform/services/platform-ut import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service"; import { BackgroundDerivedStateProvider } from "../platform/state/background-derived-state.provider"; import { BackgroundMemoryStorageService } from "../platform/storage/background-memory-storage.service"; +import { fromChromeRuntimeMessaging } from "../platform/utils/from-chrome-runtime-messaging"; import VaultTimeoutService from "../services/vault-timeout/vault-timeout.service"; import FilelessImporterBackground from "../tools/background/fileless-importer.background"; import { Fido2Background as Fido2BackgroundAbstraction } from "../vault/fido2/background/abstractions/fido2.background"; @@ -236,7 +240,7 @@ import { NativeMessagingBackground } from "./nativeMessaging.background"; import RuntimeBackground from "./runtime.background"; export default class MainBackground { - messagingService: MessagingServiceAbstraction; + messagingService: MessageSender; storageService: BrowserLocalStorageService; secureStorageService: AbstractStorageService; memoryStorageService: AbstractMemoryStorageService; @@ -326,6 +330,8 @@ export default class MainBackground { stateEventRunnerService: StateEventRunnerService; ssoLoginService: SsoLoginServiceAbstraction; billingAccountProfileStateService: BillingAccountProfileStateService; + // eslint-disable-next-line rxjs/no-exposed-subjects -- Needed to give access to services module + intraprocessMessagingSubject: Subject<Message<object>>; userKeyInitService: UserKeyInitService; scriptInjectorService: BrowserScriptInjectorService; @@ -369,15 +375,25 @@ export default class MainBackground { const logoutCallback = async (expired: boolean, userId?: UserId) => await this.logout(expired, userId); - this.messagingService = - this.isPrivateMode && BrowserApi.isManifestVersion(2) - ? new BrowserMessagingPrivateModeBackgroundService() - : new BrowserMessagingService(); this.logService = new ConsoleLogService(false); this.cryptoFunctionService = new WebCryptoFunctionService(self); this.keyGenerationService = new KeyGenerationService(this.cryptoFunctionService); this.storageService = new BrowserLocalStorageService(); + this.intraprocessMessagingSubject = new Subject<Message<object>>(); + + this.messagingService = MessageSender.combine( + new SubjectMessageSender(this.intraprocessMessagingSubject), + new ChromeMessageSender(this.logService), + ); + + const messageListener = new MessageListener( + merge( + this.intraprocessMessagingSubject.asObservable(), // For messages from the same context + fromChromeRuntimeMessaging(), // For messages from other contexts + ), + ); + const mv3MemoryStorageCreator = (partitionName: string) => { // TODO: Consider using multithreaded encrypt service in popup only context return new LocalBackedSessionStorageService( @@ -560,21 +576,6 @@ export default class MainBackground { this.twoFactorService = new TwoFactorService(this.i18nService, this.platformUtilsService); - // eslint-disable-next-line - const that = this; - const backgroundMessagingService = new (class extends MessagingServiceAbstraction { - // AuthService should send the messages to the background not popup. - send = (subscriber: string, arg: any = {}) => { - if (BrowserApi.isManifestVersion(3)) { - that.messagingService.send(subscriber, arg); - return; - } - - const message = Object.assign({}, { command: subscriber }, arg); - void that.runtimeBackground.processMessage(message, that as any); - }; - })(); - this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider); this.devicesApiService = new DevicesApiServiceImplementation(this.apiService); @@ -605,7 +606,7 @@ export default class MainBackground { this.authService = new AuthService( this.accountService, - backgroundMessagingService, + this.messagingService, this.cryptoService, this.apiService, this.stateService, @@ -626,7 +627,7 @@ export default class MainBackground { this.tokenService, this.appIdService, this.platformUtilsService, - backgroundMessagingService, + this.messagingService, this.logService, this.keyConnectorService, this.environmentService, @@ -914,6 +915,7 @@ export default class MainBackground { this.logService, this.configService, this.fido2Background, + messageListener, ); this.nativeMessagingBackground = new NativeMessagingBackground( this.accountService, diff --git a/apps/browser/src/background/nativeMessaging.background.ts b/apps/browser/src/background/nativeMessaging.background.ts index e5eed06c21..5ac9961147 100644 --- a/apps/browser/src/background/nativeMessaging.background.ts +++ b/apps/browser/src/background/nativeMessaging.background.ts @@ -399,7 +399,7 @@ export class NativeMessagingBackground { // 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.runtimeBackground.processMessage({ command: "unlocked" }, null); + this.runtimeBackground.processMessage({ command: "unlocked" }); } break; } diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts index 44fe4818e0..f457889e96 100644 --- a/apps/browser/src/background/runtime.background.ts +++ b/apps/browser/src/background/runtime.background.ts @@ -1,4 +1,4 @@ -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, mergeMap } from "rxjs"; import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service"; import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants"; @@ -10,6 +10,7 @@ import { SystemService } from "@bitwarden/common/platform/abstractions/system.se import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CipherType } from "@bitwarden/common/vault/enums"; +import { MessageListener } from "../../../../libs/common/src/platform/messaging"; import { closeUnlockPopout, openSsoAuthResultPopout, @@ -44,6 +45,7 @@ export default class RuntimeBackground { private logService: LogService, private configService: ConfigService, private fido2Background: Fido2Background, + private messageListener: MessageListener, ) { // onInstalled listener must be wired up before anything else, so we do it in the ctor chrome.runtime.onInstalled.addListener((details: any) => { @@ -60,92 +62,47 @@ export default class RuntimeBackground { const backgroundMessageListener = ( msg: any, sender: chrome.runtime.MessageSender, - sendResponse: any, + sendResponse: (response: any) => void, ) => { const messagesWithResponse = ["biometricUnlock"]; if (messagesWithResponse.includes(msg.command)) { - this.processMessage(msg, sender).then( + this.processMessageWithSender(msg, sender).then( (value) => sendResponse({ result: value }), (error) => sendResponse({ error: { ...error, message: error.message } }), ); return true; } - this.processMessage(msg, sender).catch((e) => this.logService.error(e)); + void this.processMessageWithSender(msg, sender).catch((err) => + this.logService.error( + `Error while processing message in RuntimeBackground '${msg?.command}'. Error: ${err?.message ?? "Unknown Error"}`, + ), + ); + return false; }; + this.messageListener.allMessages$ + .pipe( + mergeMap(async (message: any) => { + await this.processMessage(message); + }), + ) + .subscribe(); + + // For messages that require the full on message interface BrowserApi.messageListener("runtime.background", backgroundMessageListener); - if (this.main.popupOnlyContext) { - (self as any).bitwardenBackgroundMessageListener = backgroundMessageListener; - } } - async processMessage(msg: any, sender: chrome.runtime.MessageSender) { + // Messages that need the chrome sender and send back a response need to be registered in this method. + async processMessageWithSender(msg: any, sender: chrome.runtime.MessageSender) { switch (msg.command) { - case "loggedIn": - case "unlocked": { - let item: LockedVaultPendingNotificationsData; - - if (msg.command === "loggedIn") { - await this.sendBwInstalledMessageToVault(); - } - - if (this.lockedVaultPendingNotifications?.length > 0) { - item = this.lockedVaultPendingNotifications.pop(); - await closeUnlockPopout(); - } - - await this.notificationsService.updateConnection(msg.command === "loggedIn"); - await this.main.refreshBadge(); - await this.main.refreshMenu(false); - this.systemService.cancelProcessReload(); - - if (item) { - await BrowserApi.focusWindow(item.commandToRetry.sender.tab.windowId); - await BrowserApi.focusTab(item.commandToRetry.sender.tab.id); - await BrowserApi.tabSendMessageData( - item.commandToRetry.sender.tab, - "unlockCompleted", - item, - ); - } - break; - } - case "addToLockedVaultPendingNotifications": - this.lockedVaultPendingNotifications.push(msg.data); - break; - case "logout": - await this.main.logout(msg.expired, msg.userId); - break; - case "syncCompleted": - if (msg.successfully) { - setTimeout(async () => { - await this.main.refreshBadge(); - await this.main.refreshMenu(); - }, 2000); - await this.configService.ensureConfigFetched(); - } - break; - case "openPopup": - await this.main.openPopup(); - break; case "triggerAutofillScriptInjection": await this.autofillService.injectAutofillScripts(sender.tab, sender.frameId); break; case "bgCollectPageDetails": await this.main.collectPageDetailsForContentScript(sender.tab, msg.sender, sender.frameId); break; - case "bgUpdateContextMenu": - case "editedCipher": - case "addedCipher": - case "deletedCipher": - await this.main.refreshBadge(); - await this.main.refreshMenu(); - break; - case "bgReseedStorage": - await this.main.reseedStorage(); - break; case "collectPageDetailsResponse": switch (msg.sender) { case "autofiller": @@ -209,6 +166,72 @@ export default class RuntimeBackground { break; } break; + case "biometricUnlock": { + const result = await this.main.biometricUnlock(); + return result; + } + } + } + + async processMessage(msg: any) { + switch (msg.command) { + case "loggedIn": + case "unlocked": { + let item: LockedVaultPendingNotificationsData; + + if (msg.command === "loggedIn") { + await this.sendBwInstalledMessageToVault(); + } + + if (this.lockedVaultPendingNotifications?.length > 0) { + item = this.lockedVaultPendingNotifications.pop(); + await closeUnlockPopout(); + } + + await this.notificationsService.updateConnection(msg.command === "loggedIn"); + await this.main.refreshBadge(); + await this.main.refreshMenu(false); + this.systemService.cancelProcessReload(); + + if (item) { + await BrowserApi.focusWindow(item.commandToRetry.sender.tab.windowId); + await BrowserApi.focusTab(item.commandToRetry.sender.tab.id); + await BrowserApi.tabSendMessageData( + item.commandToRetry.sender.tab, + "unlockCompleted", + item, + ); + } + break; + } + case "addToLockedVaultPendingNotifications": + this.lockedVaultPendingNotifications.push(msg.data); + break; + case "logout": + await this.main.logout(msg.expired, msg.userId); + break; + case "syncCompleted": + if (msg.successfully) { + setTimeout(async () => { + await this.main.refreshBadge(); + await this.main.refreshMenu(); + }, 2000); + await this.configService.ensureConfigFetched(); + } + break; + case "openPopup": + await this.main.openPopup(); + break; + case "bgUpdateContextMenu": + case "editedCipher": + case "addedCipher": + case "deletedCipher": + await this.main.refreshBadge(); + await this.main.refreshMenu(); + break; + case "bgReseedStorage": + await this.main.reseedStorage(); + break; case "authResult": { const env = await firstValueFrom(this.environmentService.environment$); const vaultUrl = env.getWebVaultUrl(); @@ -265,9 +288,6 @@ export default class RuntimeBackground { await this.main.clearClipboard(msg.clipboardValue, msg.timeoutMs); break; } - case "biometricUnlock": { - return await this.main.biometricUnlock(); - } } } diff --git a/apps/browser/src/platform/background/service-factories/message-sender.factory.ts b/apps/browser/src/platform/background/service-factories/message-sender.factory.ts new file mode 100644 index 0000000000..6f50b4b8f5 --- /dev/null +++ b/apps/browser/src/platform/background/service-factories/message-sender.factory.ts @@ -0,0 +1,17 @@ +import { MessageSender } from "@bitwarden/common/platform/messaging"; + +import { CachedServices, factory, FactoryOptions } from "./factory-options"; + +type MessagingServiceFactoryOptions = FactoryOptions; + +export type MessageSenderInitOptions = MessagingServiceFactoryOptions; + +export function messageSenderFactory( + cache: { messagingService?: MessageSender } & CachedServices, + opts: MessageSenderInitOptions, +): Promise<MessageSender> { + // NOTE: Name needs to match that of MainBackground property until we delete these. + return factory(cache, "messagingService", opts, () => { + throw new Error("Not implemented, not expected to be used."); + }); +} diff --git a/apps/browser/src/platform/background/service-factories/messaging-service.factory.ts b/apps/browser/src/platform/background/service-factories/messaging-service.factory.ts index 46852712aa..20c6e3f424 100644 --- a/apps/browser/src/platform/background/service-factories/messaging-service.factory.ts +++ b/apps/browser/src/platform/background/service-factories/messaging-service.factory.ts @@ -1,19 +1,5 @@ -import { MessagingService as AbstractMessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; - -import { - CachedServices, - factory, - FactoryOptions, -} from "../../background/service-factories/factory-options"; -import BrowserMessagingService from "../../services/browser-messaging.service"; - -type MessagingServiceFactoryOptions = FactoryOptions; - -export type MessagingServiceInitOptions = MessagingServiceFactoryOptions; - -export function messagingServiceFactory( - cache: { messagingService?: AbstractMessagingService } & CachedServices, - opts: MessagingServiceInitOptions, -): Promise<AbstractMessagingService> { - return factory(cache, "messagingService", opts, () => new BrowserMessagingService()); -} +// Export old messaging service stuff to minimize changes +export { + messageSenderFactory as messagingServiceFactory, + MessageSenderInitOptions as MessagingServiceInitOptions, +} from "./message-sender.factory"; diff --git a/apps/browser/src/platform/messaging/chrome-message.sender.ts b/apps/browser/src/platform/messaging/chrome-message.sender.ts new file mode 100644 index 0000000000..0e57ecfb4e --- /dev/null +++ b/apps/browser/src/platform/messaging/chrome-message.sender.ts @@ -0,0 +1,37 @@ +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { CommandDefinition, MessageSender } from "@bitwarden/common/platform/messaging"; +import { getCommand } from "@bitwarden/common/platform/messaging/internal"; + +type ErrorHandler = (logger: LogService, command: string) => void; + +const HANDLED_ERRORS: Record<string, ErrorHandler> = { + "Could not establish connection. Receiving end does not exist.": (logger, command) => + logger.debug(`Receiving end didn't exist for command '${command}'`), + + "The message port closed before a response was received.": (logger, command) => + logger.debug(`Port was closed for command '${command}'`), +}; + +export class ChromeMessageSender implements MessageSender { + constructor(private readonly logService: LogService) {} + + send<T extends object>( + commandDefinition: string | CommandDefinition<T>, + payload: object | T = {}, + ): void { + const command = getCommand(commandDefinition); + chrome.runtime.sendMessage(Object.assign(payload, { command: command }), () => { + if (chrome.runtime.lastError) { + const errorHandler = HANDLED_ERRORS[chrome.runtime.lastError.message]; + if (errorHandler != null) { + errorHandler(this.logService, command); + return; + } + + this.logService.warning( + `Unhandled error while sending message with command '${command}': ${chrome.runtime.lastError.message}`, + ); + } + }); + } +} diff --git a/apps/browser/src/platform/services/browser-messaging-private-mode-background.service.ts b/apps/browser/src/platform/services/browser-messaging-private-mode-background.service.ts deleted file mode 100644 index 0c7008473b..0000000000 --- a/apps/browser/src/platform/services/browser-messaging-private-mode-background.service.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; - -export default class BrowserMessagingPrivateModeBackgroundService implements MessagingService { - send(subscriber: string, arg: any = {}) { - const message = Object.assign({}, { command: subscriber }, arg); - (self as any).bitwardenPopupMainMessageListener(message); - } -} diff --git a/apps/browser/src/platform/services/browser-messaging-private-mode-popup.service.ts b/apps/browser/src/platform/services/browser-messaging-private-mode-popup.service.ts deleted file mode 100644 index 5883f61197..0000000000 --- a/apps/browser/src/platform/services/browser-messaging-private-mode-popup.service.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; - -export default class BrowserMessagingPrivateModePopupService implements MessagingService { - send(subscriber: string, arg: any = {}) { - const message = Object.assign({}, { command: subscriber }, arg); - (self as any).bitwardenBackgroundMessageListener(message); - } -} diff --git a/apps/browser/src/platform/services/browser-messaging.service.ts b/apps/browser/src/platform/services/browser-messaging.service.ts deleted file mode 100644 index 5eff957cb5..0000000000 --- a/apps/browser/src/platform/services/browser-messaging.service.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; - -import { BrowserApi } from "../browser/browser-api"; - -export default class BrowserMessagingService implements MessagingService { - send(subscriber: string, arg: any = {}) { - return BrowserApi.sendMessage(subscriber, arg); - } -} diff --git a/apps/browser/src/platform/utils/from-chrome-runtime-messaging.ts b/apps/browser/src/platform/utils/from-chrome-runtime-messaging.ts new file mode 100644 index 0000000000..e30f35b680 --- /dev/null +++ b/apps/browser/src/platform/utils/from-chrome-runtime-messaging.ts @@ -0,0 +1,26 @@ +import { map, share } from "rxjs"; + +import { tagAsExternal } from "@bitwarden/common/platform/messaging/internal"; + +import { fromChromeEvent } from "../browser/from-chrome-event"; + +/** + * Creates an observable that listens to messages through `chrome.runtime.onMessage`. + * @returns An observable stream of messages. + */ +export const fromChromeRuntimeMessaging = () => { + return fromChromeEvent(chrome.runtime.onMessage).pipe( + map(([message, sender]) => { + message ??= {}; + + // Force the sender onto the message as long as we won't overwrite anything + if (!("webExtSender" in message)) { + message.webExtSender = sender; + } + + return message; + }), + tagAsExternal, + share(), + ); +}; diff --git a/apps/browser/src/popup/app.component.ts b/apps/browser/src/popup/app.component.ts index 2aba93ac95..7acaf1ba93 100644 --- a/apps/browser/src/popup/app.component.ts +++ b/apps/browser/src/popup/app.component.ts @@ -1,17 +1,16 @@ import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core"; import { NavigationEnd, Router, RouterOutlet } from "@angular/router"; -import { filter, concatMap, Subject, takeUntil, firstValueFrom, map } from "rxjs"; +import { filter, concatMap, Subject, takeUntil, firstValueFrom, tap, map } from "rxjs"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; -import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { MessageListener } from "@bitwarden/common/platform/messaging"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { DialogService, SimpleDialogOptions, ToastService } from "@bitwarden/components"; import { BrowserApi } from "../platform/browser/browser-api"; -import { ZonedMessageListenerService } from "../platform/browser/zoned-message-listener.service"; import { BrowserStateService } from "../platform/services/abstractions/browser-state.service"; import { BrowserSendStateService } from "../tools/popup/services/browser-send-state.service"; import { VaultBrowserStateService } from "../vault/services/vault-browser-state.service"; @@ -34,7 +33,6 @@ export class AppComponent implements OnInit, OnDestroy { private destroy$ = new Subject<void>(); constructor( - private broadcasterService: BroadcasterService, private authService: AuthService, private i18nService: I18nService, private router: Router, @@ -46,7 +44,7 @@ export class AppComponent implements OnInit, OnDestroy { private ngZone: NgZone, private platformUtilsService: PlatformUtilsService, private dialogService: DialogService, - private browserMessagingApi: ZonedMessageListenerService, + private messageListener: MessageListener, private toastService: ToastService, ) {} @@ -78,77 +76,76 @@ export class AppComponent implements OnInit, OnDestroy { window.onkeypress = () => this.recordActivity(); }); - const bitwardenPopupMainMessageListener = (msg: any, sender: any) => { - if (msg.command === "doneLoggingOut") { - this.authService.logOut(async () => { - if (msg.expired) { - this.toastService.showToast({ - variant: "warning", - title: this.i18nService.t("loggedOut"), - message: this.i18nService.t("loginExpired"), + this.messageListener.allMessages$ + .pipe( + tap((msg: any) => { + if (msg.command === "doneLoggingOut") { + this.authService.logOut(async () => { + if (msg.expired) { + this.toastService.showToast({ + variant: "warning", + title: this.i18nService.t("loggedOut"), + message: this.i18nService.t("loginExpired"), + }); + } + + // 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(["home"]); }); + this.changeDetectorRef.detectChanges(); + } else if (msg.command === "authBlocked") { + // 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(["home"]); + } else if ( + msg.command === "locked" && + (msg.userId == null || msg.userId == this.activeUserId) + ) { + // 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"]); + } else if (msg.command === "showDialog") { + // 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.showDialog(msg); + } else if (msg.command === "showNativeMessagingFinterprintDialog") { + // TODO: Should be refactored to live in another service. + // 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.showNativeMessagingFingerprintDialog(msg); + } else if (msg.command === "showToast") { + this.toastService._showToast(msg); + } else if (msg.command === "reloadProcess") { + const forceWindowReload = + this.platformUtilsService.isSafari() || + this.platformUtilsService.isFirefox() || + this.platformUtilsService.isOpera(); + // Wait to make sure background has reloaded first. + window.setTimeout( + () => BrowserApi.reloadExtension(forceWindowReload ? window : null), + 2000, + ); + } else if (msg.command === "reloadPopup") { + // 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(["/"]); + } else if (msg.command === "convertAccountToKeyConnector") { + // 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(["/remove-password"]); + } else if (msg.command === "switchAccountFinish") { + // TODO: unset loading? + // this.loading = false; + } else if (msg.command == "update-temp-password") { + // 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(["/update-temp-password"]); } - - // 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(["home"]); - }); - this.changeDetectorRef.detectChanges(); - } else if (msg.command === "authBlocked") { - // 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(["home"]); - } else if ( - msg.command === "locked" && - (msg.userId == null || msg.userId == this.activeUserId) - ) { - // 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"]); - } else if (msg.command === "showDialog") { - // 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.showDialog(msg); - } else if (msg.command === "showNativeMessagingFinterprintDialog") { - // TODO: Should be refactored to live in another service. - // 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.showNativeMessagingFingerprintDialog(msg); - } else if (msg.command === "showToast") { - this.toastService._showToast(msg); - } else if (msg.command === "reloadProcess") { - const forceWindowReload = - this.platformUtilsService.isSafari() || - this.platformUtilsService.isFirefox() || - this.platformUtilsService.isOpera(); - // Wait to make sure background has reloaded first. - window.setTimeout( - () => BrowserApi.reloadExtension(forceWindowReload ? window : null), - 2000, - ); - } else if (msg.command === "reloadPopup") { - // 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(["/"]); - } else if (msg.command === "convertAccountToKeyConnector") { - // 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(["/remove-password"]); - } else if (msg.command === "switchAccountFinish") { - // TODO: unset loading? - // this.loading = false; - } else if (msg.command == "update-temp-password") { - // 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(["/update-temp-password"]); - } else { - msg.webExtSender = sender; - this.broadcasterService.send(msg); - } - }; - - (self as any).bitwardenPopupMainMessageListener = bitwardenPopupMainMessageListener; - this.browserMessagingApi.messageListener("app.component", bitwardenPopupMainMessageListener); + }), + takeUntil(this.destroy$), + ) + .subscribe(); // eslint-disable-next-line rxjs/no-async-subscribe this.router.events.pipe(takeUntil(this.destroy$)).subscribe(async (event) => { diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index f3be8490c1..4ab1fe2368 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -1,5 +1,6 @@ import { APP_INITIALIZER, NgModule, NgZone } from "@angular/core"; import { Router } from "@angular/router"; +import { Subject, merge } from "rxjs"; import { UnauthGuard as BaseUnauthGuardService } from "@bitwarden/angular/auth/guards"; import { AngularThemingService } from "@bitwarden/angular/platform/services/theming/angular-theming.service"; @@ -11,6 +12,7 @@ import { OBSERVABLE_MEMORY_STORAGE, SYSTEM_THEME_OBSERVABLE, SafeInjectionToken, + INTRAPROCESS_MESSAGING_SUBJECT, } from "@bitwarden/angular/services/injection-tokens"; import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module"; import { @@ -54,7 +56,6 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService as I18nServiceAbstraction } 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 as BaseStateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; import { @@ -63,6 +64,9 @@ import { ObservableStorageService, } from "@bitwarden/common/platform/abstractions/storage.service"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; +import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging"; +// eslint-disable-next-line no-restricted-imports -- Used for dependency injection +import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; @@ -89,20 +93,23 @@ import AutofillService from "../../autofill/services/autofill.service"; import MainBackground from "../../background/main.background"; import { Account } from "../../models/account"; import { BrowserApi } from "../../platform/browser/browser-api"; +import { runInsideAngular } from "../../platform/browser/run-inside-angular.operator"; +/* eslint-disable no-restricted-imports */ +import { ChromeMessageSender } from "../../platform/messaging/chrome-message.sender"; +/* eslint-enable no-restricted-imports */ import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; import { BrowserFileDownloadService } from "../../platform/popup/services/browser-file-download.service"; import { BrowserStateService as StateServiceAbstraction } from "../../platform/services/abstractions/browser-state.service"; import { ScriptInjectorService } from "../../platform/services/abstractions/script-injector.service"; import { BrowserEnvironmentService } from "../../platform/services/browser-environment.service"; import BrowserLocalStorageService from "../../platform/services/browser-local-storage.service"; -import BrowserMessagingPrivateModePopupService from "../../platform/services/browser-messaging-private-mode-popup.service"; -import BrowserMessagingService from "../../platform/services/browser-messaging.service"; import { BrowserScriptInjectorService } from "../../platform/services/browser-script-injector.service"; import { DefaultBrowserStateService } from "../../platform/services/default-browser-state.service"; import I18nService from "../../platform/services/i18n.service"; import { ForegroundPlatformUtilsService } from "../../platform/services/platform-utils/foreground-platform-utils.service"; import { ForegroundDerivedStateProvider } from "../../platform/state/foreground-derived-state.provider"; import { ForegroundMemoryStorageService } from "../../platform/storage/foreground-memory-storage.service"; +import { fromChromeRuntimeMessaging } from "../../platform/utils/from-chrome-runtime-messaging"; import { BrowserSendStateService } from "../../tools/popup/services/browser-send-state.service"; import { FilePopoutUtilsService } from "../../tools/popup/services/file-popout-utils.service"; import { VaultBrowserStateService } from "../../vault/services/vault-browser-state.service"; @@ -155,15 +162,6 @@ const safeProviders: SafeProvider[] = [ useClass: UnauthGuardService, deps: [AuthServiceAbstraction, Router], }), - safeProvider({ - provide: MessagingService, - useFactory: () => { - return needsBackgroundInit && BrowserApi.isManifestVersion(2) - ? new BrowserMessagingPrivateModePopupService() - : new BrowserMessagingService(); - }, - deps: [], - }), safeProvider({ provide: TwoFactorService, useFactory: getBgService<TwoFactorService>("twoFactorService"), @@ -484,6 +482,65 @@ const safeProviders: SafeProvider[] = [ useClass: BrowserSendStateService, deps: [StateProvider], }), + safeProvider({ + provide: MessageListener, + useFactory: (subject: Subject<Message<object>>, ngZone: NgZone) => + new MessageListener( + merge( + subject.asObservable(), // For messages in the same context + fromChromeRuntimeMessaging().pipe(runInsideAngular(ngZone)), // For messages in the same context + ), + ), + deps: [INTRAPROCESS_MESSAGING_SUBJECT, NgZone], + }), + safeProvider({ + provide: MessageSender, + useFactory: (subject: Subject<Message<object>>, logService: LogService) => + MessageSender.combine( + new SubjectMessageSender(subject), // For sending messages in the same context + new ChromeMessageSender(logService), // For sending messages to different contexts + ), + deps: [INTRAPROCESS_MESSAGING_SUBJECT, LogService], + }), + safeProvider({ + provide: INTRAPROCESS_MESSAGING_SUBJECT, + useFactory: () => { + if (BrowserPopupUtils.backgroundInitializationRequired()) { + // There is no persistent main background which means we have one in memory, + // we need the same instance that our in memory background is utilizing. + return getBgService("intraprocessMessagingSubject")(); + } else { + return new Subject<Message<object>>(); + } + }, + deps: [], + }), + safeProvider({ + provide: MessageSender, + useFactory: (subject: Subject<Message<object>>, logService: LogService) => + MessageSender.combine( + new SubjectMessageSender(subject), // For sending messages in the same context + new ChromeMessageSender(logService), // For sending messages to different contexts + ), + deps: [INTRAPROCESS_MESSAGING_SUBJECT, LogService], + }), + safeProvider({ + provide: INTRAPROCESS_MESSAGING_SUBJECT, + useFactory: () => { + if (needsBackgroundInit) { + // We will have created a popup within this context, in that case + // we want to make sure we have the same subject as that context so we + // can message with it. + return getBgService("intraprocessMessagingSubject")(); + } else { + // There isn't a locally created background so we will communicate with + // the true background through chrome apis, in that case, we can just create + // one for ourself. + return new Subject<Message<object>>(); + } + }, + deps: [], + }), ]; @NgModule({ diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index 7fbefc10e3..e784997d82 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -60,10 +60,10 @@ import { } from "@bitwarden/common/platform/biometrics/biometric-state.service"; import { KeySuffixOptions, LogLevelType } from "@bitwarden/common/platform/enums"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; +import { MessageSender } from "@bitwarden/common/platform/messaging"; 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 { 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"; @@ -75,7 +75,6 @@ import { KeyGenerationService } from "@bitwarden/common/platform/services/key-ge 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"; -import { NoopMessagingService } from "@bitwarden/common/platform/services/noop-messaging.service"; import { StateService } from "@bitwarden/common/platform/services/state.service"; import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider"; import { @@ -155,7 +154,7 @@ global.DOMParser = new jsdom.JSDOM().window.DOMParser; const packageJson = require("../package.json"); export class Main { - messagingService: NoopMessagingService; + messagingService: MessageSender; storageService: LowdbStorageService; secureStorageService: NodeEnvSecureStorageService; memoryStorageService: MemoryStorageService; @@ -212,7 +211,6 @@ export class Main { organizationService: OrganizationService; providerService: ProviderService; twoFactorService: TwoFactorService; - broadcasterService: BroadcasterService; folderApiService: FolderApiService; userVerificationApiService: UserVerificationApiService; organizationApiService: OrganizationApiServiceAbstraction; @@ -298,7 +296,7 @@ export class Main { stateEventRegistrarService, ); - this.messagingService = new NoopMessagingService(); + this.messagingService = MessageSender.EMPTY; this.accountService = new AccountServiceImplementation( this.messagingService, @@ -422,8 +420,6 @@ export class Main { this.searchService = new SearchService(this.logService, this.i18nService, this.stateProvider); - this.broadcasterService = new BroadcasterService(); - this.collectionService = new CollectionService( this.cryptoService, this.i18nService, diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index 264f26cbe2..d1d51c0f1c 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -1,4 +1,5 @@ import { APP_INITIALIZER, NgModule } from "@angular/core"; +import { Subject, merge } from "rxjs"; import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider"; import { @@ -14,6 +15,7 @@ import { SYSTEM_THEME_OBSERVABLE, SafeInjectionToken, STATE_FACTORY, + INTRAPROCESS_MESSAGING_SUBJECT, } 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"; @@ -23,7 +25,6 @@ import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/ab import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.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"; import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; @@ -42,6 +43,9 @@ import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/ import { SystemService as SystemServiceAbstraction } from "@bitwarden/common/platform/abstractions/system.service"; import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; +import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging"; +// eslint-disable-next-line no-restricted-imports -- Used for dependency injection +import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; @@ -63,11 +67,12 @@ import { ELECTRON_SUPPORTS_SECURE_STORAGE, ElectronPlatformUtilsService, } from "../../platform/services/electron-platform-utils.service"; -import { ElectronRendererMessagingService } from "../../platform/services/electron-renderer-messaging.service"; +import { ElectronRendererMessageSender } from "../../platform/services/electron-renderer-message.sender"; import { ElectronRendererSecureStorageService } from "../../platform/services/electron-renderer-secure-storage.service"; import { ElectronRendererStorageService } from "../../platform/services/electron-renderer-storage.service"; import { ElectronStateService } from "../../platform/services/electron-state.service"; import { I18nRendererService } from "../../platform/services/i18n.renderer.service"; +import { fromIpcMessaging } from "../../platform/utils/from-ipc-messaging"; import { fromIpcSystemTheme } from "../../platform/utils/from-ipc-system-theme"; import { EncryptedMessageHandlerService } from "../../services/encrypted-message-handler.service"; import { NativeMessageHandlerService } from "../../services/native-message-handler.service"; @@ -138,9 +143,24 @@ const safeProviders: SafeProvider[] = [ deps: [SYSTEM_LANGUAGE, LOCALES_DIRECTORY, GlobalStateProvider], }), safeProvider({ - provide: MessagingServiceAbstraction, - useClass: ElectronRendererMessagingService, - deps: [BroadcasterServiceAbstraction], + provide: MessageSender, + useFactory: (subject: Subject<Message<object>>) => + MessageSender.combine( + new ElectronRendererMessageSender(), // Communication with main process + new SubjectMessageSender(subject), // Communication with ourself + ), + deps: [INTRAPROCESS_MESSAGING_SUBJECT], + }), + safeProvider({ + provide: MessageListener, + useFactory: (subject: Subject<Message<object>>) => + new MessageListener( + merge( + subject.asObservable(), // For messages from the same context + fromIpcMessaging(), // For messages from the main process + ), + ), + deps: [INTRAPROCESS_MESSAGING_SUBJECT], }), safeProvider({ provide: AbstractStorageService, diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index a4783e0573..0655e5600d 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -1,7 +1,7 @@ import * as path from "path"; import { app } from "electron"; -import { firstValueFrom } from "rxjs"; +import { Subject, firstValueFrom } from "rxjs"; import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service"; import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service"; @@ -11,6 +11,9 @@ import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwar 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 { Message, MessageSender } from "@bitwarden/common/platform/messaging"; +// eslint-disable-next-line no-restricted-imports -- For dependency creation +import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal"; 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"; @@ -18,7 +21,6 @@ import { KeyGenerationService } from "@bitwarden/common/platform/services/key-ge 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"; -import { NoopMessagingService } from "@bitwarden/common/platform/services/noop-messaging.service"; /* eslint-disable import/no-restricted-paths -- We need the implementation to inject, but generally this should not be accessed */ import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider"; import { DefaultActiveUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-active-user-state.provider"; @@ -59,7 +61,7 @@ export class Main { storageService: ElectronStorageService; memoryStorageService: MemoryStorageService; memoryStorageForStateProviders: MemoryStorageServiceForStateProviders; - messagingService: ElectronMainMessagingService; + messagingService: MessageSender; stateService: StateService; environmentService: DefaultEnvironmentService; mainCryptoFunctionService: MainCryptoFunctionService; @@ -131,7 +133,7 @@ export class Main { this.i18nService = new I18nMainService("en", "./locales/", globalStateProvider); const accountService = new AccountServiceImplementation( - new NoopMessagingService(), + MessageSender.EMPTY, this.logService, globalStateProvider, ); @@ -223,7 +225,13 @@ export class Main { this.updaterMain = new UpdaterMain(this.i18nService, this.windowMain); this.trayMain = new TrayMain(this.windowMain, this.i18nService, this.desktopSettingsService); - this.messagingService = new ElectronMainMessagingService(this.windowMain, (message) => { + const messageSubject = new Subject<Message<object>>(); + this.messagingService = MessageSender.combine( + new SubjectMessageSender(messageSubject), // For local messages + new ElectronMainMessagingService(this.windowMain), + ); + + messageSubject.asObservable().subscribe((message) => { this.messagingMain.onMessage(message); }); diff --git a/apps/desktop/src/main/power-monitor.main.ts b/apps/desktop/src/main/power-monitor.main.ts index 067a380ba0..8cad5c1d9e 100644 --- a/apps/desktop/src/main/power-monitor.main.ts +++ b/apps/desktop/src/main/power-monitor.main.ts @@ -1,6 +1,7 @@ import { powerMonitor } from "electron"; -import { ElectronMainMessagingService } from "../services/electron-main-messaging.service"; +import { MessageSender } from "@bitwarden/common/platform/messaging"; + import { isSnapStore } from "../utils"; // tslint:disable-next-line @@ -10,7 +11,7 @@ const IdleCheckInterval = 30 * 1000; // 30 seconds export class PowerMonitorMain { private idle = false; - constructor(private messagingService: ElectronMainMessagingService) {} + constructor(private messagingService: MessageSender) {} init() { // ref: https://github.com/electron/electron/issues/13767 diff --git a/apps/desktop/src/platform/preload.ts b/apps/desktop/src/platform/preload.ts index 04819998d5..771d25ef0a 100644 --- a/apps/desktop/src/platform/preload.ts +++ b/apps/desktop/src/platform/preload.ts @@ -124,12 +124,21 @@ export default { sendMessage: (message: { command: string } & any) => ipcRenderer.send("messagingService", message), - onMessage: (callback: (message: { command: string } & any) => void) => { - ipcRenderer.on("messagingService", (_event, message: any) => { - if (message.command) { - callback(message); - } - }); + onMessage: { + addListener: (callback: (message: { command: string } & any) => void) => { + ipcRenderer.addListener("messagingService", (_event, message: any) => { + if (message.command) { + callback(message); + } + }); + }, + removeListener: (callback: (message: { command: string } & any) => void) => { + ipcRenderer.removeListener("messagingService", (_event, message: any) => { + if (message.command) { + callback(message); + } + }); + }, }, launchUri: (uri: string) => ipcRenderer.invoke("launchUri", uri), diff --git a/apps/desktop/src/platform/services/electron-renderer-message.sender.ts b/apps/desktop/src/platform/services/electron-renderer-message.sender.ts new file mode 100644 index 0000000000..037c303b3b --- /dev/null +++ b/apps/desktop/src/platform/services/electron-renderer-message.sender.ts @@ -0,0 +1,12 @@ +import { MessageSender, CommandDefinition } from "@bitwarden/common/platform/messaging"; +import { getCommand } from "@bitwarden/common/platform/messaging/internal"; + +export class ElectronRendererMessageSender implements MessageSender { + send<T extends object>( + commandDefinition: CommandDefinition<T> | string, + payload: object | T = {}, + ): void { + const command = getCommand(commandDefinition); + ipc.platform.sendMessage(Object.assign({}, { command: command }, payload)); + } +} diff --git a/apps/desktop/src/platform/services/electron-renderer-messaging.service.ts b/apps/desktop/src/platform/services/electron-renderer-messaging.service.ts deleted file mode 100644 index 192efc1dc6..0000000000 --- a/apps/desktop/src/platform/services/electron-renderer-messaging.service.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; - -export class ElectronRendererMessagingService implements MessagingService { - constructor(private broadcasterService: BroadcasterService) { - ipc.platform.onMessage((message) => this.sendMessage(message.command, message, false)); - } - - send(subscriber: string, arg: any = {}) { - this.sendMessage(subscriber, arg, true); - } - - private sendMessage(subscriber: string, arg: any = {}, toMain: boolean) { - const message = Object.assign({}, { command: subscriber }, arg); - this.broadcasterService.send(message); - if (toMain) { - ipc.platform.sendMessage(message); - } - } -} diff --git a/apps/desktop/src/platform/utils/from-ipc-messaging.ts b/apps/desktop/src/platform/utils/from-ipc-messaging.ts new file mode 100644 index 0000000000..254a215ceb --- /dev/null +++ b/apps/desktop/src/platform/utils/from-ipc-messaging.ts @@ -0,0 +1,15 @@ +import { fromEventPattern, share } from "rxjs"; + +import { Message } from "@bitwarden/common/platform/messaging"; +import { tagAsExternal } from "@bitwarden/common/platform/messaging/internal"; + +/** + * Creates an observable that when subscribed to will listen to messaging events through IPC. + * @returns An observable stream of messages. + */ +export const fromIpcMessaging = () => { + return fromEventPattern<Message<object>>( + (handler) => ipc.platform.onMessage.addListener(handler), + (handler) => ipc.platform.onMessage.removeListener(handler), + ).pipe(tagAsExternal, share()); +}; diff --git a/apps/desktop/src/services/electron-main-messaging.service.ts b/apps/desktop/src/services/electron-main-messaging.service.ts index 71e1b1d7d5..ce4ffd903a 100644 --- a/apps/desktop/src/services/electron-main-messaging.service.ts +++ b/apps/desktop/src/services/electron-main-messaging.service.ts @@ -2,18 +2,17 @@ import * as path from "path"; import { app, dialog, ipcMain, Menu, MenuItem, nativeTheme, Notification, shell } from "electron"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { ThemeType } from "@bitwarden/common/platform/enums"; +import { MessageSender, CommandDefinition } from "@bitwarden/common/platform/messaging"; +// eslint-disable-next-line no-restricted-imports -- Using implementation helper in implementation +import { getCommand } from "@bitwarden/common/platform/messaging/internal"; import { SafeUrls } from "@bitwarden/common/platform/misc/safe-urls"; import { WindowMain } from "../main/window.main"; import { RendererMenuItem } from "../utils"; -export class ElectronMainMessagingService implements MessagingService { - constructor( - private windowMain: WindowMain, - private onMessage: (message: any) => void, - ) { +export class ElectronMainMessagingService implements MessageSender { + constructor(private windowMain: WindowMain) { ipcMain.handle("appVersion", () => { return app.getVersion(); }); @@ -88,9 +87,9 @@ export class ElectronMainMessagingService implements MessagingService { }); } - send(subscriber: string, arg: any = {}) { - const message = Object.assign({}, { command: subscriber }, arg); - this.onMessage(message); + send<T extends object>(commandDefinition: CommandDefinition<T> | string, arg: T | object = {}) { + const command = getCommand(commandDefinition); + const message = Object.assign({}, { command: command }, arg); if (this.windowMain.win != null) { this.windowMain.win.webContents.send("messagingService", message); } diff --git a/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.ts b/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.ts index 0c5f1d706b..bd138cad29 100644 --- a/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.ts +++ b/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.ts @@ -103,7 +103,8 @@ export class TrialBillingStepComponent implements OnInit { planDescription, }); - this.messagingService.send("organizationCreated", organizationId); + // TODO: No one actually listening to this? + this.messagingService.send("organizationCreated", { organizationId }); } protected changedCountry() { 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 645b1f29ac..23d48d93be 100644 --- a/apps/web/src/app/billing/organizations/organization-plans.component.ts +++ b/apps/web/src/app/billing/organizations/organization-plans.component.ts @@ -587,7 +587,8 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { this.formPromise = doSubmit(); const organizationId = await this.formPromise; this.onSuccess.emit({ organizationId: organizationId }); - this.messagingService.send("organizationCreated", organizationId); + // TODO: No one actually listening to this message? + this.messagingService.send("organizationCreated", { organizationId }); } catch (e) { this.logService.error(e); } diff --git a/apps/web/src/app/core/broadcaster-messaging.service.ts b/apps/web/src/app/core/broadcaster-messaging.service.ts deleted file mode 100644 index 7c8e4eef43..0000000000 --- a/apps/web/src/app/core/broadcaster-messaging.service.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Injectable } from "@angular/core"; - -import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; - -@Injectable() -export class BroadcasterMessagingService implements MessagingService { - constructor(private broadcasterService: BroadcasterService) {} - - send(subscriber: string, arg: any = {}) { - const message = Object.assign({}, { command: subscriber }, arg); - this.broadcasterService.send(message); - } -} diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index 9d53bc39f0..a274764756 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -22,7 +22,6 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; @@ -51,7 +50,6 @@ import { WebStorageServiceProvider } from "../platform/web-storage-service.provi import { WindowStorageService } from "../platform/window-storage.service"; import { CollectionAdminService } from "../vault/core/collection-admin.service"; -import { BroadcasterMessagingService } from "./broadcaster-messaging.service"; import { EventService } from "./event.service"; import { InitService } from "./init.service"; import { ModalService } from "./modal.service"; @@ -117,11 +115,6 @@ const safeProviders: SafeProvider[] = [ useClass: WebPlatformUtilsService, useAngularDecorators: true, }), - safeProvider({ - provide: MessagingServiceAbstraction, - useClass: BroadcasterMessagingService, - useAngularDecorators: true, - }), safeProvider({ provide: ModalServiceAbstraction, useClass: ModalService, diff --git a/apps/web/src/app/vault/components/premium-badge.stories.ts b/apps/web/src/app/vault/components/premium-badge.stories.ts index 5433dd9981..c61bbb46a5 100644 --- a/apps/web/src/app/vault/components/premium-badge.stories.ts +++ b/apps/web/src/app/vault/components/premium-badge.stories.ts @@ -4,15 +4,15 @@ import { of } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { MessageSender } from "@bitwarden/common/platform/messaging"; import { BadgeModule, I18nMockService } from "@bitwarden/components"; import { PremiumBadgeComponent } from "./premium-badge.component"; -class MockMessagingService implements MessagingService { - send(subscriber: string, arg?: any) { +class MockMessagingService implements MessageSender { + send = () => { alert("Clicked on badge"); - } + }; } export default { @@ -31,7 +31,7 @@ export default { }, }, { - provide: MessagingService, + provide: MessageSender, useFactory: () => { return new MockMessagingService(); }, diff --git a/libs/angular/src/platform/services/broadcaster.service.ts b/libs/angular/src/platform/services/broadcaster.service.ts deleted file mode 100644 index cf58d2b311..0000000000 --- a/libs/angular/src/platform/services/broadcaster.service.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Injectable } from "@angular/core"; - -import { BroadcasterService as BaseBroadcasterService } from "@bitwarden/common/platform/services/broadcaster.service"; - -@Injectable() -export class BroadcasterService extends BaseBroadcasterService {} diff --git a/libs/angular/src/services/injection-tokens.ts b/libs/angular/src/services/injection-tokens.ts index 7d39078797..6fffe722fb 100644 --- a/libs/angular/src/services/injection-tokens.ts +++ b/libs/angular/src/services/injection-tokens.ts @@ -1,5 +1,5 @@ import { InjectionToken } from "@angular/core"; -import { Observable } from "rxjs"; +import { Observable, Subject } from "rxjs"; import { AbstractMemoryStorageService, @@ -8,6 +8,7 @@ import { } from "@bitwarden/common/platform/abstractions/storage.service"; import { ThemeType } from "@bitwarden/common/platform/enums"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; +import { Message } from "@bitwarden/common/platform/messaging"; declare const tag: unique symbol; /** @@ -49,3 +50,6 @@ export const LOG_MAC_FAILURES = new SafeInjectionToken<boolean>("LOG_MAC_FAILURE export const SYSTEM_THEME_OBSERVABLE = new SafeInjectionToken<Observable<ThemeType>>( "SYSTEM_THEME_OBSERVABLE", ); +export const INTRAPROCESS_MESSAGING_SUBJECT = new SafeInjectionToken<Subject<Message<object>>>( + "INTRAPROCESS_MESSAGING_SUBJECT", +); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index b0d84e7c3b..204ff5a294 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -1,4 +1,5 @@ import { ErrorHandler, LOCALE_ID, NgModule } from "@angular/core"; +import { Subject } from "rxjs"; import { AuthRequestServiceAbstraction, @@ -116,7 +117,7 @@ import { BillingApiService } from "@bitwarden/common/billing/services/billing-ap import { OrganizationBillingService } from "@bitwarden/common/billing/services/organization-billing.service"; import { PaymentMethodWarningsService } from "@bitwarden/common/billing/services/payment-method-warnings.service"; import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { BroadcasterService as BroadcasterServiceAbstraction } from "@bitwarden/common/platform/abstractions/broadcaster.service"; +import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service"; @@ -137,6 +138,9 @@ import { DefaultBiometricStateService, } from "@bitwarden/common/platform/biometrics/biometric-state.service"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; +import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging"; +// eslint-disable-next-line no-restricted-imports -- Used for dependency injection +import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal"; import { devFlagEnabled, flagEnabled } from "@bitwarden/common/platform/misc/flags"; import { Account } from "@bitwarden/common/platform/models/domain/account"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; @@ -147,6 +151,7 @@ import { ConsoleLogService } from "@bitwarden/common/platform/services/console-l import { CryptoService } from "@bitwarden/common/platform/services/crypto.service"; import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation"; import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation"; +import { DefaultBroadcasterService } from "@bitwarden/common/platform/services/default-broadcaster.service"; import { DefaultEnvironmentService } from "@bitwarden/common/platform/services/default-environment.service"; import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service"; import { KeyGenerationService } from "@bitwarden/common/platform/services/key-generation.service"; @@ -247,7 +252,6 @@ import { import { AuthGuard } from "../auth/guards/auth.guard"; 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"; @@ -270,6 +274,7 @@ import { SYSTEM_LANGUAGE, SYSTEM_THEME_OBSERVABLE, WINDOW, + INTRAPROCESS_MESSAGING_SUBJECT, } from "./injection-tokens"; import { ModalService } from "./modal.service"; @@ -625,7 +630,11 @@ const safeProviders: SafeProvider[] = [ BillingAccountProfileStateService, ], }), - safeProvider({ provide: BroadcasterServiceAbstraction, useClass: BroadcasterService, deps: [] }), + safeProvider({ + provide: BroadcasterService, + useClass: DefaultBroadcasterService, + deps: [MessageSender, MessageListener], + }), safeProvider({ provide: VaultTimeoutSettingsServiceAbstraction, useClass: VaultTimeoutSettingsService, @@ -1127,6 +1136,21 @@ const safeProviders: SafeProvider[] = [ useClass: LoggingErrorHandler, deps: [], }), + safeProvider({ + provide: INTRAPROCESS_MESSAGING_SUBJECT, + useFactory: () => new Subject<Message<object>>(), + deps: [], + }), + safeProvider({ + provide: MessageListener, + useFactory: (subject: Subject<Message<object>>) => new MessageListener(subject.asObservable()), + deps: [INTRAPROCESS_MESSAGING_SUBJECT], + }), + safeProvider({ + provide: MessageSender, + useFactory: (subject: Subject<Message<object>>) => new SubjectMessageSender(subject), + deps: [INTRAPROCESS_MESSAGING_SUBJECT], + }), safeProvider({ provide: ProviderApiServiceAbstraction, useClass: ProviderApiService, diff --git a/libs/common/src/platform/abstractions/messaging.service.ts b/libs/common/src/platform/abstractions/messaging.service.ts index ab4332c283..f24279f932 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 { - abstract send(subscriber: string, arg?: any): void; -} +// Export the new message sender as the legacy MessagingService to minimize changes in the initial PR, +// team specific PR's will come after. +export { MessageSender as MessagingService } from "../messaging/message.sender"; diff --git a/libs/common/src/platform/messaging/helpers.spec.ts b/libs/common/src/platform/messaging/helpers.spec.ts new file mode 100644 index 0000000000..fcd36b4411 --- /dev/null +++ b/libs/common/src/platform/messaging/helpers.spec.ts @@ -0,0 +1,46 @@ +import { Subject, firstValueFrom } from "rxjs"; + +import { getCommand, isExternalMessage, tagAsExternal } from "./helpers"; +import { Message, CommandDefinition } from "./types"; + +describe("helpers", () => { + describe("getCommand", () => { + it("can get the command from just a string", () => { + const command = getCommand("myCommand"); + + expect(command).toEqual("myCommand"); + }); + + it("can get the command from a message definition", () => { + const commandDefinition = new CommandDefinition<object>("myCommand"); + + const command = getCommand(commandDefinition); + + expect(command).toEqual("myCommand"); + }); + }); + + describe("tag integration", () => { + it("can tag and identify as tagged", async () => { + const messagesSubject = new Subject<Message<object>>(); + + const taggedMessages = messagesSubject.asObservable().pipe(tagAsExternal); + + const firstValuePromise = firstValueFrom(taggedMessages); + + messagesSubject.next({ command: "test" }); + + const result = await firstValuePromise; + + expect(isExternalMessage(result)).toEqual(true); + }); + }); + + describe("isExternalMessage", () => { + it.each([null, { command: "myCommand", test: "object" }, undefined] as Message< + Record<string, unknown> + >[])("returns false when value is %s", (value: Message<object>) => { + expect(isExternalMessage(value)).toBe(false); + }); + }); +}); diff --git a/libs/common/src/platform/messaging/helpers.ts b/libs/common/src/platform/messaging/helpers.ts new file mode 100644 index 0000000000..bf119432e0 --- /dev/null +++ b/libs/common/src/platform/messaging/helpers.ts @@ -0,0 +1,23 @@ +import { MonoTypeOperatorFunction, map } from "rxjs"; + +import { Message, CommandDefinition } from "./types"; + +export const getCommand = (commandDefinition: CommandDefinition<object> | string) => { + if (typeof commandDefinition === "string") { + return commandDefinition; + } else { + return commandDefinition.command; + } +}; + +export const EXTERNAL_SOURCE_TAG = Symbol("externalSource"); + +export const isExternalMessage = (message: Message<object>) => { + return (message as Record<PropertyKey, unknown>)?.[EXTERNAL_SOURCE_TAG] === true; +}; + +export const tagAsExternal: MonoTypeOperatorFunction<Message<object>> = map( + (message: Message<object>) => { + return Object.assign(message, { [EXTERNAL_SOURCE_TAG]: true }); + }, +); diff --git a/libs/common/src/platform/messaging/index.ts b/libs/common/src/platform/messaging/index.ts new file mode 100644 index 0000000000..a9b4eca5ae --- /dev/null +++ b/libs/common/src/platform/messaging/index.ts @@ -0,0 +1,4 @@ +export { MessageListener } from "./message.listener"; +export { MessageSender } from "./message.sender"; +export { Message, CommandDefinition } from "./types"; +export { isExternalMessage } from "./helpers"; diff --git a/libs/common/src/platform/messaging/internal.ts b/libs/common/src/platform/messaging/internal.ts new file mode 100644 index 0000000000..08763d48bc --- /dev/null +++ b/libs/common/src/platform/messaging/internal.ts @@ -0,0 +1,5 @@ +// Built in implementations +export { SubjectMessageSender } from "./subject-message.sender"; + +// Helpers meant to be used only by other implementations +export { tagAsExternal, getCommand } from "./helpers"; diff --git a/libs/common/src/platform/messaging/message.listener.spec.ts b/libs/common/src/platform/messaging/message.listener.spec.ts new file mode 100644 index 0000000000..98bbf1fdc8 --- /dev/null +++ b/libs/common/src/platform/messaging/message.listener.spec.ts @@ -0,0 +1,47 @@ +import { Subject } from "rxjs"; + +import { subscribeTo } from "../../../spec/observable-tracker"; + +import { MessageListener } from "./message.listener"; +import { Message, CommandDefinition } from "./types"; + +describe("MessageListener", () => { + const subject = new Subject<Message<{ test: number }>>(); + const sut = new MessageListener(subject.asObservable()); + + const testCommandDefinition = new CommandDefinition<{ test: number }>("myCommand"); + + describe("allMessages$", () => { + it("runs on all nexts", async () => { + const tracker = subscribeTo(sut.allMessages$); + + const pausePromise = tracker.pauseUntilReceived(2); + + subject.next({ command: "command1", test: 1 }); + subject.next({ command: "command2", test: 2 }); + + await pausePromise; + + expect(tracker.emissions[0]).toEqual({ command: "command1", test: 1 }); + expect(tracker.emissions[1]).toEqual({ command: "command2", test: 2 }); + }); + }); + + describe("messages$", () => { + it("runs on only my commands", async () => { + const tracker = subscribeTo(sut.messages$(testCommandDefinition)); + + const pausePromise = tracker.pauseUntilReceived(2); + + subject.next({ command: "notMyCommand", test: 1 }); + subject.next({ command: "myCommand", test: 2 }); + subject.next({ command: "myCommand", test: 3 }); + subject.next({ command: "notMyCommand", test: 4 }); + + await pausePromise; + + expect(tracker.emissions[0]).toEqual({ command: "myCommand", test: 2 }); + expect(tracker.emissions[1]).toEqual({ command: "myCommand", test: 3 }); + }); + }); +}); diff --git a/libs/common/src/platform/messaging/message.listener.ts b/libs/common/src/platform/messaging/message.listener.ts new file mode 100644 index 0000000000..df453c8422 --- /dev/null +++ b/libs/common/src/platform/messaging/message.listener.ts @@ -0,0 +1,41 @@ +import { EMPTY, Observable, filter } from "rxjs"; + +import { Message, CommandDefinition } from "./types"; + +/** + * A class that allows for listening to messages coming through the application, + * allows for listening of all messages or just the messages you care about. + * + * @note Consider NOT using messaging at all if you can. State Providers offer an observable stream of + * data that is persisted. This can serve messages that might have been used to notify of settings changes + * or vault data changes and those observables should be preferred over messaging. + */ +export class MessageListener { + constructor(private readonly messageStream: Observable<Message<object>>) {} + + /** + * A stream of all messages sent through the application. It does not contain type information for the + * other properties on the messages. You are encouraged to instead subscribe to an individual message + * through {@link messages$}. + */ + allMessages$ = this.messageStream; + + /** + * Creates an observable stream filtered to just the command given via the {@link CommandDefinition} and typed + * to the generic contained in the CommandDefinition. Be careful using this method unless all your messages are being + * sent through `MessageSender.send`, if that isn't the case you should have lower confidence in the message + * payload being the expected type. + * + * @param commandDefinition The CommandDefinition containing the information about the message type you care about. + */ + messages$<T extends object>(commandDefinition: CommandDefinition<T>): Observable<T> { + return this.allMessages$.pipe( + filter((msg) => msg?.command === commandDefinition.command), + ) as Observable<T>; + } + + /** + * A helper property for returning a MessageListener that will never emit any messages and will immediately complete. + */ + static readonly EMPTY = new MessageListener(EMPTY); +} diff --git a/libs/common/src/platform/messaging/message.sender.ts b/libs/common/src/platform/messaging/message.sender.ts new file mode 100644 index 0000000000..6bf2661580 --- /dev/null +++ b/libs/common/src/platform/messaging/message.sender.ts @@ -0,0 +1,62 @@ +import { CommandDefinition } from "./types"; + +class MultiMessageSender implements MessageSender { + constructor(private readonly innerMessageSenders: MessageSender[]) {} + + send<T extends object>( + commandDefinition: string | CommandDefinition<T>, + payload: object | T = {}, + ): void { + for (const messageSender of this.innerMessageSenders) { + messageSender.send(commandDefinition, payload); + } + } +} + +export abstract class MessageSender { + /** + * A method for sending messages in a type safe manner. The passed in command definition + * will require you to provide a compatible type in the payload parameter. + * + * @example + * const MY_COMMAND = new CommandDefinition<{ test: number }>("myCommand"); + * + * this.messageSender.send(MY_COMMAND, { test: 14 }); + * + * @param commandDefinition + * @param payload + */ + abstract send<T extends object>(commandDefinition: CommandDefinition<T>, payload: T): void; + + /** + * A legacy method for sending messages in a non-type safe way. + * + * @remarks Consider defining a {@link CommandDefinition} and passing that in for the first parameter to + * get compilation errors when defining an incompatible payload. + * + * @param command The string based command of your message. + * @param payload Extra contextual information regarding the message. Be aware that this payload may + * be serialized and lose all prototype information. + */ + abstract send(command: string, payload?: object): void; + + /** Implementation of the other two overloads, read their docs instead. */ + abstract send<T extends object>( + commandDefinition: CommandDefinition<T> | string, + payload: T | object, + ): void; + + /** + * A helper method for combine multiple {@link MessageSender}'s. + * @param messageSenders The message senders that should be combined. + * @returns A message sender that will relay all messages to the given message senders. + */ + static combine(...messageSenders: MessageSender[]) { + return new MultiMessageSender(messageSenders); + } + + /** + * A helper property for creating a {@link MessageSender} that sends to nowhere. + */ + static readonly EMPTY: MessageSender = new MultiMessageSender([]); +} diff --git a/libs/common/src/platform/messaging/subject-message.sender.spec.ts b/libs/common/src/platform/messaging/subject-message.sender.spec.ts new file mode 100644 index 0000000000..4278fca7bc --- /dev/null +++ b/libs/common/src/platform/messaging/subject-message.sender.spec.ts @@ -0,0 +1,65 @@ +import { Subject } from "rxjs"; + +import { subscribeTo } from "../../../spec/observable-tracker"; + +import { SubjectMessageSender } from "./internal"; +import { MessageSender } from "./message.sender"; +import { Message, CommandDefinition } from "./types"; + +describe("SubjectMessageSender", () => { + const subject = new Subject<Message<{ test: number }>>(); + const subjectObservable = subject.asObservable(); + + const sut: MessageSender = new SubjectMessageSender(subject); + + describe("send", () => { + it("will send message with command from message definition", async () => { + const commandDefinition = new CommandDefinition<{ test: number }>("myCommand"); + + const tracker = subscribeTo(subjectObservable); + const pausePromise = tracker.pauseUntilReceived(1); + + sut.send(commandDefinition, { test: 1 }); + + await pausePromise; + + expect(tracker.emissions[0]).toEqual({ command: "myCommand", test: 1 }); + }); + + it("will send message with command from normal string", async () => { + const tracker = subscribeTo(subjectObservable); + const pausePromise = tracker.pauseUntilReceived(1); + + sut.send("myCommand", { test: 1 }); + + await pausePromise; + + expect(tracker.emissions[0]).toEqual({ command: "myCommand", test: 1 }); + }); + + it("will send message with object even if payload not given", async () => { + const tracker = subscribeTo(subjectObservable); + const pausePromise = tracker.pauseUntilReceived(1); + + sut.send("myCommand"); + + await pausePromise; + + expect(tracker.emissions[0]).toEqual({ command: "myCommand" }); + }); + + it.each([null, undefined])( + "will send message with object even if payload is null-ish (%s)", + async (payloadValue) => { + const tracker = subscribeTo(subjectObservable); + const pausePromise = tracker.pauseUntilReceived(1); + + sut.send("myCommand", payloadValue); + + await pausePromise; + + expect(tracker.emissions[0]).toEqual({ command: "myCommand" }); + }, + ); + }); +}); diff --git a/libs/common/src/platform/messaging/subject-message.sender.ts b/libs/common/src/platform/messaging/subject-message.sender.ts new file mode 100644 index 0000000000..94ae6f27f3 --- /dev/null +++ b/libs/common/src/platform/messaging/subject-message.sender.ts @@ -0,0 +1,17 @@ +import { Subject } from "rxjs"; + +import { getCommand } from "./internal"; +import { MessageSender } from "./message.sender"; +import { Message, CommandDefinition } from "./types"; + +export class SubjectMessageSender implements MessageSender { + constructor(private readonly messagesSubject: Subject<Message<object>>) {} + + send<T extends object>( + commandDefinition: string | CommandDefinition<T>, + payload: object | T = {}, + ): void { + const command = getCommand(commandDefinition); + this.messagesSubject.next(Object.assign(payload ?? {}, { command: command })); + } +} diff --git a/libs/common/src/platform/messaging/types.ts b/libs/common/src/platform/messaging/types.ts new file mode 100644 index 0000000000..f30163344f --- /dev/null +++ b/libs/common/src/platform/messaging/types.ts @@ -0,0 +1,13 @@ +declare const tag: unique symbol; + +/** + * A class for defining information about a message, this is helpful + * alonside `MessageSender` and `MessageListener` for providing a type + * safe(-ish) way of sending and receiving messages. + */ +export class CommandDefinition<T extends object> { + [tag]: T; + constructor(readonly command: string) {} +} + +export type Message<T extends object> = { command: string } & T; diff --git a/libs/common/src/platform/services/broadcaster.service.ts b/libs/common/src/platform/services/broadcaster.service.ts deleted file mode 100644 index 9d823b00e0..0000000000 --- a/libs/common/src/platform/services/broadcaster.service.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { - BroadcasterService as BroadcasterServiceAbstraction, - MessageBase, -} from "../abstractions/broadcaster.service"; - -export class BroadcasterService implements BroadcasterServiceAbstraction { - subscribers: Map<string, (message: MessageBase) => void> = new Map< - string, - (message: MessageBase) => void - >(); - - send(message: MessageBase, id?: string) { - if (id != null) { - if (this.subscribers.has(id)) { - this.subscribers.get(id)(message); - } - return; - } - - this.subscribers.forEach((value) => { - value(message); - }); - } - - subscribe(id: string, messageCallback: (message: MessageBase) => void) { - this.subscribers.set(id, messageCallback); - } - - unsubscribe(id: string) { - if (this.subscribers.has(id)) { - this.subscribers.delete(id); - } - } -} diff --git a/libs/common/src/platform/services/default-broadcaster.service.ts b/libs/common/src/platform/services/default-broadcaster.service.ts new file mode 100644 index 0000000000..a16745c643 --- /dev/null +++ b/libs/common/src/platform/services/default-broadcaster.service.ts @@ -0,0 +1,36 @@ +import { Subscription } from "rxjs"; + +import { BroadcasterService, MessageBase } from "../abstractions/broadcaster.service"; +import { MessageListener, MessageSender } from "../messaging"; + +/** + * Temporary implementation that just delegates to the message sender and message listener + * and manages their subscriptions. + */ +export class DefaultBroadcasterService implements BroadcasterService { + subscriptions = new Map<string, Subscription>(); + + constructor( + private readonly messageSender: MessageSender, + private readonly messageListener: MessageListener, + ) {} + + send(message: MessageBase, id?: string) { + this.messageSender.send(message?.command, message); + } + + subscribe(id: string, messageCallback: (message: MessageBase) => void) { + this.subscriptions.set( + id, + this.messageListener.allMessages$.subscribe((message) => { + messageCallback(message); + }), + ); + } + + unsubscribe(id: string) { + const subscription = this.subscriptions.get(id); + subscription?.unsubscribe(); + this.subscriptions.delete(id); + } +} diff --git a/libs/common/src/platform/services/noop-messaging.service.ts b/libs/common/src/platform/services/noop-messaging.service.ts deleted file mode 100644 index d1a60bc5bc..0000000000 --- a/libs/common/src/platform/services/noop-messaging.service.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { MessagingService } from "../abstractions/messaging.service"; - -export class NoopMessagingService implements MessagingService { - send(subscriber: string, arg: any = {}) { - // Do nothing... - } -} From ec1af0cf9f7588e5ec476d573f2f041bbde0d722 Mon Sep 17 00:00:00 2001 From: Jonathan Prusik <jprusik@users.noreply.github.com> Date: Fri, 19 Apr 2024 15:21:54 -0400 Subject: [PATCH 237/351] [PM-7610] [MV3] Guard overlay visibility and autofill on page load settings from awaiting indefinitely when there is no active account (#8833) * guard overlay visibility and autofill on page load settings from awaiting indefinitely when there is no active account * cleanup --- .../autofill-service.factory.ts | 8 +++++++- .../services/autofill.service.spec.ts | 1 + .../src/autofill/services/autofill.service.ts | 20 +++++++++++++++++-- .../browser/src/background/main.background.ts | 3 ++- .../src/popup/services/services.module.ts | 1 + 5 files changed, 29 insertions(+), 4 deletions(-) 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 7b423ca4f4..bee5da18b5 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 @@ -1,3 +1,7 @@ +import { + accountServiceFactory, + AccountServiceInitOptions, +} from "../../../auth/background/service-factories/account-service.factory"; import { UserVerificationServiceInitOptions, userVerificationServiceFactory, @@ -50,7 +54,8 @@ export type AutoFillServiceInitOptions = AutoFillServiceOptions & LogServiceInitOptions & UserVerificationServiceInitOptions & DomainSettingsServiceInitOptions & - BrowserScriptInjectorServiceInitOptions; + BrowserScriptInjectorServiceInitOptions & + AccountServiceInitOptions; export function autofillServiceFactory( cache: { autofillService?: AbstractAutoFillService } & CachedServices, @@ -71,6 +76,7 @@ export function autofillServiceFactory( await userVerificationServiceFactory(cache, opts), await billingAccountProfileStateServiceFactory(cache, opts), await browserScriptInjectorServiceFactory(cache, opts), + await accountServiceFactory(cache, opts), ), ); } diff --git a/apps/browser/src/autofill/services/autofill.service.spec.ts b/apps/browser/src/autofill/services/autofill.service.spec.ts index 5064d2e7df..d1fbf79bfa 100644 --- a/apps/browser/src/autofill/services/autofill.service.spec.ts +++ b/apps/browser/src/autofill/services/autofill.service.spec.ts @@ -87,6 +87,7 @@ describe("AutofillService", () => { userVerificationService, billingAccountProfileStateService, scriptInjectorService, + accountService, ); domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider); diff --git a/apps/browser/src/autofill/services/autofill.service.ts b/apps/browser/src/autofill/services/autofill.service.ts index 6cf58558dc..8f85d65692 100644 --- a/apps/browser/src/autofill/services/autofill.service.ts +++ b/apps/browser/src/autofill/services/autofill.service.ts @@ -1,7 +1,9 @@ import { firstValueFrom } from "rxjs"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; +import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types"; @@ -57,6 +59,7 @@ export default class AutofillService implements AutofillServiceInterface { private userVerificationService: UserVerificationService, private billingAccountProfileStateService: BillingAccountProfileStateService, private scriptInjectorService: ScriptInjectorService, + private accountService: AccountService, ) {} /** @@ -104,13 +107,26 @@ export default class AutofillService implements AutofillServiceInterface { frameId = 0, triggeringOnPageLoad = true, ): Promise<void> { - const mainAutofillScript = (await this.getOverlayVisibility()) + // Autofill settings loaded from state can await the active account state indefinitely if + // not guarded by an active account check (e.g. the user is logged in) + const activeAccount = await firstValueFrom(this.accountService.activeAccount$); + + // These settings are not available until the user logs in + let overlayVisibility: InlineMenuVisibilitySetting = AutofillOverlayVisibility.Off; + let autoFillOnPageLoadIsEnabled = false; + + if (activeAccount) { + overlayVisibility = await this.getOverlayVisibility(); + } + const mainAutofillScript = overlayVisibility ? "bootstrap-autofill-overlay.js" : "bootstrap-autofill.js"; const injectedScripts = [mainAutofillScript]; - const autoFillOnPageLoadIsEnabled = await this.getAutofillOnPageLoad(); + if (activeAccount) { + autoFillOnPageLoadIsEnabled = await this.getAutofillOnPageLoad(); + } if (triggeringOnPageLoad && autoFillOnPageLoadIsEnabled) { injectedScripts.push("autofiller.js"); diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index f2003f9621..c627c0032b 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -29,6 +29,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/services/policy/p import { ProviderService } from "@bitwarden/common/admin-console/services/provider.service"; import { AccountService as AccountServiceAbstraction } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.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 { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; @@ -137,7 +138,6 @@ import { EventUploadService } from "@bitwarden/common/services/event/event-uploa import { NotificationsService } from "@bitwarden/common/services/notifications.service"; import { SearchService } from "@bitwarden/common/services/search.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service"; -import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/src/auth/abstractions/avatar.service"; import { PasswordGenerationService, PasswordGenerationServiceAbstraction, @@ -807,6 +807,7 @@ export default class MainBackground { this.userVerificationService, this.billingAccountProfileStateService, this.scriptInjectorService, + this.accountService, ); this.auditService = new AuditService(this.cryptoFunctionService, this.apiService); diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 4ab1fe2368..123e901e4e 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -312,6 +312,7 @@ const safeProviders: SafeProvider[] = [ UserVerificationService, BillingAccountProfileStateService, ScriptInjectorService, + AccountServiceAbstraction, ], }), safeProvider({ From c8a3cb5708429b9807f4917f1b57fa12a1b76a83 Mon Sep 17 00:00:00 2001 From: MtnBurrit0 <77340197+mimartin12@users.noreply.github.com> Date: Fri, 19 Apr 2024 13:39:06 -0600 Subject: [PATCH 238/351] [DEVOPS-1919] - Slack messages contain the incorrect git commit sha (#8813) * Initial run to see what data I can access * Update to use JQ * Use dev action * Implement artifact build sha - Moved notify job to happen post artifact check - Removed git sha job - Updated jobs to use real artifact sha * Update .github/workflows/deploy-web.yml Co-authored-by: Vince Grassia <593223+vgrassia@users.noreply.github.com> * Handle web build triggers - Update GH environment with commit as well --------- Co-authored-by: Vince Grassia <593223+vgrassia@users.noreply.github.com> --- .github/workflows/deploy-web.yml | 148 ++++++++++++++++--------------- 1 file changed, 77 insertions(+), 71 deletions(-) diff --git a/.github/workflows/deploy-web.yml b/.github/workflows/deploy-web.yml index 769e700588..6a5d9f1405 100644 --- a/.github/workflows/deploy-web.yml +++ b/.github/workflows/deploy-web.yml @@ -128,29 +128,90 @@ jobs: - name: Success Code run: exit 0 - get-branch-or-tag-sha: - name: Get Branch or Tag SHA + artifact-check: + name: Check if Web artifact is present runs-on: ubuntu-22.04 + needs: setup + env: + _ENVIRONMENT_ARTIFACT: ${{ needs.setup.outputs.environment-artifact }} outputs: - branch-or-tag-sha: ${{ steps.get-branch-or-tag-sha.outputs.sha }} + artifact-build-commit: ${{ steps.set-artifact-commit.outputs.commit }} steps: - - name: Checkout Branch - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - 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-run-id + continue-on-error: true with: - ref: ${{ inputs.branch-or-tag }} - fetch-depth: 0 + workflow: build-web.yml + path: apps/web + workflow_conclusion: success + run_id: ${{ inputs.build-web-run-id }} + artifacts: ${{ env._ENVIRONMENT_ARTIFACT }} - - name: Get Branch or Tag SHA - id: get-branch-or-tag-sha + - 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-latest-artifacts + continue-on-error: true + with: + workflow: build-web.yml + path: apps/web + workflow_conclusion: success + branch: ${{ inputs.branch-or-tag }} + artifacts: ${{ env._ENVIRONMENT_ARTIFACT }} + + - name: Login to Azure + if: ${{ steps.download-latest-artifacts.outcome == 'failure' }} + uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + with: + creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} + + - name: Retrieve secrets for Build trigger + if: ${{ steps.download-latest-artifacts.outcome == 'failure' }} + id: retrieve-secret + uses: bitwarden/gh-actions/get-keyvault-secrets@main + with: + keyvault: "bitwarden-ci" + secrets: "github-pat-bitwarden-devops-bot-repo-scope" + + - name: 'Trigger build web for missing branch/tag ${{ inputs.branch-or-tag }}' + if: ${{ steps.download-latest-artifacts.outcome == 'failure' }} + uses: convictional/trigger-workflow-and-wait@f69fa9eedd3c62a599220f4d5745230e237904be # v1.6.5 + id: trigger-build-web + with: + owner: bitwarden + repo: clients + github_token: ${{ steps.retrieve-secret.outputs.github-pat-bitwarden-devops-bot-repo-scope }} + workflow_file_name: build-web.yml + ref: ${{ inputs.branch-or-tag }} + wait_interval: 100 + + - name: Set artifact build commit + id: set-artifact-commit + env: + GH_TOKEN: ${{ github.token }} run: | - echo "sha=$(git rev-parse origin/${{ inputs.branch-or-tag }})" >> $GITHUB_OUTPUT + # If run-id was used, get the commit from the download-latest-artifacts-run-id step + if [ "${{ inputs.build-web-run-id }}" ]; then + echo "commit=${{ steps.download-latest-artifacts-run-id.outputs.artifact-build-commit }}" >> $GITHUB_OUTPUT + + elif [ "${{ steps.download-latest-artifacts.outcome }}" == "failure" ]; then + # If the download-latest-artifacts step failed, query the GH API to get the commit SHA of the artifact that was just built with trigger-build-web. + commit=$(gh api /repos/bitwarden/clients/actions/runs/${{ steps.trigger-build-web.outputs.workflow_id }}/artifacts --jq '.artifacts[0].workflow_run.head_sha') + echo "commit=$commit" >> $GITHUB_OUTPUT + + else + # Set the commit to the output of step download-latest-artifacts. + echo "commit=${{ steps.download-latest-artifacts.outputs.artifact-build-commit }}" >> $GITHUB_OUTPUT + fi notify-start: name: Notify Slack with start message needs: - approval - setup - - get-branch-or-tag-sha + - artifact-check runs-on: ubuntu-22.04 if: ${{ always() && contains( inputs.environment , 'QA' ) }} outputs: @@ -165,66 +226,10 @@ jobs: tag: ${{ inputs.branch-or-tag }} slack-channel: team-eng-qa-devops event: 'start' - commit-sha: ${{ needs.get-branch-or-tag-sha.outputs.branch-or-tag-sha }} + commit-sha: ${{ needs.artifact-check.outputs.artifact-build-commit }} url: https://github.com/bitwarden/clients/actions/runs/${{ github.run_id }} AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - artifact-check: - name: Check if Web artifact is present - runs-on: ubuntu-22.04 - needs: setup - 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 - with: - workflow: build-web.yml - path: apps/web - workflow_conclusion: success - branch: ${{ inputs.branch-or-tag }} - artifacts: ${{ env._ENVIRONMENT_ARTIFACT }} - - - name: Login to Azure - if: ${{ steps.download-artifacts.outcome == 'failure' }} - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 - with: - creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - - - name: Retrieve secrets for Build trigger - if: ${{ steps.download-artifacts.outcome == 'failure' }} - id: retrieve-secret - uses: bitwarden/gh-actions/get-keyvault-secrets@main - with: - keyvault: "bitwarden-ci" - secrets: "github-pat-bitwarden-devops-bot-repo-scope" - - - name: 'Trigger build web for missing branch/tag ${{ inputs.branch-or-tag }}' - if: ${{ steps.download-artifacts.outcome == 'failure' }} - uses: convictional/trigger-workflow-and-wait@f69fa9eedd3c62a599220f4d5745230e237904be # v1.6.5 - with: - owner: bitwarden - repo: clients - github_token: ${{ steps.retrieve-secret.outputs.github-pat-bitwarden-devops-bot-repo-scope }} - workflow_file_name: build-web.yml - ref: ${{ inputs.branch-or-tag }} - wait_interval: 100 - azure-deploy: name: Deploy Web Vault to ${{ inputs.environment }} Storage Account needs: @@ -248,6 +253,7 @@ jobs: environment: ${{ env._ENVIRONMENT_NAME }} task: 'deploy' description: 'Deployment from branch/tag: ${{ inputs.branch-or-tag }}' + ref: ${{ needs.artifact-check.outputs.artifact-build-commit }} - name: Login to Azure uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 @@ -349,10 +355,10 @@ jobs: runs-on: ubuntu-22.04 if: ${{ always() && contains( inputs.environment , 'QA' ) }} needs: + - setup - notify-start - azure-deploy - - setup - - get-branch-or-tag-sha + - artifact-check steps: - uses: bitwarden/gh-actions/report-deployment-status-to-slack@main with: @@ -362,6 +368,6 @@ jobs: slack-channel: ${{ needs.notify-start.outputs.channel_id }} event: ${{ needs.azure-deploy.result }} url: https://github.com/bitwarden/clients/actions/runs/${{ github.run_id }} - commit-sha: ${{ needs.get-branch-or-tag-sha.outputs.branch-or-tag-sha }} + commit-sha: ${{ needs.artifact-check.outputs.artifact-build-commit }} update-ts: ${{ needs.notify-start.outputs.ts }} AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} From 26b3259c705178e499f55fbcbd29ccc77c2d8f94 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 19 Apr 2024 19:40:20 +0000 Subject: [PATCH 239/351] Autosync the updated translations (#8837) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/web/src/locales/af/messages.json | 9 ++ apps/web/src/locales/ar/messages.json | 9 ++ apps/web/src/locales/az/messages.json | 9 ++ apps/web/src/locales/be/messages.json | 9 ++ apps/web/src/locales/bg/messages.json | 63 +++++++----- apps/web/src/locales/bn/messages.json | 9 ++ apps/web/src/locales/bs/messages.json | 9 ++ apps/web/src/locales/ca/messages.json | 53 +++++----- apps/web/src/locales/cs/messages.json | 43 ++++---- apps/web/src/locales/cy/messages.json | 9 ++ apps/web/src/locales/da/messages.json | 9 ++ apps/web/src/locales/de/messages.json | 9 ++ apps/web/src/locales/el/messages.json | 9 ++ apps/web/src/locales/en_GB/messages.json | 9 ++ apps/web/src/locales/en_IN/messages.json | 9 ++ apps/web/src/locales/eo/messages.json | 9 ++ apps/web/src/locales/es/messages.json | 9 ++ apps/web/src/locales/et/messages.json | 9 ++ apps/web/src/locales/eu/messages.json | 9 ++ apps/web/src/locales/fa/messages.json | 9 ++ apps/web/src/locales/fi/messages.json | 119 ++++++++++++----------- apps/web/src/locales/fil/messages.json | 9 ++ apps/web/src/locales/fr/messages.json | 43 ++++---- apps/web/src/locales/gl/messages.json | 9 ++ apps/web/src/locales/he/messages.json | 9 ++ apps/web/src/locales/hi/messages.json | 9 ++ apps/web/src/locales/hr/messages.json | 9 ++ apps/web/src/locales/hu/messages.json | 11 ++- apps/web/src/locales/id/messages.json | 9 ++ apps/web/src/locales/it/messages.json | 11 ++- apps/web/src/locales/ja/messages.json | 63 +++++++----- apps/web/src/locales/ka/messages.json | 9 ++ apps/web/src/locales/km/messages.json | 9 ++ apps/web/src/locales/kn/messages.json | 9 ++ apps/web/src/locales/ko/messages.json | 9 ++ apps/web/src/locales/lv/messages.json | 15 ++- apps/web/src/locales/ml/messages.json | 9 ++ apps/web/src/locales/mr/messages.json | 9 ++ apps/web/src/locales/my/messages.json | 9 ++ apps/web/src/locales/nb/messages.json | 9 ++ apps/web/src/locales/ne/messages.json | 9 ++ apps/web/src/locales/nl/messages.json | 9 ++ apps/web/src/locales/nn/messages.json | 9 ++ apps/web/src/locales/or/messages.json | 9 ++ apps/web/src/locales/pl/messages.json | 9 ++ apps/web/src/locales/pt_BR/messages.json | 9 ++ apps/web/src/locales/pt_PT/messages.json | 9 ++ apps/web/src/locales/ro/messages.json | 9 ++ apps/web/src/locales/ru/messages.json | 9 ++ apps/web/src/locales/si/messages.json | 9 ++ apps/web/src/locales/sk/messages.json | 91 +++++++++-------- apps/web/src/locales/sl/messages.json | 9 ++ apps/web/src/locales/sr/messages.json | 9 ++ apps/web/src/locales/sr_CS/messages.json | 9 ++ apps/web/src/locales/sv/messages.json | 21 ++-- apps/web/src/locales/te/messages.json | 9 ++ apps/web/src/locales/th/messages.json | 9 ++ apps/web/src/locales/tr/messages.json | 9 ++ apps/web/src/locales/uk/messages.json | 9 ++ apps/web/src/locales/vi/messages.json | 9 ++ apps/web/src/locales/zh_CN/messages.json | 13 ++- apps/web/src/locales/zh_TW/messages.json | 9 ++ 62 files changed, 777 insertions(+), 219 deletions(-) diff --git a/apps/web/src/locales/af/messages.json b/apps/web/src/locales/af/messages.json index 51ba16e5e0..2c226ec0b8 100644 --- a/apps/web/src/locales/af/messages.json +++ b/apps/web/src/locales/af/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/ar/messages.json b/apps/web/src/locales/ar/messages.json index 4377e5655b..be25dead79 100644 --- a/apps/web/src/locales/ar/messages.json +++ b/apps/web/src/locales/ar/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/az/messages.json b/apps/web/src/locales/az/messages.json index 2e94d219d8..d53278e93a 100644 --- a/apps/web/src/locales/az/messages.json +++ b/apps/web/src/locales/az/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "Bu kolleksiya yalnız admin konsolundan əlçatandır" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/be/messages.json b/apps/web/src/locales/be/messages.json index 2cfb8af125..7c3b76b610 100644 --- a/apps/web/src/locales/be/messages.json +++ b/apps/web/src/locales/be/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/bg/messages.json b/apps/web/src/locales/bg/messages.json index 05a819f97a..f4ee30ba95 100644 --- a/apps/web/src/locales/bg/messages.json +++ b/apps/web/src/locales/bg/messages.json @@ -7607,7 +7607,7 @@ "message": "Портал за доставчици" }, "success": { - "message": "Success" + "message": "Успех" }, "viewCollection": { "message": "Преглед на колекцията" @@ -7907,7 +7907,7 @@ "message": "Не може да добавяте себе си към групи." }, "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "message": "Известие: от 2 май 2024г. неразпределените елементи на организациите вече няма се виждат в изгледа с „Всички трезори“ на различните устройства, а ще бъдат достъпни само през Административната конзола. Добавете тези елементи към някоя колекция в Административната конзола, за да станат видими." }, "unassignedItemsBannerNotice": { "message": "Известие: неразпределените елементи в организацията вече няма да се виждат в изгледа с всички трезори на различните устройства, а са достъпни само през Административната конзола." @@ -7966,79 +7966,88 @@ "message": "Грешка при задаването на целева папка." }, "integrationsAndSdks": { - "message": "Integrations & SDKs", + "message": "Интеграции и набори за разработка (SDK)", "description": "The title for the section that deals with integrations and SDKs." }, "integrations": { - "message": "Integrations" + "message": "Интеграции" }, "integrationsDesc": { - "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + "message": "Автоматично синхронизиране на тайните от „Управлението на тайни“ на Битуорден към външна услуга." }, "sdks": { - "message": "SDKs" + "message": "Набори за разработка (SDK)" }, "sdksDesc": { - "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + "message": "Използвайте набора за разработка (SDK) за Управлението на тайни на Битуорден със следните програмни езици, за да създадете свои собствени приложения." }, "setUpGithubActions": { - "message": "Set up Github Actions" + "message": "Настройка на действия в Github" }, "setUpGitlabCICD": { - "message": "Set up GitLab CI/CD" + "message": "Настройка на GitLab CI/CD" }, "setUpAnsible": { - "message": "Set up Ansible" + "message": "Настройка на Ansible" }, "cSharpSDKRepo": { - "message": "View C# repository" + "message": "Преглед на хранилището за C#" }, "cPlusPlusSDKRepo": { - "message": "View C++ repository" + "message": "Преглед на хранилището за C++" }, "jsWebAssemblySDKRepo": { - "message": "View JS WebAssembly repository" + "message": "Преглед на хранилището за JS WebAssembly" }, "javaSDKRepo": { - "message": "View Java repository" + "message": "Преглед на хранилището за Java" }, "pythonSDKRepo": { - "message": "View Python repository" + "message": "Преглед на хранилището за Python" }, "phpSDKRepo": { - "message": "View php repository" + "message": "Преглед на хранилището за php" }, "rubySDKRepo": { - "message": "View Ruby repository" + "message": "Преглед на хранилището за Ruby" }, "goSDKRepo": { - "message": "View Go repository" + "message": "Преглед на хранилището за Go" }, "createNewClientToManageAsProvider": { - "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + "message": "Създайте нова организация, която да управлявате като доставчик. Допълнителните места ще бъдат отразени в следващия платежен период." }, "selectAPlan": { - "message": "Select a plan" + "message": "Изберете план" }, "thirtyFivePercentDiscount": { - "message": "35% Discount" + "message": "Отстъпка от 35%" }, "monthPerMember": { - "message": "month per member" + "message": "на месец за член" }, "seats": { - "message": "Seats" + "message": "Места" }, "addOrganization": { - "message": "Add organization" + "message": "Добавяне на организация" }, "createdNewClient": { - "message": "Successfully created new client" + "message": "Новият клиент е създаден успешно" }, "noAccess": { - "message": "No access" + "message": "Нямате достъп" }, "collectionAdminConsoleManaged": { - "message": "This collection is only accessible from the admin console" + "message": "Тази колекция е достъпна само през административната конзола" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/bn/messages.json b/apps/web/src/locales/bn/messages.json index 46d0e574ce..9b60b1149b 100644 --- a/apps/web/src/locales/bn/messages.json +++ b/apps/web/src/locales/bn/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/bs/messages.json b/apps/web/src/locales/bs/messages.json index 85880f9cbf..3b2b0f91bf 100644 --- a/apps/web/src/locales/bs/messages.json +++ b/apps/web/src/locales/bs/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/ca/messages.json b/apps/web/src/locales/ca/messages.json index cc072c3b96..bf3071b506 100644 --- a/apps/web/src/locales/ca/messages.json +++ b/apps/web/src/locales/ca/messages.json @@ -7607,7 +7607,7 @@ "message": "Portal del proveïdor" }, "success": { - "message": "Success" + "message": "Èxit" }, "viewCollection": { "message": "Mostra col·lecció" @@ -7637,10 +7637,10 @@ "message": "No s'ha assignat cap col·lecció" }, "successfullyAssignedCollections": { - "message": "Successfully assigned collections" + "message": "Col·leccions assignades correctament" }, "bulkCollectionAssignmentWarning": { - "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "message": "Heu seleccionat $TOTAL_COUNT$ elements. No podeu actualitzar-ne $READONLY_COUNT$ dels quals perquè no teniu permisos d'edició.", "placeholders": { "total_count": { "content": "$1", @@ -7656,10 +7656,10 @@ "message": "Elements" }, "assignedSeats": { - "message": "Assigned seats" + "message": "Seients assignats" }, "assigned": { - "message": "Assigned" + "message": "Assignat" }, "used": { "message": "Utilitzat" @@ -7668,40 +7668,40 @@ "message": "Queden" }, "unlinkOrganization": { - "message": "Unlink organization" + "message": "Desenllaça l'organització" }, "manageSeats": { - "message": "MANAGE SEATS" + "message": "GESTIONA SEIENTS" }, "manageSeatsDescription": { - "message": "Adjustments to seats will be reflected in the next billing cycle." + "message": "Els ajustos dels seients es reflectiran en el pròxim cicle de facturació." }, "unassignedSeatsDescription": { - "message": "Unassigned subscription seats" + "message": "Seients de subscripció no assignats" }, "purchaseSeatDescription": { - "message": "Additional seats purchased" + "message": "Seients addicionals adquirits" }, "assignedSeatCannotUpdate": { - "message": "Assigned Seats can not be updated. Please contact your organization owner for assistance." + "message": "Els seients assignats no es poden actualitzar. Poseu-vos en contacte amb el propietari de l'organització per obtenir ajuda." }, "subscriptionUpdateFailed": { - "message": "Subscription update failed" + "message": "L'actualització de la subscripció ha fallat" }, "trial": { - "message": "Trial", + "message": "Prova", "description": "A subscription status label." }, "pastDue": { - "message": "Past due", + "message": "Vençuda", "description": "A subscription status label" }, "subscriptionExpired": { - "message": "Subscription expired", + "message": "La subscripció ha caducat", "description": "The date header used when a subscription is past due." }, "pastDueWarningForChargeAutomatically": { - "message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "message": "Teniu un període de gràcia de $DAYS$ dies a partir de la data de caducitat de la vostra subscripció per mantenir-la. Resoleu les factures vençudes abans de $SUSPENSION_DATE$.", "placeholders": { "days": { "content": "$1", @@ -7715,7 +7715,7 @@ "description": "A warning shown to the user when their subscription is past due and they are charged automatically." }, "pastDueWarningForSendInvoice": { - "message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "message": "Teniu un període de gràcia de $DAYS$ dies a partir de la data en què cal que la primera factura no pagada mantinga la subscripció. Resoleu les factures vençudes abans de $SUSPENSION_DATE$.", "placeholders": { "days": { "content": "$1", @@ -7729,22 +7729,22 @@ "description": "A warning shown to the user when their subscription is past due and they pay via invoice." }, "unpaidInvoice": { - "message": "Unpaid invoice", + "message": "Factura no pagada", "description": "The header of a warning box shown to a user whose subscription is unpaid." }, "toReactivateYourSubscription": { - "message": "To reactivate your subscription, please resolve the past due invoices.", + "message": "Per reactivar la subscripció, resoleu les factures vençudes.", "description": "The body of a warning box shown to a user whose subscription is unpaid." }, "cancellationDate": { - "message": "Cancellation date", + "message": "Data de cancel·lació", "description": "The date header used when a subscription is cancelled." }, "machineAccountsCannotCreate": { - "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + "message": "No es poden crear comptes de màquina en organitzacions suspeses. Poseu-vos en contacte amb el propietari de l'organització per obtenir ajuda." }, "machineAccount": { - "message": "Machine account", + "message": "Compte de màquina", "description": "A machine user which can be used to automate processes and access secrets in the system." }, "machineAccounts": { @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/cs/messages.json b/apps/web/src/locales/cs/messages.json index 6b82766cd3..b6e62aef9a 100644 --- a/apps/web/src/locales/cs/messages.json +++ b/apps/web/src/locales/cs/messages.json @@ -7907,7 +7907,7 @@ "message": "Do skupiny nemůžete přidat sami sebe." }, "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "message": "Upozornění: Dne 2. května 2024 již nebudou nepřiřazené položky organizace viditelné v zobrazení Všechny trezory ve všech zařízeních 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é." }, "unassignedItemsBannerNotice": { "message": "Upozornění: Nepřiřazené položky organizace již nejsou viditelné ve vašem zobrazení všech trezorů napříč zařízeními a jsou nyní přístupné pouze v konzoli správce." @@ -7966,53 +7966,53 @@ "message": "Chyba při přiřazování cílové složky." }, "integrationsAndSdks": { - "message": "Integrations & SDKs", + "message": "Integrace a SDK", "description": "The title for the section that deals with integrations and SDKs." }, "integrations": { - "message": "Integrations" + "message": "Integrace" }, "integrationsDesc": { - "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + "message": "Automaticky synchronizuje tajné klíče se správce tajných klíčů Bitwardenu do služby třetí strany." }, "sdks": { - "message": "SDKs" + "message": "SDK" }, "sdksDesc": { - "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + "message": "Použije SDK správce tajných klíčů Bitwardenu v následujících programovacích jazycích k vytvoření vlastních aplikací." }, "setUpGithubActions": { - "message": "Set up Github Actions" + "message": "Nastavit akce GitHubu" }, "setUpGitlabCICD": { - "message": "Set up GitLab CI/CD" + "message": "Nastavit GitLab CI/CD" }, "setUpAnsible": { - "message": "Set up Ansible" + "message": "Nastavit Ansible" }, "cSharpSDKRepo": { - "message": "View C# repository" + "message": "Zobrazit repozitář C#" }, "cPlusPlusSDKRepo": { - "message": "View C++ repository" + "message": "Zobrazit repozitář C++" }, "jsWebAssemblySDKRepo": { - "message": "View JS WebAssembly repository" + "message": "Zobrazit repozitář JS WebAssembly" }, "javaSDKRepo": { - "message": "View Java repository" + "message": "Zobrazit repozitář Java" }, "pythonSDKRepo": { - "message": "View Python repository" + "message": "Zobrazit repozitář Python" }, "phpSDKRepo": { - "message": "View php repository" + "message": "Zobrazit repozitář PHP" }, "rubySDKRepo": { - "message": "View Ruby repository" + "message": "Zobrazit repozitář Ruby" }, "goSDKRepo": { - "message": "View Go repository" + "message": "Zobrazit repozitář Go" }, "createNewClientToManageAsProvider": { "message": "Vytvořte novou klientskou organizaci pro správu jako poskytovatele. Další uživatelé budou reflektováni v dalším platebním cyklu." @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/cy/messages.json b/apps/web/src/locales/cy/messages.json index 7394c3fe2a..961b3c93a3 100644 --- a/apps/web/src/locales/cy/messages.json +++ b/apps/web/src/locales/cy/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/da/messages.json b/apps/web/src/locales/da/messages.json index 081fef7865..e5042229fe 100644 --- a/apps/web/src/locales/da/messages.json +++ b/apps/web/src/locales/da/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "Denne samling er kun tilgængelig via Admin-konsol" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/de/messages.json b/apps/web/src/locales/de/messages.json index 3fb7598416..bd2e0946f6 100644 --- a/apps/web/src/locales/de/messages.json +++ b/apps/web/src/locales/de/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/el/messages.json b/apps/web/src/locales/el/messages.json index 742a76402d..ed374326fe 100644 --- a/apps/web/src/locales/el/messages.json +++ b/apps/web/src/locales/el/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/en_GB/messages.json b/apps/web/src/locales/en_GB/messages.json index 66f728cb4d..3b0a99715f 100644 --- a/apps/web/src/locales/en_GB/messages.json +++ b/apps/web/src/locales/en_GB/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/en_IN/messages.json b/apps/web/src/locales/en_IN/messages.json index c0c77d3d78..1868130a1c 100644 --- a/apps/web/src/locales/en_IN/messages.json +++ b/apps/web/src/locales/en_IN/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/eo/messages.json b/apps/web/src/locales/eo/messages.json index 2a6db2ea4b..88509a8299 100644 --- a/apps/web/src/locales/eo/messages.json +++ b/apps/web/src/locales/eo/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/es/messages.json b/apps/web/src/locales/es/messages.json index 9f0f085e9d..e851539df1 100644 --- a/apps/web/src/locales/es/messages.json +++ b/apps/web/src/locales/es/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/et/messages.json b/apps/web/src/locales/et/messages.json index 0c3f7cb300..1cef83a823 100644 --- a/apps/web/src/locales/et/messages.json +++ b/apps/web/src/locales/et/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/eu/messages.json b/apps/web/src/locales/eu/messages.json index c315d62520..bf79583e58 100644 --- a/apps/web/src/locales/eu/messages.json +++ b/apps/web/src/locales/eu/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/fa/messages.json b/apps/web/src/locales/fa/messages.json index 32feda3edd..0621239c5e 100644 --- a/apps/web/src/locales/fa/messages.json +++ b/apps/web/src/locales/fa/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/fi/messages.json b/apps/web/src/locales/fi/messages.json index 377f7b084f..4cc682caa2 100644 --- a/apps/web/src/locales/fi/messages.json +++ b/apps/web/src/locales/fi/messages.json @@ -630,7 +630,7 @@ "message": "Suojausavain ei kelpaa. Yritä uudelleen." }, "twoFactorForPasskeysNotSupportedOnClientUpdateToLogIn": { - "message": "Kaksivaiheista salausavainkirjautumista ei tueta. Päivitä sovellus kirjautuaksesi sisään." + "message": "Kaksivaiheista suojausavainkirjautumista ei tueta. Päivitä sovellus kirjautuaksesi sisään." }, "loginWithPasskeyInfo": { "message": "Käytä generoitua suojausavainta, joka kirjaa sinut automaattisesti sisään ilman salasanaa. Henkilöllisyytesi vahvistetaan kasvojen tunnistuksen tai sormenjäljen kataisilla biometrisillä tiedoilla, tai jollakin muulla FIDO2-suojausratkaisulla." @@ -2799,7 +2799,7 @@ "message": "Verkkoholvi" }, "cli": { - "message": "komentorivi" + "message": "Komentorivi" }, "bitWebVault": { "message": "Bitwarden Verkkoholvi" @@ -4948,10 +4948,10 @@ "message": "Uusi asiakasorganisaatio" }, "newClientOrganizationDesc": { - "message": "Luo uusi asiakasorganisaatio, joka liitetään sinuun toimittajana. Voit käyttää ja hallita tätä organisaatiota." + "message": "Luo uusi asiakasorganisaatio, jonka toimittajaksi sinut määritetään. Voit käyttää ja hallita tätä organisaatiota." }, "newClient": { - "message": "Uusi pääte" + "message": "Uusi asiakas" }, "addExistingOrganization": { "message": "Lisää olemassa oleva organisaatio" @@ -6238,7 +6238,7 @@ "description": "Title for creating a new project." }, "softDeleteSecretWarning": { - "message": "Salaisuuksien poistaminen voi vaikuttaa olemassa oleviin integrointeihin.", + "message": "Salaisuuksien poistaminen voi vaikuttaa olemassa oleviin integraatioihin.", "description": "Warns that deleting secrets can have consequences on integrations" }, "softDeletesSuccessToast": { @@ -7071,7 +7071,7 @@ "description": "The individual description shown to the user when the user doesn't have access to delete a project." }, "smProjectsDeleteBulkConfirmation": { - "message": "Seuraavien projektien poistaminen ei ole mahdollista. Haluatko jatkaa?", + "message": "Seuraavia projekteja ei ole mahdollista poistaa. Haluatko jatkaa?", "description": "The message shown to the user when bulk deleting projects and the user doesn't have access to some projects." }, "updateKdfSettings": { @@ -7217,7 +7217,7 @@ "message": "Tilille ei ole asetettu pääsalasanaa" }, "removeOrgUserNoMasterPasswordDesc": { - "message": "Käyttäjän $USER$ poistaminen asettamatta hänen tililleen pääsalasanaa voi estää häntä kirjautumasta hänen omalle tililleen. Haluatko varmasti jatkaa?", + "message": "Käyttäjän $USER$ poistaminen asettamatta hänen tililleen pääsalasanaa voi estää häntä kirjautumasta heidän omalle tililleen. Haluatko varmasti jatkaa?", "placeholders": { "user": { "content": "$1", @@ -7484,7 +7484,7 @@ "message": "Laajennuksella voit tallentaa kirjautumistietoja ja automaattitäyttää lomakkeita avaamatta verkkosovellusta." }, "projectAccessUpdated": { - "message": "Projektin käyttöoikeudet on muutettu" + "message": "Projektin käyttöoikeuksia muutettiin" }, "unexpectedErrorSend": { "message": "Odottamaton virhe ladattaessa Sendiä. Yritä myöhemmin uudelleen." @@ -7514,7 +7514,7 @@ "message": "Ylläpitäjät voivat käyttää ja hallinnoida kokoelmia." }, "serviceAccountAccessUpdated": { - "message": "Palvelutilin oikeuksia muutettiin" + "message": "Palvelutilin käyttöoikeuksia muutettiin" }, "commonImportFormats": { "message": "Yleiset muodot", @@ -7595,7 +7595,7 @@ "message": "Ilmainen 1 vuoden ajan" }, "newWebApp": { - "message": "Tervetuloa uuteen, entistä parempaan verkkosovellukseen. Lue lisää siitä, mikä on muuttunut." + "message": "Tervetuloa uuteen, entistä parempaan verkkosovellukseen. Katso mikä on muuttunut." }, "releaseBlog": { "message": "Lue julkaisublogia" @@ -7607,10 +7607,10 @@ "message": "Toimittajaportaali" }, "success": { - "message": "Success" + "message": "Onnistui" }, "viewCollection": { - "message": "Tarkastele kokoelmaa" + "message": "Näytä kokoelma" }, "restrictedGroupAccess": { "message": "Et voi lisätä itseäsi ryhmiin." @@ -7640,7 +7640,7 @@ "message": "Kokoelmat määritettiin" }, "bulkCollectionAssignmentWarning": { - "message": "Olet valinnut $TOTAL_COUNT$ kohdetta. Et voi päivittää näistä $READONLY_COUNT$ kohdetta, koska käyttöoikeutesi eivät salli muokkausta.", + "message": "Olet valinnut $TOTAL_COUNT$ kohdetta. Näistä $READONLY_COUNT$ et voi muuttaa, koska käyttöoikeutesi eivät salli muokkausta.", "placeholders": { "total_count": { "content": "$1", @@ -7668,7 +7668,7 @@ "message": "Jäljellä" }, "unlinkOrganization": { - "message": "Irrota organisaatio" + "message": "Poista organisaatioliitos" }, "manageSeats": { "message": "HALLITSE KÄYTTÄJÄPAIKKOJA" @@ -7904,33 +7904,33 @@ "message": "Konetilin oikeuksia muutettiin" }, "restrictedGroupAccessDesc": { - "message": "You cannot add yourself to a group." + "message": "Et voi lisätä itseäsi ryhmään." }, "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "message": "Huomioi: Alkaen 2. toukokuuta 2024, määrittämättömät organisaatiokohteet eivät enää näy laitteidesi \"Kaikki holvit\" -näkymissä, vaan ne ovat käytettävissä vain Hallintapaneelista." }, "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + "message": "Huomioi: Määrittämättömät organisaatiokohteet eivät enää näy laitteidesi \"Kaikki holvit\" -näkymissä, vaan ne ovat käytettävissä vain Hallintapaneelista." }, "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + "message": "Huomioi: Alkaen 16. toukokuuta 2024, määrittämättömät organisaatiokohteet eivät enää näy laitteidesi \"Kaikki holvit\" -näkymissä, vaan ne ovat käytettävissä vain Hallintapaneelista." }, "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", + "message": "Määritä nämä kohteet kokoelmaan", "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.", + "message": ", jotta ne näkyvät.", "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." }, "deleteProvider": { - "message": "Delete provider" + "message": "Poista toimittaja" }, "deleteProviderConfirmation": { - "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + "message": "Toimittajan poisto on pysyvää ja peruuttamatonta. Vahvista palveluntarjoajan ja kaikkien siihen liittyvien tietojen poisto syöttämällä pääsalasanasi." }, "deleteProviderName": { - "message": "Cannot delete $ID$", + "message": "Toimittajaa $ID$ ei ole mahdollista poistaa", "placeholders": { "id": { "content": "$1", @@ -7939,7 +7939,7 @@ } }, "deleteProviderWarningDesc": { - "message": "You must unlink all clients before you can delete $ID$", + "message": "Kaikki liitetyt asiakkaat on poistettava ennen toimittajan $ID$ poistoa.", "placeholders": { "id": { "content": "$1", @@ -7948,97 +7948,106 @@ } }, "providerDeleted": { - "message": "Provider deleted" + "message": "Toimittaja poistettiin" }, "providerDeletedDesc": { - "message": "The Provider and all associated data has been deleted." + "message": "Toimittaja ja kaikki siihen liittyvät tiedot on poistettu." }, "deleteProviderRecoverConfirmDesc": { - "message": "You have requested to delete this Provider. Use the button below to confirm." + "message": "Olet pyytänyt tämän toimittajan poistoa. Vahvista alla olevalla painikeella." }, "deleteProviderWarning": { - "message": "Deleting your provider is permanent. It cannot be undone." + "message": "Toimittajasi poisto on pysyvä toimenpide, eikä sen peruminen ole mahdollista." }, "errorAssigningTargetCollection": { - "message": "Error assigning target collection." + "message": "Virhe määritettäessä kohdekokoelmaa." }, "errorAssigningTargetFolder": { - "message": "Error assigning target folder." + "message": "Virhe määritettäessä kohdekansiota." }, "integrationsAndSdks": { - "message": "Integrations & SDKs", + "message": "Integraatiot ja SDK:t", "description": "The title for the section that deals with integrations and SDKs." }, "integrations": { - "message": "Integrations" + "message": "Integraatiot" }, "integrationsDesc": { - "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + "message": "Synkronoi salaisuudet automaattisesti Bitwardenin Salaisuushallinnan ja ulkopuolisen palvelun välillä." }, "sdks": { - "message": "SDKs" + "message": "SDK:t" }, "sdksDesc": { - "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + "message": "Bitwardenin Salaisuushallinnan SDK:n avulla voit kehittää omia sovelluksiasi seuraavilla ohjelmointikielillä." }, "setUpGithubActions": { - "message": "Set up Github Actions" + "message": "Määritä GitHub Actions" }, "setUpGitlabCICD": { - "message": "Set up GitLab CI/CD" + "message": "Määritä GitLab CI/CD" }, "setUpAnsible": { - "message": "Set up Ansible" + "message": "Määritä Ansible" }, "cSharpSDKRepo": { - "message": "View C# repository" + "message": "Näytä C#-arkisto" }, "cPlusPlusSDKRepo": { - "message": "View C++ repository" + "message": "Näytä C++-arkisto" }, "jsWebAssemblySDKRepo": { - "message": "View JS WebAssembly repository" + "message": "Näytä JS WebAssembly -arkisto" }, "javaSDKRepo": { - "message": "View Java repository" + "message": "Näytä Java-arkisto" }, "pythonSDKRepo": { - "message": "View Python repository" + "message": "Näytä Python-arkisto" }, "phpSDKRepo": { - "message": "View php repository" + "message": "Näytä php-arkisto" }, "rubySDKRepo": { - "message": "View Ruby repository" + "message": "Näytä Ruby-arkisto" }, "goSDKRepo": { - "message": "View Go repository" + "message": "Näytä Go-arkisto" }, "createNewClientToManageAsProvider": { - "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + "message": "Luo uusi asiakasorganisaatio, jota hallitset toimittajana. Uudet käyttäjäpaikat näkyvät seuraavalla laskutusjaksolla." }, "selectAPlan": { - "message": "Select a plan" + "message": "Valitse tilaus" }, "thirtyFivePercentDiscount": { - "message": "35% Discount" + "message": "35 %:n alennus" }, "monthPerMember": { - "message": "month per member" + "message": "kuukaudessa/jäsen" }, "seats": { - "message": "Seats" + "message": "Käyttäjäpaikat" }, "addOrganization": { - "message": "Add organization" + "message": "Lisää organisaatio" }, "createdNewClient": { - "message": "Successfully created new client" + "message": "Uuden asiakkaan luonti onnistui." }, "noAccess": { - "message": "No access" + "message": "Käyttöoikeutta ei ole" }, "collectionAdminConsoleManaged": { - "message": "This collection is only accessible from the admin console" + "message": "Tämä kokoelma on käytettävissä vain hallintakonsolista" + }, + "organizationOptionsMenu": { + "message": "Näytä/piilota organisaatiovalikko" + }, + "vaultItemSelect": { + "message": "Valitse holvin kohde" + }, + "collectionItemSelect": { + "message": "Valitse kokoelman kohde" } } diff --git a/apps/web/src/locales/fil/messages.json b/apps/web/src/locales/fil/messages.json index 50c9f8d4e8..eb3094e043 100644 --- a/apps/web/src/locales/fil/messages.json +++ b/apps/web/src/locales/fil/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/fr/messages.json b/apps/web/src/locales/fr/messages.json index 2934176956..1f1f343897 100644 --- a/apps/web/src/locales/fr/messages.json +++ b/apps/web/src/locales/fr/messages.json @@ -7907,7 +7907,7 @@ "message": "Vous ne pouvez pas vous ajouter vous-même à un groupe." }, "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "message": "Remarque : le 2 mai 2024, les éléments d'organisation non assignés ne seront plus visibles dans votre vue Tous les coffres sur les appareils et seront uniquement accessibles via la Console Admin. Assignez ces éléments à une collection à partir de la Console Admin pour les rendre visibles." }, "unassignedItemsBannerNotice": { "message": "Remarque : Les éléments d'organisation non assignés ne sont plus visibles dans la vue Tous les coffres sur les appareils et ne sont maintenant accessibles que via la Console Admin." @@ -7966,53 +7966,53 @@ "message": "Erreur lors de l'assignation du dossier cible." }, "integrationsAndSdks": { - "message": "Integrations & SDKs", + "message": "Intégrations & SDKs", "description": "The title for the section that deals with integrations and SDKs." }, "integrations": { - "message": "Integrations" + "message": "Intégrations" }, "integrationsDesc": { - "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + "message": "Synchroniser automatiquement les secrets à partir du Secrets Manager de Bitwarden vers un service tiers." }, "sdks": { - "message": "SDKs" + "message": "SDK" }, "sdksDesc": { - "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + "message": "Utilisez Bitwarden Secrets Manager SDK dans les langages de programmation suivants pour construire vos propres applications." }, "setUpGithubActions": { - "message": "Set up Github Actions" + "message": "Configurer Github Actions" }, "setUpGitlabCICD": { - "message": "Set up GitLab CI/CD" + "message": "Configurer GitLab CI/CD" }, "setUpAnsible": { - "message": "Set up Ansible" + "message": "Configurer Ansible" }, "cSharpSDKRepo": { - "message": "View C# repository" + "message": "Afficher le dépôt C#" }, "cPlusPlusSDKRepo": { - "message": "View C++ repository" + "message": "Afficher le dépôt C++" }, "jsWebAssemblySDKRepo": { - "message": "View JS WebAssembly repository" + "message": "Afficher le dépôt JS WebAssembly" }, "javaSDKRepo": { - "message": "View Java repository" + "message": "Afficher le dépôt Java" }, "pythonSDKRepo": { - "message": "View Python repository" + "message": "Afficher le dépôt Python" }, "phpSDKRepo": { - "message": "View php repository" + "message": "Afficher le dépôt php" }, "rubySDKRepo": { - "message": "View Ruby repository" + "message": "Afficher le dépôt Ruby" }, "goSDKRepo": { - "message": "View Go repository" + "message": "Afficher le dépôt Go" }, "createNewClientToManageAsProvider": { "message": "Créez une nouvelle organisation de clients à gérer en tant que Fournisseur. Des sièges supplémentaires seront reflétés lors du prochain cycle de facturation." @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "Cette collection n'est accessible qu'à partir de la Console Admin" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/gl/messages.json b/apps/web/src/locales/gl/messages.json index ed04c3a3ef..13fa5f60c0 100644 --- a/apps/web/src/locales/gl/messages.json +++ b/apps/web/src/locales/gl/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/he/messages.json b/apps/web/src/locales/he/messages.json index b7b4d59227..b57005a76b 100644 --- a/apps/web/src/locales/he/messages.json +++ b/apps/web/src/locales/he/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/hi/messages.json b/apps/web/src/locales/hi/messages.json index 23321b27ef..818cc1cef5 100644 --- a/apps/web/src/locales/hi/messages.json +++ b/apps/web/src/locales/hi/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/hr/messages.json b/apps/web/src/locales/hr/messages.json index edc6adaf3b..ffeb8c2201 100644 --- a/apps/web/src/locales/hr/messages.json +++ b/apps/web/src/locales/hr/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/hu/messages.json b/apps/web/src/locales/hu/messages.json index 4ec6f15029..b85733325b 100644 --- a/apps/web/src/locales/hu/messages.json +++ b/apps/web/src/locales/hu/messages.json @@ -7607,7 +7607,7 @@ "message": "Szolgáltató portál" }, "success": { - "message": "Success" + "message": "Sikeres" }, "viewCollection": { "message": "Gyűjtemény megtekintése" @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "Ez a gyűjtemény csak az adminisztrátori konzolról érhető el." + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/id/messages.json b/apps/web/src/locales/id/messages.json index 3bc9ef4e62..df1af27271 100644 --- a/apps/web/src/locales/id/messages.json +++ b/apps/web/src/locales/id/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/it/messages.json b/apps/web/src/locales/it/messages.json index 11dec69959..04342f13b1 100644 --- a/apps/web/src/locales/it/messages.json +++ b/apps/web/src/locales/it/messages.json @@ -7607,7 +7607,7 @@ "message": "Portale Fornitori" }, "success": { - "message": "Success" + "message": "Successo" }, "viewCollection": { "message": "Visualizza raccolta" @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Attiva/Disattiva Menu Organizzazione" + }, + "vaultItemSelect": { + "message": "Seleziona elemento della cassaforte" + }, + "collectionItemSelect": { + "message": "Seleziona elemento della raccolta" } } diff --git a/apps/web/src/locales/ja/messages.json b/apps/web/src/locales/ja/messages.json index 17b58a4299..a701dd6e8d 100644 --- a/apps/web/src/locales/ja/messages.json +++ b/apps/web/src/locales/ja/messages.json @@ -7607,7 +7607,7 @@ "message": "プロバイダーポータル" }, "success": { - "message": "Success" + "message": "成功" }, "viewCollection": { "message": "コレクションを表示" @@ -7907,7 +7907,7 @@ "message": "あなた自身をグループに追加することはできません。" }, "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "message": "お知らせ:2024年5月2日に、 割り当てられていない組織アイテムはデバイス間のすべての保管庫ビューに表示されなくなり、管理コンソールからのみアクセス可能になります。 管理コンソールからコレクションにこれらのアイテムを割り当てると、表示できるようになります。" }, "unassignedItemsBannerNotice": { "message": "注意: 割り当てられていない組織アイテムは、デバイス間のすべての保管庫ビューでは表示されなくなり、管理コンソールからのみアクセスできるようになりました。" @@ -7966,79 +7966,88 @@ "message": "ターゲットフォルダーの割り当てに失敗しました。" }, "integrationsAndSdks": { - "message": "Integrations & SDKs", + "message": "連携&SDK", "description": "The title for the section that deals with integrations and SDKs." }, "integrations": { - "message": "Integrations" + "message": "システム連携" }, "integrationsDesc": { - "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + "message": "Bitwarden シークレットマネージャーのシークレットを、サードパーティーのサービスに自動的に同期します。" }, "sdks": { - "message": "SDKs" + "message": "SDK" }, "sdksDesc": { - "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + "message": "Bitwarden シークレットマネージャー SDK を以下のプログラミング言語で使用して、独自のアプリを構築できます。" }, "setUpGithubActions": { - "message": "Set up Github Actions" + "message": "Github アクションを設定" }, "setUpGitlabCICD": { - "message": "Set up GitLab CI/CD" + "message": "GitLab CI/CD の設定" }, "setUpAnsible": { - "message": "Set up Ansible" + "message": "Ansible を設定" }, "cSharpSDKRepo": { - "message": "View C# repository" + "message": "C# リポジトリを表示" }, "cPlusPlusSDKRepo": { - "message": "View C++ repository" + "message": "C++ リポジトリを表示" }, "jsWebAssemblySDKRepo": { - "message": "View JS WebAssembly repository" + "message": "JS WebAssembly リポジトリを表示" }, "javaSDKRepo": { - "message": "View Java repository" + "message": "Java リポジトリを表示" }, "pythonSDKRepo": { - "message": "View Python repository" + "message": "Python リポジトリを表示" }, "phpSDKRepo": { - "message": "View php repository" + "message": "PHP リポジトリを表示" }, "rubySDKRepo": { - "message": "View Ruby repository" + "message": "Ruby リポジトリを表示" }, "goSDKRepo": { - "message": "View Go repository" + "message": "Go リポジトリを表示" }, "createNewClientToManageAsProvider": { - "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + "message": "プロバイダーとして管理するための新しいクライアント組織を作成します。次の請求サイクルに追加のシートが反映されます。" }, "selectAPlan": { - "message": "Select a plan" + "message": "プランを選択" }, "thirtyFivePercentDiscount": { - "message": "35% Discount" + "message": "35%割引" }, "monthPerMember": { - "message": "month per member" + "message": "月/メンバーあたり" }, "seats": { - "message": "Seats" + "message": "シート" }, "addOrganization": { - "message": "Add organization" + "message": "組織を追加" }, "createdNewClient": { - "message": "Successfully created new client" + "message": "新しいクライアントを作成しました" }, "noAccess": { - "message": "No access" + "message": "アクセス不可" }, "collectionAdminConsoleManaged": { - "message": "This collection is only accessible from the admin console" + "message": "このコレクションは管理コンソールからのみアクセス可能です" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/ka/messages.json b/apps/web/src/locales/ka/messages.json index 6d34252b88..defe9e621d 100644 --- a/apps/web/src/locales/ka/messages.json +++ b/apps/web/src/locales/ka/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/km/messages.json b/apps/web/src/locales/km/messages.json index ed04c3a3ef..13fa5f60c0 100644 --- a/apps/web/src/locales/km/messages.json +++ b/apps/web/src/locales/km/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/kn/messages.json b/apps/web/src/locales/kn/messages.json index ae6f2c2a89..cfb188b580 100644 --- a/apps/web/src/locales/kn/messages.json +++ b/apps/web/src/locales/kn/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/ko/messages.json b/apps/web/src/locales/ko/messages.json index f9240f21aa..9dd856e656 100644 --- a/apps/web/src/locales/ko/messages.json +++ b/apps/web/src/locales/ko/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/lv/messages.json b/apps/web/src/locales/lv/messages.json index 0dc0dff097..b20f350231 100644 --- a/apps/web/src/locales/lv/messages.json +++ b/apps/web/src/locales/lv/messages.json @@ -7607,7 +7607,7 @@ "message": "Nodrošinātāju portāls" }, "success": { - "message": "Success" + "message": "Izdevās" }, "viewCollection": { "message": "Skatīt krājumu" @@ -8036,9 +8036,18 @@ "message": "Successfully created new client" }, "noAccess": { - "message": "No access" + "message": "Nav piekļuves" }, "collectionAdminConsoleManaged": { - "message": "This collection is only accessible from the admin console" + "message": "Šis krājums ir pieejams tikai pārvaldības konsolē" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/ml/messages.json b/apps/web/src/locales/ml/messages.json index 9b6d1ab565..c2b83a5e28 100644 --- a/apps/web/src/locales/ml/messages.json +++ b/apps/web/src/locales/ml/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/mr/messages.json b/apps/web/src/locales/mr/messages.json index ed04c3a3ef..13fa5f60c0 100644 --- a/apps/web/src/locales/mr/messages.json +++ b/apps/web/src/locales/mr/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/my/messages.json b/apps/web/src/locales/my/messages.json index ed04c3a3ef..13fa5f60c0 100644 --- a/apps/web/src/locales/my/messages.json +++ b/apps/web/src/locales/my/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/nb/messages.json b/apps/web/src/locales/nb/messages.json index 4e5fb13d15..2ffdc5214f 100644 --- a/apps/web/src/locales/nb/messages.json +++ b/apps/web/src/locales/nb/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/ne/messages.json b/apps/web/src/locales/ne/messages.json index f411847571..7854d49a7b 100644 --- a/apps/web/src/locales/ne/messages.json +++ b/apps/web/src/locales/ne/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/nl/messages.json b/apps/web/src/locales/nl/messages.json index 7bf9b4018f..b9f405cb06 100644 --- a/apps/web/src/locales/nl/messages.json +++ b/apps/web/src/locales/nl/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "Deze collectie is alleen toegankelijk vanaf de admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/nn/messages.json b/apps/web/src/locales/nn/messages.json index 3d54ea70fd..53ac66738e 100644 --- a/apps/web/src/locales/nn/messages.json +++ b/apps/web/src/locales/nn/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/or/messages.json b/apps/web/src/locales/or/messages.json index ed04c3a3ef..13fa5f60c0 100644 --- a/apps/web/src/locales/or/messages.json +++ b/apps/web/src/locales/or/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/pl/messages.json b/apps/web/src/locales/pl/messages.json index 57e0eb1929..c54b72c21c 100644 --- a/apps/web/src/locales/pl/messages.json +++ b/apps/web/src/locales/pl/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "Ta kolekcja jest dostępna tylko z konsoli administracyjnej" + }, + "organizationOptionsMenu": { + "message": "Przełącz menu organizacji" + }, + "vaultItemSelect": { + "message": "Wybierz element sejfu" + }, + "collectionItemSelect": { + "message": "Wybierz element kolekcji" } } diff --git a/apps/web/src/locales/pt_BR/messages.json b/apps/web/src/locales/pt_BR/messages.json index 4f43c2aad7..2b296694bb 100644 --- a/apps/web/src/locales/pt_BR/messages.json +++ b/apps/web/src/locales/pt_BR/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/pt_PT/messages.json b/apps/web/src/locales/pt_PT/messages.json index c2f2fbeee3..961d4531fa 100644 --- a/apps/web/src/locales/pt_PT/messages.json +++ b/apps/web/src/locales/pt_PT/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "Esta coleção só é acessível a partir da Consola de administração" + }, + "organizationOptionsMenu": { + "message": "Alternar menu da organização" + }, + "vaultItemSelect": { + "message": "Selecionar item do cofre" + }, + "collectionItemSelect": { + "message": "Selecionar item da coleção" } } diff --git a/apps/web/src/locales/ro/messages.json b/apps/web/src/locales/ro/messages.json index 1a47866ebb..bd735d8f75 100644 --- a/apps/web/src/locales/ro/messages.json +++ b/apps/web/src/locales/ro/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/ru/messages.json b/apps/web/src/locales/ru/messages.json index 85ba7d5785..67b23b0e89 100644 --- a/apps/web/src/locales/ru/messages.json +++ b/apps/web/src/locales/ru/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "Эта коллекция доступна только из консоли администратора" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/si/messages.json b/apps/web/src/locales/si/messages.json index e2c1e42427..0cd67041d3 100644 --- a/apps/web/src/locales/si/messages.json +++ b/apps/web/src/locales/si/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/sk/messages.json b/apps/web/src/locales/sk/messages.json index 234a4670b9..756723129f 100644 --- a/apps/web/src/locales/sk/messages.json +++ b/apps/web/src/locales/sk/messages.json @@ -344,7 +344,7 @@ "message": "Krstné meno" }, "middleName": { - "message": "Druhé meno" + "message": "Stredné meno" }, "lastName": { "message": "Priezvisko" @@ -2814,7 +2814,7 @@ "message": "Zmenené heslo k účtu." }, "enabledUpdated2fa": { - "message": "Dvojstupňové prihlasovanie zapnuté/aktualizované." + "message": "Dvojstupňové prihlasovanie uložené" }, "disabled2fa": { "message": "Dvojstupňové prihlasovanie vypnuté." @@ -3234,10 +3234,10 @@ "message": "Skupinový prístup" }, "groupAccessUserDesc": { - "message": "Upraviť skupiny, do ktorých patrí používateľ." + "message": "Povoľte členom prístup k zbierkam ich pridaním do jednej alebo viac skupín." }, "invitedUsers": { - "message": "Používatelia pozvaní." + "message": "Používatelia pozvaní" }, "resendInvitation": { "message": "Znovu poslať pozvánku" @@ -3246,7 +3246,7 @@ "message": "Znovu poslať e-mail" }, "hasBeenReinvited": { - "message": "$USER$ bol znovu pozvaný.", + "message": "$USER$ bol znovu pozvaný", "placeholders": { "user": { "content": "$1", @@ -3270,7 +3270,7 @@ } }, "confirmUsers": { - "message": "Potvrdiť používateľov" + "message": "Potvrdiť členov" }, "usersNeedConfirmed": { "message": "Máte používateľov, ktorí prijali pozvanie, ale ešte ich musíte potvrdiť. Používatelia nebudú mať prístup k organizácii, kým nebudú potvrdení." @@ -3294,13 +3294,13 @@ "message": "Skontrolujte si doručenú poštu, mali by ste obdržať odkaz pre verifikáciu." }, "emailVerified": { - "message": "Vaša emailová adresa bola overená." + "message": "Emailová adresa konta bola overená" }, "emailVerifiedFailed": { "message": "Overovanie zlyhalo. Skúste si odoslať nový verifikačný e-mail." }, "emailVerificationRequired": { - "message": "Vyžaduje sa overenie e-mailu" + "message": "Vyžaduje sa overenie e-mailom" }, "emailVerificationRequiredDesc": { "message": "Na používanie tejto funkcie musíte overiť svoj e-mail." @@ -7907,7 +7907,7 @@ "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "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." }, "unassignedItemsBannerNotice": { "message": "Upozornenie: Nepriradené položky organizácie už nie sú viditeľné v zobrazení Všetky trezory a sú prístupné iba cez Správcovskú konzolu." @@ -7924,13 +7924,13 @@ "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." }, "deleteProvider": { - "message": "Delete provider" + "message": "Odstrániť poskytovateľa" }, "deleteProviderConfirmation": { - "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + "message": "Odstránenie poskytovateľa je trvalé a nenávratné. Zadajte vaše hlavné heslo pre potvrdenie odstránenia poskytovateľa a súvisiacich dát." }, "deleteProviderName": { - "message": "Cannot delete $ID$", + "message": "$ID$ sa nedá odstrániť", "placeholders": { "id": { "content": "$1", @@ -7939,7 +7939,7 @@ } }, "deleteProviderWarningDesc": { - "message": "You must unlink all clients before you can delete $ID$", + "message": "Pred tým než budete môcť odstrániť $ID$, musíte odpojiť všetkých klientov", "placeholders": { "id": { "content": "$1", @@ -7948,16 +7948,16 @@ } }, "providerDeleted": { - "message": "Provider deleted" + "message": "Poskytovateľ odstránený" }, "providerDeletedDesc": { - "message": "The Provider and all associated data has been deleted." + "message": "Poskytovateľ a všetky súvisiace dáta boli odstránené." }, "deleteProviderRecoverConfirmDesc": { - "message": "You have requested to delete this Provider. Use the button below to confirm." + "message": "Požiadali ste o odstránenie tohto Poskytovateľa. Pre potvrdenie operácie použite tlačidlo nižšie." }, "deleteProviderWarning": { - "message": "Deleting your provider is permanent. It cannot be undone." + "message": "Odstránenie poskytovateľa je trvalé. Nedá sa vrátiť späť." }, "errorAssigningTargetCollection": { "message": "Chyba pri priraďovaní cieľovej kolekcie." @@ -7966,79 +7966,88 @@ "message": "Chyba pri priraďovaní cieľového priečinka." }, "integrationsAndSdks": { - "message": "Integrations & SDKs", + "message": "Integrácie a SDK", "description": "The title for the section that deals with integrations and SDKs." }, "integrations": { - "message": "Integrations" + "message": "Integrácie" }, "integrationsDesc": { - "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + "message": "Automaticky synchronizovať položky z Bitwarden Secrets Manager do služby tretej strany." }, "sdks": { - "message": "SDKs" + "message": "SDK" }, "sdksDesc": { - "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + "message": "Použite Bitwarden Secrets Manager SDK v následujúcich programovacích jazykoch pre vytvorenie vašej vlastnej aplikácie." }, "setUpGithubActions": { - "message": "Set up Github Actions" + "message": "Nastaviť Github Actions" }, "setUpGitlabCICD": { - "message": "Set up GitLab CI/CD" + "message": "Nastaviť GitLab CI/CD" }, "setUpAnsible": { - "message": "Set up Ansible" + "message": "Nastaviť Ansible" }, "cSharpSDKRepo": { - "message": "View C# repository" + "message": "Zobraziť C# repozitár" }, "cPlusPlusSDKRepo": { - "message": "View C++ repository" + "message": "Zobraziť C++ repozitár" }, "jsWebAssemblySDKRepo": { - "message": "View JS WebAssembly repository" + "message": "Zobraziť JS WebAssembly repozitár" }, "javaSDKRepo": { - "message": "View Java repository" + "message": "Zobraziť Java repozitár" }, "pythonSDKRepo": { - "message": "View Python repository" + "message": "Zobraziť Python repozitár" }, "phpSDKRepo": { - "message": "View php repository" + "message": "Zobraziť php repozitár" }, "rubySDKRepo": { - "message": "View Ruby repository" + "message": "Zobraziť Ruby repozitár" }, "goSDKRepo": { - "message": "View Go repository" + "message": "Zobraziť Go repozitár" }, "createNewClientToManageAsProvider": { - "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + "message": "Vytvoriť novú klientskú organizáciu ktorú môžete spravovať ako Poskytovateľ. Dodatočné sedenia sa prejavia v najbližšom fakturačnom období." }, "selectAPlan": { - "message": "Select a plan" + "message": "Vyberte plán" }, "thirtyFivePercentDiscount": { - "message": "35% Discount" + "message": "Zľava 35%" }, "monthPerMember": { "message": "month per member" }, "seats": { - "message": "Seats" + "message": "Sedenia" }, "addOrganization": { - "message": "Add organization" + "message": "Pridať organizáciu" }, "createdNewClient": { - "message": "Successfully created new client" + "message": "Nový klient úspešne vytvorený" }, "noAccess": { - "message": "No access" + "message": "Žiadny prístup" }, "collectionAdminConsoleManaged": { - "message": "This collection is only accessible from the admin console" + "message": "Táto zbierka je dostupná iba z administrátorskej konzoly" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/sl/messages.json b/apps/web/src/locales/sl/messages.json index c209ef05bd..dfde437380 100644 --- a/apps/web/src/locales/sl/messages.json +++ b/apps/web/src/locales/sl/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/sr/messages.json b/apps/web/src/locales/sr/messages.json index a59b6a8855..f927113fad 100644 --- a/apps/web/src/locales/sr/messages.json +++ b/apps/web/src/locales/sr/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/sr_CS/messages.json b/apps/web/src/locales/sr_CS/messages.json index 1ff89501f3..d6c354f4da 100644 --- a/apps/web/src/locales/sr_CS/messages.json +++ b/apps/web/src/locales/sr_CS/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/sv/messages.json b/apps/web/src/locales/sv/messages.json index 389f1682a8..21a100c5af 100644 --- a/apps/web/src/locales/sv/messages.json +++ b/apps/web/src/locales/sv/messages.json @@ -7966,17 +7966,17 @@ "message": "Error assigning target folder." }, "integrationsAndSdks": { - "message": "Integrations & SDKs", + "message": "Integrationer och SDK:er", "description": "The title for the section that deals with integrations and SDKs." }, "integrations": { - "message": "Integrations" + "message": "Integrationer" }, "integrationsDesc": { - "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + "message": "Synkronisera automatiskt hemligheter från Bitwarden Secrets Manager till en tredjepartstjänst." }, "sdks": { - "message": "SDKs" + "message": "SDK:er" }, "sdksDesc": { "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." @@ -7985,10 +7985,10 @@ "message": "Set up Github Actions" }, "setUpGitlabCICD": { - "message": "Set up GitLab CI/CD" + "message": "Ställ in GitLab CI/CD" }, "setUpAnsible": { - "message": "Set up Ansible" + "message": "Ställ in Ansible" }, "cSharpSDKRepo": { "message": "View C# repository" @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/te/messages.json b/apps/web/src/locales/te/messages.json index ed04c3a3ef..13fa5f60c0 100644 --- a/apps/web/src/locales/te/messages.json +++ b/apps/web/src/locales/te/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/th/messages.json b/apps/web/src/locales/th/messages.json index ec114aad90..929c91e74b 100644 --- a/apps/web/src/locales/th/messages.json +++ b/apps/web/src/locales/th/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/tr/messages.json b/apps/web/src/locales/tr/messages.json index 9177321cb5..1087bc89bb 100644 --- a/apps/web/src/locales/tr/messages.json +++ b/apps/web/src/locales/tr/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/uk/messages.json b/apps/web/src/locales/uk/messages.json index ecb31b6da0..c9434d72d6 100644 --- a/apps/web/src/locales/uk/messages.json +++ b/apps/web/src/locales/uk/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "Ця збірка доступна тільки з консолі адміністратора" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/vi/messages.json b/apps/web/src/locales/vi/messages.json index 0696e226e0..81fd3c2820 100644 --- a/apps/web/src/locales/vi/messages.json +++ b/apps/web/src/locales/vi/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/zh_CN/messages.json b/apps/web/src/locales/zh_CN/messages.json index f6093d9bc7..a9a0d85457 100644 --- a/apps/web/src/locales/zh_CN/messages.json +++ b/apps/web/src/locales/zh_CN/messages.json @@ -7939,7 +7939,7 @@ } }, "deleteProviderWarningDesc": { - "message": "You must unlink all clients before you can delete $ID$", + "message": "删除 $ID$ 之前,您必须取消链接所有的客户端。", "placeholders": { "id": { "content": "$1", @@ -8018,7 +8018,7 @@ "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." }, "selectAPlan": { - "message": "Select a plan" + "message": "选择套餐" }, "thirtyFivePercentDiscount": { "message": "35% Discount" @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } diff --git a/apps/web/src/locales/zh_TW/messages.json b/apps/web/src/locales/zh_TW/messages.json index 1bc56d2157..42e47a6e31 100644 --- a/apps/web/src/locales/zh_TW/messages.json +++ b/apps/web/src/locales/zh_TW/messages.json @@ -8040,5 +8040,14 @@ }, "collectionAdminConsoleManaged": { "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" } } From a2fc666823a696c39b6a0adcd7305d73250baf9a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 19 Apr 2024 19:40:56 +0000 Subject: [PATCH 240/351] Autosync the updated translations (#8838) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/browser/src/_locales/ar/messages.json | 4 +- apps/browser/src/_locales/az/messages.json | 4 +- apps/browser/src/_locales/be/messages.json | 4 +- apps/browser/src/_locales/bg/messages.json | 6 +- apps/browser/src/_locales/bn/messages.json | 4 +- apps/browser/src/_locales/bs/messages.json | 4 +- apps/browser/src/_locales/ca/messages.json | 4 +- apps/browser/src/_locales/cs/messages.json | 4 +- apps/browser/src/_locales/cy/messages.json | 4 +- apps/browser/src/_locales/da/messages.json | 4 +- apps/browser/src/_locales/de/messages.json | 4 +- apps/browser/src/_locales/el/messages.json | 4 +- apps/browser/src/_locales/en_GB/messages.json | 4 +- apps/browser/src/_locales/en_IN/messages.json | 4 +- apps/browser/src/_locales/es/messages.json | 4 +- apps/browser/src/_locales/et/messages.json | 4 +- apps/browser/src/_locales/eu/messages.json | 4 +- apps/browser/src/_locales/fa/messages.json | 4 +- apps/browser/src/_locales/fi/messages.json | 14 +-- apps/browser/src/_locales/fil/messages.json | 4 +- apps/browser/src/_locales/fr/messages.json | 4 +- apps/browser/src/_locales/gl/messages.json | 4 +- apps/browser/src/_locales/he/messages.json | 4 +- apps/browser/src/_locales/hi/messages.json | 4 +- apps/browser/src/_locales/hr/messages.json | 4 +- apps/browser/src/_locales/hu/messages.json | 6 +- apps/browser/src/_locales/id/messages.json | 4 +- apps/browser/src/_locales/it/messages.json | 6 +- apps/browser/src/_locales/ja/messages.json | 4 +- apps/browser/src/_locales/ka/messages.json | 4 +- apps/browser/src/_locales/km/messages.json | 4 +- apps/browser/src/_locales/kn/messages.json | 4 +- apps/browser/src/_locales/ko/messages.json | 4 +- apps/browser/src/_locales/lt/messages.json | 4 +- apps/browser/src/_locales/lv/messages.json | 6 +- apps/browser/src/_locales/ml/messages.json | 4 +- apps/browser/src/_locales/mr/messages.json | 4 +- apps/browser/src/_locales/my/messages.json | 4 +- apps/browser/src/_locales/nb/messages.json | 4 +- apps/browser/src/_locales/ne/messages.json | 4 +- apps/browser/src/_locales/nl/messages.json | 4 +- apps/browser/src/_locales/nn/messages.json | 4 +- apps/browser/src/_locales/or/messages.json | 4 +- apps/browser/src/_locales/pl/messages.json | 4 +- apps/browser/src/_locales/pt_BR/messages.json | 4 +- apps/browser/src/_locales/pt_PT/messages.json | 4 +- apps/browser/src/_locales/ro/messages.json | 4 +- apps/browser/src/_locales/ru/messages.json | 4 +- apps/browser/src/_locales/si/messages.json | 4 +- apps/browser/src/_locales/sk/messages.json | 6 +- apps/browser/src/_locales/sl/messages.json | 4 +- apps/browser/src/_locales/sr/messages.json | 4 +- apps/browser/src/_locales/sv/messages.json | 4 +- apps/browser/src/_locales/te/messages.json | 4 +- apps/browser/src/_locales/th/messages.json | 4 +- apps/browser/src/_locales/tr/messages.json | 4 +- apps/browser/src/_locales/uk/messages.json | 4 +- apps/browser/src/_locales/vi/messages.json | 4 +- apps/browser/src/_locales/zh_CN/messages.json | 12 +- apps/browser/src/_locales/zh_TW/messages.json | 4 +- apps/browser/store/locales/ar/copy.resx | 108 ++++++++++-------- apps/browser/store/locales/az/copy.resx | 107 +++++++++-------- apps/browser/store/locales/be/copy.resx | 104 +++++++++++------ apps/browser/store/locales/bg/copy.resx | 107 +++++++++-------- apps/browser/store/locales/bn/copy.resx | 103 ++++++++++------- apps/browser/store/locales/bs/copy.resx | 103 ++++++++++------- apps/browser/store/locales/ca/copy.resx | 107 +++++++++-------- apps/browser/store/locales/cs/copy.resx | 107 +++++++++-------- apps/browser/store/locales/cy/copy.resx | 107 +++++++++-------- apps/browser/store/locales/da/copy.resx | 107 +++++++++-------- apps/browser/store/locales/de/copy.resx | 107 +++++++++-------- apps/browser/store/locales/el/copy.resx | 107 +++++++++-------- apps/browser/store/locales/en_GB/copy.resx | 103 ++++++++++------- apps/browser/store/locales/en_IN/copy.resx | 103 ++++++++++------- apps/browser/store/locales/es/copy.resx | 107 +++++++++-------- apps/browser/store/locales/et/copy.resx | 103 ++++++++++------- apps/browser/store/locales/eu/copy.resx | 107 +++++++++-------- apps/browser/store/locales/fa/copy.resx | 108 ++++++++++-------- apps/browser/store/locales/fi/copy.resx | 107 +++++++++-------- apps/browser/store/locales/fil/copy.resx | 103 ++++++++++++----- apps/browser/store/locales/fr/copy.resx | 107 +++++++++-------- apps/browser/store/locales/gl/copy.resx | 107 +++++++++-------- apps/browser/store/locales/he/copy.resx | 103 ++++++++++------- apps/browser/store/locales/hi/copy.resx | 103 ++++++++++------- apps/browser/store/locales/hr/copy.resx | 108 ++++++++++-------- apps/browser/store/locales/hu/copy.resx | 104 ++++++++++------- apps/browser/store/locales/id/copy.resx | 103 ++++++++++------- apps/browser/store/locales/it/copy.resx | 107 +++++++++-------- apps/browser/store/locales/ja/copy.resx | 107 +++++++++-------- apps/browser/store/locales/ka/copy.resx | 103 ++++++++++------- apps/browser/store/locales/km/copy.resx | 103 ++++++++++------- apps/browser/store/locales/kn/copy.resx | 108 ++++++++++-------- apps/browser/store/locales/ko/copy.resx | 107 +++++++++-------- apps/browser/store/locales/lt/copy.resx | 107 +++++++++-------- apps/browser/store/locales/lv/copy.resx | 107 +++++++++-------- apps/browser/store/locales/ml/copy.resx | 99 ++++++++++------ apps/browser/store/locales/mr/copy.resx | 103 ++++++++++------- apps/browser/store/locales/my/copy.resx | 103 ++++++++++------- apps/browser/store/locales/nb/copy.resx | 103 ++++++++++------- apps/browser/store/locales/ne/copy.resx | 103 ++++++++++------- apps/browser/store/locales/nl/copy.resx | 108 ++++++++++-------- apps/browser/store/locales/nn/copy.resx | 103 ++++++++++------- apps/browser/store/locales/or/copy.resx | 103 ++++++++++------- apps/browser/store/locales/pl/copy.resx | 108 ++++++++++-------- apps/browser/store/locales/pt_BR/copy.resx | 107 +++++++++-------- apps/browser/store/locales/pt_PT/copy.resx | 107 +++++++++-------- apps/browser/store/locales/ro/copy.resx | 107 +++++++++-------- apps/browser/store/locales/ru/copy.resx | 108 ++++++++++-------- apps/browser/store/locales/si/copy.resx | 103 ++++++++++------- apps/browser/store/locales/sk/copy.resx | 107 +++++++++-------- apps/browser/store/locales/sl/copy.resx | 108 ++++++++++-------- apps/browser/store/locales/sr/copy.resx | 107 +++++++++-------- apps/browser/store/locales/sv/copy.resx | 107 +++++++++-------- apps/browser/store/locales/te/copy.resx | 103 ++++++++++------- apps/browser/store/locales/th/copy.resx | 107 +++++++++-------- apps/browser/store/locales/tr/copy.resx | 107 +++++++++-------- apps/browser/store/locales/uk/copy.resx | 107 +++++++++-------- apps/browser/store/locales/vi/copy.resx | 107 +++++++++-------- apps/browser/store/locales/zh_CN/copy.resx | 108 ++++++++++-------- apps/browser/store/locales/zh_TW/copy.resx | 108 ++++++++++-------- 120 files changed, 3848 insertions(+), 2760 deletions(-) diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index b7e26f3362..e08894be0b 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - مدير كلمات مرور مجاني", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "مدير كلمات مرور مجاني وآمن لجميع أجهزتك.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index 20834af27f..1e5062d8c6 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Ödənişsiz Parol Meneceri", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Bütün cihazlarınız üçün güvənli və ödənişsiz bir parol meneceri.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index 630ad48ee6..91ff397b3a 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden – бясплатны менеджар пароляў", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Бяспечны і бясплатны менеджар пароляў для ўсіх вашых прылад.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index edfcd8a9b4..33be2608b4 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -3,11 +3,11 @@ "message": "Битуорден (Bitwarden)" }, "extName": { - "message": "Bitwarden", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Безопасно и безплатно управление за всичките ви устройства.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -3001,7 +3001,7 @@ "description": "Notification message for when saving credentials has failed." }, "success": { - "message": "Success" + "message": "Успех" }, "removePasskey": { "message": "Премахване на секретния ключ" diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index 2ab44bf7e1..a12308648a 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "আপনার সমস্ত ডিভাইসের জন্য একটি সুরক্ষিত এবং বিনামূল্যের পাসওয়ার্ড ব্যবস্থাপক।", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index 8d2a25db9c..7f406fabee 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Free Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "A secure and free password manager for all of your devices.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index 30d4c4e636..7c8bd63aea 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Administrador de contrasenyes segur i gratuït per a tots els vostres dispositius.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index 42a4f9edb1..bd3c6882df 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden – Bezplatný správce hesel", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Bezpečný a bezplatný správce hesel pro všechna Vaše zařízení.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index 52d3cf7d56..c718c1d876 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Rheolydd cyfineiriau am ddim", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Rheolydd cyfrineiriau diogel a rhad ac am ddim ar gyfer eich holl ddyfeisiau.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index 08aebe98e1..777c3b484f 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Gratis adgangskodemanager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "En sikker og gratis adgangskodemanager til alle dine enheder.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index 4edca5557c..8f2a59af1e 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Kostenloser Passwortmanager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Ein sicherer und kostenloser Passwortmanager für all deine Geräte.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index 75cd5ef2fa..8c65e61e53 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Δωρεάν Διαχειριστής Κωδικών", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Ένας ασφαλής και δωρεάν διαχειριστής κωδικών, για όλες σας τις συσκευές.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index b4b7b314db..e4d90adf1a 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Free Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "A secure and free password manager for all of your devices.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index 6dd78dc292..7cc17240d2 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Free Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "A secure and free password manager for all of your devices.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index 20b9a91814..3e488bce4c 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Bitwarden es un gestor de contraseñas seguro y gratuito para todos tus dispositivos.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index c832528847..785a3e4986 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Tasuta paroolihaldur", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Turvaline ja tasuta paroolihaldur kõikidele sinu seadmetele.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index 9504f06c65..9a07b9d9ae 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Pasahitz kudeatzailea", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Bitwarden, zure gailu guztietarako pasahitzen kudeatzaile seguru eta doakoa da.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index c96c5c35cf..c68dc43ef4 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - مدیریت کلمه عبور رایگان", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "یک مدیریت کننده کلمه عبور رایگان برای تمامی دستگاه‌هایتان.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index b3602afd82..2cdb6a2379 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden – Ilmainen salasanahallinta", + "message": "Bitwarden – Salasanahallinta", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Turvallinen ja ilmainen salasanahallinta kaikille laitteillesi.", + "message": "Kotona, töissä tai reissussa, Bitwarden suojaa helposti kaikki salasanasi, avainkoodisi ja arkaluonteiset tietosi.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -3001,7 +3001,7 @@ "description": "Notification message for when saving credentials has failed." }, "success": { - "message": "Success" + "message": "Onnistui" }, "removePasskey": { "message": "Poista suojausavain" @@ -3010,10 +3010,10 @@ "message": "Suojausavain poistettiin" }, "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + "message": "Huomioi: Määrittämättömät organisaatiokohteet eivät enää näy \"Kaikki holvit\" -näkymässä, vaan ne ovat käytettävissä vain Hallintapaneelista." }, "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." + "message": "Huomioi: Alkaen 16. toukokuuta 2024, määrittämättömät organisaatiokohteet eivät enää näy \"Kaikki holvit\" -näkymässä, vaan ne ovat käytettävissä vain Hallintapaneelista." }, "unassignedItemsBannerCTAPartOne": { "message": "Määritä nämä kohteet kokoelmaan", @@ -3027,9 +3027,9 @@ "message": "Hallintapaneelista" }, "errorAssigningTargetCollection": { - "message": "Error assigning target collection." + "message": "Virhe määritettäessä kohdekokoelmaa." }, "errorAssigningTargetFolder": { - "message": "Error assigning target folder." + "message": "Virhe määritettäessä kohdekansiota." } } diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index 28418d984d..0dfb4a39c9 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Libreng Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Isang ligtas at libreng password manager para sa lahat ng iyong mga aparato.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index 206f07800d..742e31ee58 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Gestion des mots de passe", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Un gestionnaire de mots de passe sécurisé et gratuit pour tous vos appareils.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index 95b880d1a5..b4c151eeb0 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Free Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "A secure and free password manager for all of your devices.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index efb82e64ec..61482da54a 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - מנהל סיסמאות חינמי", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "מנהל סיסמאות חינמי ומאובטח עבור כל המכשירים שלך.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index 9c13fa6efa..b76405eed8 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -3,11 +3,11 @@ "message": "bitwarden" }, "extName": { - "message": "Bitwarden", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "bitwarden is a secure and free password manager for all of your devices.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index ee4c4e7859..2dc500bc1e 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - besplatni upravitelj lozinki", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Bitwarden je siguran i besplatan upravitelj lozinki za sve tvoje uređaje.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index 7d6a8a208b..e47f2cda1f 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Ingyenes jelszókezelő", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Egy biztonságos és ingyenes jelszókezelő az összes eszközre.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -3001,7 +3001,7 @@ "description": "Notification message for when saving credentials has failed." }, "success": { - "message": "Success" + "message": "Sikeres" }, "removePasskey": { "message": "Jelszó eltávolítása" diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index 92b60324ad..d4399d8e15 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Pengelola Sandi Gratis", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Bitwarden adalah sebuah pengelola sandi yang aman dan gratis untuk semua perangkat Anda.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index 6887b134df..93ae682190 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Password Manager Gratis", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Un password manager sicuro e gratis per tutti i tuoi dispositivi.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -3001,7 +3001,7 @@ "description": "Notification message for when saving credentials has failed." }, "success": { - "message": "Success" + "message": "Successo" }, "removePasskey": { "message": "Rimuovi passkey" diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index 744c76a509..52ff21727a 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - 無料パスワードマネージャー", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Bitwarden はあらゆる端末で使える、安全な無料パスワードマネージャーです。", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index ab7b84d22e..2c18502eca 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Free Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "A secure and free password manager for all of your devices.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index 67e1f24787..5d1b024c60 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Free Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "A secure and free password manager for all of your devices.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index 9b363cba1f..047270808e 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -3,11 +3,11 @@ "message": "ಬಿಟ್ವಾರ್ಡೆನ್" }, "extName": { - "message": "ಬಿಟ್‌ವಾರ್ಡೆನ್ - ಉಚಿತ ಪಾಸ್‌ವರ್ಡ್ ನಿರ್ವಾಹಕ", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "ನಿಮ್ಮ ಎಲ್ಲಾ ಸಾಧನಗಳಿಗೆ ಸುರಕ್ಷಿತ ಮತ್ತು ಉಚಿತ ಪಾಸ್‌ವರ್ಡ್ ನಿರ್ವಾಹಕ.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index 3e4f5769c0..4bc4302f8b 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - 무료 비밀번호 관리자", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "당신의 모든 기기에서 사용할 수 있는, 안전한 무료 비밀번호 관리자입니다.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index c690c2727b..b1a2c857e0 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Saugi ir nemokama slaptažodžių tvarkyklė visiems įrenginiams.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index d26b10a30c..4055693486 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Drošs bezmaksas paroļu pārvaldnieks visām Tavām ierīcēm.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -3001,7 +3001,7 @@ "description": "Notification message for when saving credentials has failed." }, "success": { - "message": "Success" + "message": "Izdevās" }, "removePasskey": { "message": "Noņemt piekļuves atslēgu" diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index 1db5f6458b..d9703137fe 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - സൗജന്യ പാസ്സ്‌വേഡ് മാനേജർ ", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "നിങ്ങളുടെ എല്ലാ ഉപകരണങ്ങൾക്കും സുരക്ഷിതവും സൗജന്യവുമായ പാസ്‌വേഡ് മാനേജർ.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index 06cf84efff..f67f617d3b 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - विनामूल्य पासवर्ड व्यवस्थापक", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "तुमच्या सर्व उपकरणांसाठी एक सुरक्षित व विनामूल्य पासवर्ड व्यवस्थापक.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index 67e1f24787..5d1b024c60 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Free Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "A secure and free password manager for all of your devices.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index 220fe95e23..649163a8dc 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden — Fri passordbehandling", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Bitwarden er en sikker og fri passordbehandler for alle dine PCer og mobiler.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index 67e1f24787..5d1b024c60 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Free Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "A secure and free password manager for all of your devices.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index 808e599e70..5a52b4f7ef 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Gratis wachtwoordbeheer", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Een veilige en gratis oplossing voor wachtwoordbeheer voor al je apparaten.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index 67e1f24787..5d1b024c60 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Free Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "A secure and free password manager for all of your devices.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index 67e1f24787..5d1b024c60 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Free Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "A secure and free password manager for all of your devices.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index 1ef79bac42..e768a70d52 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - darmowy menedżer haseł", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Bezpieczny i darmowy menedżer haseł dla wszystkich Twoich urządzeń.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index 9322e680d2..c6e62fbd4f 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Um gerenciador de senhas seguro e gratuito para todos os seus dispositivos.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index d4fdc1be81..06ba8eed26 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Gestor de Palavras-passe", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Um gestor de palavras-passe seguro e gratuito para todos os seus dispositivos.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index 5e8c82e70b..b3e0a2066f 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Manager de parole gratuit", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Un manager de parole sigur și gratuit pentru toate dispozitivele dvs.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index 046fe2b931..e594dbdce2 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - бесплатный менеджер паролей", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Защищенный и бесплатный менеджер паролей для всех ваших устройств.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index e576a50dd7..05e2dc3edd 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -3,11 +3,11 @@ "message": "බිට්වාඩන්" }, "extName": { - "message": "බිට්වාඩන් - නොමිලේ මුරපදය කළමනාකරු", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "ඔබගේ සියලු උපාංග සඳහා ආරක්ෂිත සහ නොමිලේ මුරපද කළමණාකරුවෙකු.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index e34fa525be..eab1d105eb 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Bezplatný správca hesiel", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "bitwarden je bezpečný a bezplatný správca hesiel pre všetky vaše zariadenia.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -1754,7 +1754,7 @@ } }, "send": { - "message": "Odoslať", + "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "searchSends": { diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index c7f8ed0481..935678efc8 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Brezplačni upravitelj gesel", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Varen in brezplačen upravitelj gesel za vse vaše naprave.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index 8d1ee8264f..5819546800 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - бесплатни менаџер лозинки", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Сигурни и бесплатни менаџер лозинки за све ваше уређаје.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index ebe5e6d281..d96e86b8d3 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Gratis lösenordshanterare", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Bitwarden är en säker och gratis lösenordshanterare för alla dina enheter.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index 67e1f24787..5d1b024c60 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Free Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "A secure and free password manager for all of your devices.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index 718440438e..794d0e6c22 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -3,11 +3,11 @@ "message": "bitwarden" }, "extName": { - "message": "bitwarden - Free Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "bitwarden is a secure and free password manager for all of your devices.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index 7633b47258..8408253b86 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Ücretsiz Parola Yöneticisi", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Tüm cihazlarınız için güvenli ve ücretsiz bir parola yöneticisi.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index b09d966142..b590b92041 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Bitwarden - це захищений і безкоштовний менеджер паролів для всіх ваших пристроїв.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index af518878b9..ab1d0d515b 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Quản lý mật khẩu miễn phí", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Trình quản lý mật khẩu an toàn và miễn phí cho mọi thiết bị của bạn.", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index 5842d4f4c1..a2f856c31b 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - 免费密码管理器", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "安全且免费的跨平台密码管理器。", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -3001,7 +3001,7 @@ "description": "Notification message for when saving credentials has failed." }, "success": { - "message": "Success" + "message": "成功" }, "removePasskey": { "message": "移除通行密钥" @@ -3024,12 +3024,12 @@ "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" + "message": "管理控制台" }, "errorAssigningTargetCollection": { - "message": "Error assigning target collection." + "message": "分配目标集合时出错。" }, "errorAssigningTargetFolder": { - "message": "Error assigning target folder." + "message": "分配目标文件夹时出错。" } } diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index a8163dab98..1ecfdfc50e 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - 免費密碼管理工具", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Bitwarden 是一款安全、免費、跨平台的密碼管理工具。", + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/store/locales/ar/copy.resx b/apps/browser/store/locales/ar/copy.resx index e74606ff15..e1bfa48b44 100644 --- a/apps/browser/store/locales/ar/copy.resx +++ b/apps/browser/store/locales/ar/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,42 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden - مدير كلمات مرور مجاني</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>مدير كلمات مرور مجاني وآمن لجميع أجهزتك</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>شركة Bitwarden, Inc هي الشركة الأم لشركة 8bit Solutions LLC. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -تم تصنيف Bitwarden كأفصل مدير كلمات مرور بواسطة كل من The Verge، U.S News &amp; World Report، CNET، وغيرهم. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -قم بادراة وحفظ وتأمين كلمات المرور الخاصة بك، ومشاركتها بين اجهزتك من اي مكان. -يوفر Bitwarden حل مفتوح المصدر لادارة كلمات المرور للجميع، سواء في المنزل، في العمل او في اي مكان. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -قم بانشاء كلمات مرور قوية وفريدة وعشوائية حسب متطلبات الأمان للصفحات التي تزورها. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -يوفر Bitwarden Send امكانية ارسال البيانات --- النصوص والملفات --- بطريقة مشفرة وسريعة لأي شخص. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -يوفر Bitwarden خطط خاصة للفرق والشركات والمؤسسات لتمكنك من مشاركة كلمات المرور مع زملائك في العمل. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -لماذا قد تختار Bitwarden: -تشفير على مستوى عالمي -كلمات المرور محمية بتشفير متقدم تام (end-to-end encryption) من نوعية AES-256 bit، مع salted hashing، و PBKDF2 SHA-256. كل هذا لابقاء بياناتك محمية وخاصة. +More reasons to choose Bitwarden: -مولد كلمات المرور المدمج -قم بانشاء كلمات مرور قوية وفريدة وعشوائية حسب متطلبات الأمان للصفحات التي تزورها. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -الترجمات العالمية -يتوفر Bitwarden باكثر من 40 لغة، وتتنامى الترجمات بفضل مجتمعنا العالمي. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -تطبيقات متعددة المنصات -قم بحماية ومشاركة بياناتاك الحساسة عبر خزنة Bitwarden من اي متصفح ويب، او هاتف ذكي، او جهاز كمبيوتر، وغيرها. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>مدير كلمات مرور مجاني وآمن لجميع أجهزتك</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>مزامنة خزنتك والوصول إليها من عدة أجهزة</value> diff --git a/apps/browser/store/locales/az/copy.resx b/apps/browser/store/locales/az/copy.resx index cb05f8e5d9..2a3d507df2 100644 --- a/apps/browser/store/locales/az/copy.resx +++ b/apps/browser/store/locales/az/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,41 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden – Ödənişsiz Parol Meneceri</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>Bütün cihazlarınız üçün güvənli və ödənişsiz bir parol meneceri</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>Bitwarden, Inc., 8bit Solutions LLC-nin ana şirkətidir. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -THE VERGE, U.S. NEWS &amp; WORLD REPORT, CNET VƏ BİR ÇOXUNA GÖRƏ ƏN YAXŞI PAROL MENECERİDİR. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Hər yerdən limitsiz cihazda limitsiz parolu idarə edin, saxlayın, qoruyun və paylaşın. Bitwarden evdə, işdə və ya yolda hər kəsə açıq mənbəli parol idarəetmə həllərini təqdim edir. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Çox istifadə etdiyiniz hər veb sayt üçün təhlükəsizlik tələblərinə görə güclü, unikal və təsadüfi parollar yaradın. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send şifrələnmiş məlumatların (fayl və sadə mətnləri) birbaşa və sürətli göndərilməsini təmin edir. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden, parolları iş yoldaşlarınızla təhlükəsiz paylaşa bilməyiniz üçün şirkətlərə Teams və Enterprise planları təklif edir. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Nəyə görə Bitwarden-i seçməliyik: -Yüksək səviyyə şifrələmə -Parollarınız qabaqcıl ucdan-uca şifrələmə (AES-256 bit, salted hashtag və PBKDF2 SHA-256) ilə qorunur, beləcə datanızın güvənli və gizli qalmasını təmin edir. +More reasons to choose Bitwarden: -Daxili parol yaradıcı -Çox istifadə etdiyiniz hər veb sayt üçün təhlükəsizlik tələblərinə görə güclü, unikal və təsadüfi şifrələr yaradın. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Qlobal tərcümələr -Bitwarden tərcümələri 40 dildə mövcuddur və qlobal cəmiyyətimiz sayəsində böyüməyə davam edir. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Çarpaz platform tətbiqləri -Bitwarden anbarındakı həssas verilənləri, istənilən brauzerdən, mobil cihazdan və ya masaüstü əməliyyat sistemindən və daha çoxundan qoruyub paylaşın. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>Bütün cihazlarınız üçün güvənli və ödənişsiz bir parol meneceri</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Anbarınıza bir neçə cihazdan eyniləşdirərək müraciət edin</value> diff --git a/apps/browser/store/locales/be/copy.resx b/apps/browser/store/locales/be/copy.resx index f84dd699a7..65c337826b 100644 --- a/apps/browser/store/locales/be/copy.resx +++ b/apps/browser/store/locales/be/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,24 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden – бясплатны менеджар пароляў</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>Бяспечны і бясплатны менеджар пароляў для ўсіх вашых прылад</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>Bitwarden - просты і бяспечны спосаб захоўваць усе вашы імёны карыстальніка і паролі, а таксама лёгка іх сінхранізаваць паміж усімі вашымі прыладамі. Пашырэнне праграмы Bitwarden дазваляе хутка ўвайсці на любы вэб-сайт з дапамогай Safari або Chrome і падтрымліваецца сотнямі іншых папулярных праграм. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -Крадзеж пароляў — сур'ёзная праблема. Сайты і праграмы, якія вы выкарыстоўваеце падвяргаюцца нападам кожны дзень. Праблемы ў іх бяспецы могуць прывесці да крадзяжу вашага пароля. Акрамя таго, калі вы выкарыстоўваеце адны і тыя ж паролі на розных сайтах і праграмах, то хакеры могуць лёгка атрымаць доступ да некалькіх вашых уліковых запісаў адразу (да паштовай скрыні, да банкаўскага рахунку ды г. д.). +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Эксперты па бяспецы рэкамендуюць выкарыстоўваць розныя выпадкова знегерыраваныя паролі для кожнага створанага вамі ўліковага запісу. Але як жа кіраваць усімі гэтымі паролямі? Bitwarden дазваляе вам лёгка атрымаць доступ да вашых пароляў, а гэтак жа ствараць і захоўваць іх. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Bitwarden захоўвае ўсе вашы імёны карыстальніка і паролі ў зашыфраваным сховішчы, якое сінхранізуецца паміж усімі вашымі прыладамі. Да таго, як даныя пакінуць вашу прыладу, яны будуць зашыфраваны і толькі потым адпраўлены. Мы ў Bitwarden не зможам прачытаць вашы даныя, нават калі мы гэтага захочам. Вашы даныя зашыфраваны пры дапамозе алгарытму AES-256 і PBKDF2 SHA-256. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden — гэта праграмнае забеспячэнне з адкрытым на 100% зыходным кодам. Зыходны код Bitwarden размешчаны на GitHub, і кожны можа свабодна праглядаць, правяраць і рабіць унёсак у код Bitwarden.</value> +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. + +Use Bitwarden to secure your workforce and share sensitive information with colleagues. + + +More reasons to choose Bitwarden: + +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. + +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. + +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! +</value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>Бяспечны і бясплатны менеджар пароляў для ўсіх вашых прылад</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Сінхранізацыя і доступ да сховішча з некалькіх прылад</value> diff --git a/apps/browser/store/locales/bg/copy.resx b/apps/browser/store/locales/bg/copy.resx index 29c468f045..bc08f6a107 100644 --- a/apps/browser/store/locales/bg/copy.resx +++ b/apps/browser/store/locales/bg/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,41 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden — безплатен управител на пароли</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>Сигурен и свободен управител на пароли за всички устройства</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>„Bitwarden, Inc.“ е компанията-майка на „8bit Solutions LLC“. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -ОПРЕДЕЛЕН КАТО НАЙ-ДОБРИЯТ УПРАВИТЕЛ НА ПАРОЛИ ОТ „THE VERGE“, „U.S. NEWS &amp; WORLD REPORT“, „CNET“ И ОЩЕ. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Управлявайте, съхранявайте, защитавайте и споделяйте неограничен брой пароли на неограничен брой устройства от всяка точка на света. Битуорден предоставя решение за управление на паролите с отворен код, от което може да се възползва всеки, било то у дома, на работа или на път. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Създавайте сигурни, уникални и случайни пароли според изискванията за сигурност на всеки уеб сайт, който посещавате. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -С Изпращанията на Битуорден можете незабавно да предавате шифрована информация под формата на файлове и обикновен текст – директно и с всекиго. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Битуорден предлага планове за екипи и големи фирми, така че служителите в компаниите да могат безопасно да споделят пароли помежду си. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Защо да изберете Битуорден: -Шифроване от най-висока класа -Паролите са защитени със сложен шифър „от край до край“ (AES-256 bit, salted hashtag и PBKDF2 SHA-256), така че данните Ви остават да са защитени и неприкосновени. +More reasons to choose Bitwarden: -Вграден генератор на пароли -Създавайте сигурни, уникални и случайни пароли според изискванията за сигурност на всеки уеб сайт, който посещавате. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Глобални преводи -Битуорден е преведен на 40 езика и този брой не спира да расте, благодарение на нашата глобална общност. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Приложения за всяка система -Защитете и споделяйте поверителните данни от своя трезор в Битуорден от всеки браузър, мобилно устройство или компютър. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>Сигурен и свободен управител на пароли за всички устройства</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Удобен достъп до трезора, който се синхронизира от всички устройства</value> diff --git a/apps/browser/store/locales/bn/copy.resx b/apps/browser/store/locales/bn/copy.resx index a8eb4f7c75..1bcfb19001 100644 --- a/apps/browser/store/locales/bn/copy.resx +++ b/apps/browser/store/locales/bn/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,41 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden – বিনামূল্যের পাসওয়ার্ড ব্যবস্থাপক</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>আপনার সমস্ত ডিভাইসের জন্য একটি সুরক্ষিত এবং বিনামূল্যের পাসওয়ার্ড ব্যবস্থাপক</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS &amp; WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -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. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose 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. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>আপনার সমস্ত ডিভাইসের জন্য একটি সুরক্ষিত এবং বিনামূল্যের পাসওয়ার্ড ব্যবস্থাপক</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>একাধিক ডিভাইস থেকে আপনার ভল্ট সিঙ্ক এবং ব্যাবহার করুন</value> diff --git a/apps/browser/store/locales/bs/copy.resx b/apps/browser/store/locales/bs/copy.resx index 191198691d..82e4eb1d88 100644 --- a/apps/browser/store/locales/bs/copy.resx +++ b/apps/browser/store/locales/bs/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,41 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden – Free Password Manager</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>A secure and free password manager for all of your devices</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS &amp; WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -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. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose 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. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>A secure and free password manager for all of your devices</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Sync and access your vault from multiple devices</value> diff --git a/apps/browser/store/locales/ca/copy.resx b/apps/browser/store/locales/ca/copy.resx index 0bd454addb..27e685841b 100644 --- a/apps/browser/store/locales/ca/copy.resx +++ b/apps/browser/store/locales/ca/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,41 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden - Administrador de contrasenyes gratuït</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>Administrador de contrasenyes segur i gratuït per a tots els vostres dispositius</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>Bitwarden, Inc. és la companyia matriu de solucions de 8 bits LLC. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -Nomenada Millor gestor de contrasenyes per THE VERGE, U.S. NEWS &amp; WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Gestioneu, emmagatzemeu, segures i compartiu contrasenyes il·limitades a través de dispositius il·limitats des de qualsevol lloc. Bitwarden lliura solucions de gestió de contrasenyes de codi obert a tothom, ja siga a casa, a la feina o sobre la marxa. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Genereu contrasenyes fortes, úniques i aleatòries basades en els requisits de seguretat per a cada lloc web que freqüenteu. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send transmet ràpidament informació xifrada --- Fitxers i text complet - directament a qualsevol persona. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden ofereix equips i plans empresarials per a empreses perquè pugueu compartir amb seguretat contrasenyes amb els companys. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Per què triar Bitwarden: -Xifratge de classe mundial -Les contrasenyes estan protegides amb un xifratge avançat fi-a-fi (AES-256 bit, salted hashtag, and PBKDF2 SHA-256), de manera que les vostres dades es mantenen segures i privades. +More reasons to choose Bitwarden: -Generador de contrasenyes integrat -Genereu contrasenyes fortes, úniques i aleatòries basades en els requisits de seguretat per a cada lloc web que freqüenteu. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Traduccions globals -Les traduccions de Bitwarden existeixen en 40 idiomes i creixen, gràcies a la nostra comunitat global. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Aplicacions de plataforma creuada -Assegureu-vos i compartiu dades sensibles a la vostra caixa forta de Bitwarden des de qualsevol navegador, dispositiu mòbil o S.O. d'escriptori, i molt més. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>Administrador de contrasenyes segur i gratuït per a tots els vostres dispositius</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Sincronitzeu i accediu a la vostra caixa forta des de diversos dispositius</value> diff --git a/apps/browser/store/locales/cs/copy.resx b/apps/browser/store/locales/cs/copy.resx index 6b711e2863..59d8c60b40 100644 --- a/apps/browser/store/locales/cs/copy.resx +++ b/apps/browser/store/locales/cs/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,41 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden – Bezplatný správce hesel</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>Bezpečný a bezplatný správce hesel pro všechna Vaše zařízení</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>Bitwarden, Inc. je mateřskou společností 8bit Solutions LLC. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -THE VERGE, U.S. NEWS &amp; WORLD REPORT, CNET A DALŠÍ JI OZNAČILY ZA NEJLEPŠÍHO SPRÁVCE HESEL. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Spravujte, ukládejte, zabezpečujte a sdílejte neomezený počet hesel na neomezeném počtu zařízení odkudkoliv. Bitwarden poskytuje open source řešení pro správu hesel všem, ať už doma, v práci nebo na cestách. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generujte silná, jedinečná a náhodná hesla na základě bezpečnostních požadavků pro každou webovou stránku, kterou navštěvujete. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send rychle přenáší šifrované informace --- soubory a prostý text -- přímo a komukoli. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden nabízí plány Teams a Enterprise pro firmy, takže můžete bezpečně sdílet hesla s kolegy. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Proč si vybrat Bitwarden: -Šifrování na světové úrovni -Hesla jsou chráněna pokročilým koncovým šifrováním (AES-256 bit, salted hashování a PBKDF2 SHA-256), takže Vaše data zůstanou bezpečná a soukromá. +More reasons to choose Bitwarden: -Vestavěný generátor hesel -Generujte silná, jedinečná a náhodná hesla na základě bezpečnostních požadavků pro každou webovou stránku, kterou navštěvujete. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Globální překlady -Překlady Bitwarden existují ve 40 jazycích a díky naší globální komunitě se stále rozšiřují. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Aplikace pro více platforem -Zabezpečte a sdílejte citlivá data v rámci svého trezoru Bitwarden z jakéhokoli prohlížeče, mobilního zařízení nebo operačního systému pro počítač. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>Bezpečný a bezplatný správce hesel pro všechna Vaše zařízení</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Synchronizujte a přistupujte ke svému trezoru z různých zařízení</value> diff --git a/apps/browser/store/locales/cy/copy.resx b/apps/browser/store/locales/cy/copy.resx index 8222329630..983a112c07 100644 --- a/apps/browser/store/locales/cy/copy.resx +++ b/apps/browser/store/locales/cy/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,41 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden - Rheolydd cyfineiriau am ddim</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>Rheolydd diogel a rhad ac am ddim ar gyfer eich holl ddyfeisiau</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>Bitwarden, Inc. yw rhiant-gwmni 8bit Solutions LLC. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -Y RHEOLYDD CYFRINEIRIAU GORAU YN ÔL THE VERGE, US NEWS &amp; WORLD REPORT, CNET, A MWY. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Rheolwch, storiwch, diogelwch a rhannwch gyfrineiriau di-ri ar draws dyfeiriau di-ri o unrhyw le. Mae Bitwarden yn cynnig rhaglenni rheoli cyfrineiriau cod-agored i bawb, boed gartref, yn y gwaith, neu ar fynd. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Gallwch gynhyrchu cyfrineiriau cryf, unigryw ac ar hap yn seiliedig ar ofynion diogelwch ar gyfer pob gwefan rydych chi'n ei defnyddio. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Mae Bitwarden Send yn trosglwyddo gwybodaeth wedi'i hamgryptio yn gyflym -- ffeiliau a thestun plaen -- yn uniongyrchol i unrhyw un. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Mae Bitwarden yn cynnig cynlluniau Teams ac Enterprise i gwmnïau er mwyn i chi allu rhannu cyfrineiriau gyda chydweithwyr yn ddiogel. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Pam dewis Bitwarden: -Amgryptio o'r radd flaenaf -Mae cyfrineiriau wedi'u hamddiffyn ag amgryptio datblygedig o un pen i'r llall (AES-256 bit, hashio â halen, a PBKDF2 SHA-256) er mwyn i'ch data aros yn ddiogel ac yn breifat. +More reasons to choose Bitwarden: -Cynhyrchydd cyfrineiriau -Gallwch gynhyrchu cyfrineiriau cryf, unigryw ac ar hap yn seiliedig ar ofynion diogelwch ar gyfer pob gwefan rydych chi'n ei defnyddio. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Ar gael yn eich iaith chi -Mae Bitwarden wedi'i gyfieithu i dros 40 o ieithoedd, diolch i'n cymuned fyd-eang. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Rhaglenni traws-blatfform -Diogelwch a rhannwch ddata sensitif yn eich cell Bitwarden o unrhyw borwr, dyfais symudol, neu system weithredu, a mwy. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>Rheolydd diogel a rhad ac am ddim ar gyfer eich holl ddyfeisiau</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Gallwch gael mynediad at, a chysoni, eich cell o sawl dyfais</value> diff --git a/apps/browser/store/locales/da/copy.resx b/apps/browser/store/locales/da/copy.resx index 858c56dea9..775a3edd81 100644 --- a/apps/browser/store/locales/da/copy.resx +++ b/apps/browser/store/locales/da/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,41 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden - Gratis adgangskodemanager</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>En sikker og gratis adgangskodemanager til alle dine enheder</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>Bitwarden, Inc. er moderselskab for 8bit Solutions LLC. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -UDNÆVNT BEDSTE PASSWORD MANAGER AF THE VERGE, U.S. NEWS &amp; WORLD REPORT, CNET OG FLERE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Administrér, gem, sikr og del adgangskoder ubegrænset på tværs af enheder hvor som helst. Bitwarden leverer open source adgangskodeadministrationsløsninger til alle, hvad enten det er hjemme, på arbejdspladsen eller på farten. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generér stærke, unikke og tilfældige adgangskoder baseret på sikkerhedskrav til hvert websted, du besøger. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send overfører hurtigt krypterede oplysninger --- filer og almindelig tekst - direkte til enhver. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden tilbyder Teams og Enterprise-planer for virksomheder, så du sikkert kan dele adgangskoder med kolleger. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Hvorfor vælge Bitwarden: -Kryptering i verdensklasse -Adgangskoder er beskyttet med avanceret end-to-end-kryptering (AES-256 bit, saltet hashing og PBKDF2 SHA-256), så dine data forbliver sikre og private. +More reasons to choose Bitwarden: -Indbygget adgangskodegenerator -Generér stærke, unikke og tilfældige adgangskoder baseret på sikkerhedskrav til hvert websted, du besøger. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Globale oversættelser -Bitwarden findes på 40 sprog, og flere kommer til, takket være vores globale fællesskab. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Applikationer på tværs af platforme -Beskyt og del følsomme data i din Bitwarden boks fra enhver browser, mobilenhed eller desktop OS og mere. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>En sikker og gratis adgangskodemanager til alle dine enheder</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Synkroniser og få adgang til din boks fra flere enheder</value> diff --git a/apps/browser/store/locales/de/copy.resx b/apps/browser/store/locales/de/copy.resx index 139a6026fd..2267c6c85e 100644 --- a/apps/browser/store/locales/de/copy.resx +++ b/apps/browser/store/locales/de/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,41 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden - Kostenloser Passwort-Manager</value> + <value>Bitwarden Passwort-Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>Ein sicherer und kostenloser Passwort-Manager für all deine Geräte</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>Bitwarden, Inc. ist die Muttergesellschaft von 8bit Solutions LLC. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -AUSGEZEICHNET ALS BESTER PASSWORTMANAGER VON THE VERGE, U.S. NEWS &amp; WORLD REPORT, CNET UND ANDEREN. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Verwalte, speichere, sichere und teile unbegrenzte Passwörter von überall auf unbegrenzten Geräten. Bitwarden liefert Open-Source-Passwort-Management-Lösungen für alle, sei es zu Hause, am Arbeitsplatz oder unterwegs. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generiere starke, einzigartige und zufällige Passwörter basierend auf Sicherheitsanforderungen für jede Website, die du häufig besuchst. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send überträgt schnell verschlüsselte Informationen - Dateien und Klartext - direkt an jeden. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden bietet Teams und Enterprise Pläne für Unternehmen an, damit du Passwörter sicher mit Kollegen teilen kannst. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Warum Bitwarden: -Weltklasse Verschlüsselung -Passwörter sind durch erweiterte Ende-zu-Ende-Verschlüsselung (AES-256 Bit, salted hashing und PBKDF2 SHA-256) so bleiben deine Daten sicher und privat. +More reasons to choose Bitwarden: -Integrierter Passwortgenerator -Generiere starke, einzigartige und zufällige Passwörter basierend auf Sicherheitsanforderungen für jede Website, die du häufig besuchst. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Globale Übersetzungen -Bitwarden Übersetzungen existieren in 40 Sprachen und wachsen dank unserer globalen Community. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Plattformübergreifende Anwendungen -Sichere und teile vertrauliche Daten in deinem Bitwarden Tresor von jedem Browser, jedem mobilen Gerät oder Desktop-Betriebssystem und mehr. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>Ein sicherer und kostenloser Passwort-Manager für all deine Geräte</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Synchronisiere und greife auf deinen Tresor von unterschiedlichen Geräten aus zu</value> diff --git a/apps/browser/store/locales/el/copy.resx b/apps/browser/store/locales/el/copy.resx index 01def6ea5a..fb50f95bdc 100644 --- a/apps/browser/store/locales/el/copy.resx +++ b/apps/browser/store/locales/el/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,41 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden – Δωρεάν Διαχειριστής Κωδικών</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>Ένας ασφαλής και δωρεάν διαχειριστής κωδικών για όλες τις συσκευές σας</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>Η Bitwarden, Inc. είναι η μητρική εταιρεία της 8bit Solutions LLC. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -ΟΝΟΜΑΣΘΗΚΕ ΩΣ Ο ΚΑΛΥΤΕΡΟΣ ΔΙΑΧΕΙΡΙΣΤΗΣ ΚΩΔΙΚΩΝ ΠΡΟΣΒΑΣΗΣ ΑΠΟ ΤΟ VERGE, το U.S. NEWS &amp; WORLD REPORT, το CNET και άλλα. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Διαχειριστείτε, αποθηκεύστε, ασφαλίστε και μοιραστείτε απεριόριστους κωδικούς πρόσβασης σε απεριόριστες συσκευές από οπουδήποτε. Το Bitwarden παρέχει λύσεις διαχείρισης κωδικών πρόσβασης ανοιχτού κώδικα σε όλους, στο σπίτι, στη δουλειά ή εν κινήσει. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Δημιουργήστε ισχυρούς, μοναδικούς και τυχαίους κωδικούς πρόσβασης βάσει των απαιτήσεων ασφαλείας, για κάθε ιστότοπο που συχνάζετε. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Το Bitwarden Send αποστέλλει γρήγορα κρυπτογραφημένες πληροφορίες --- αρχεία και απλό κείμενο -- απευθείας σε οποιονδήποτε. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Το Bitwarden προσφέρει προγράμματα Teams και Enterprise για εταιρείες, ώστε να μπορείτε να μοιράζεστε με ασφάλεια τους κωδικούς πρόσβασης με τους συναδέλφους σας. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Γιατί να επιλέξετε το Bitwarden: -Κρυπτογράφηση παγκόσμιας κλάσης -Οι κωδικοί πρόσβασης προστατεύονται με προηγμένη end-to-end κρυπτογράφηση (AES-256 bit, salted hashing και PBKDF2 SHA-256), ώστε τα δεδομένα σας να παραμένουν ασφαλή και ιδιωτικά. +More reasons to choose Bitwarden: -Ενσωματωμένη Γεννήτρια Κωδικών Πρόσβασης -Δημιουργήστε ισχυρούς, μοναδικούς και τυχαίους κωδικούς πρόσβασης βάσει των απαιτήσεων ασφαλείας, για κάθε ιστότοπο που συχνάζετε. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Παγκόσμιες Μεταφράσεις -Υπάρχουν μεταφράσεις για το Bitwarden σε 40 γλώσσες και αυξάνονται συνεχώς, χάρη στην παγκόσμια κοινότητά μας. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Εφαρμογές για όλες τις πλατφόρμες -Ασφαλίστε και μοιραστείτε ευαίσθητα δεδομένα εντός του Bitwarden Vault από οποιοδήποτε πρόγραμμα περιήγησης, κινητή συσκευή ή λειτουργικό σύστημα υπολογιστών, και πολλά άλλα. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>Ένας ασφαλής και δωρεάν διαχειριστής κωδικών για όλες τις συσκευές σας</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Συγχρονίστε και αποκτήστε πρόσβαση στο θησαυροφυλάκιό σας από πολλαπλές συσκευές</value> diff --git a/apps/browser/store/locales/en_GB/copy.resx b/apps/browser/store/locales/en_GB/copy.resx index 191198691d..82e4eb1d88 100644 --- a/apps/browser/store/locales/en_GB/copy.resx +++ b/apps/browser/store/locales/en_GB/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,41 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden – Free Password Manager</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>A secure and free password manager for all of your devices</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS &amp; WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -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. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose 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. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>A secure and free password manager for all of your devices</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Sync and access your vault from multiple devices</value> diff --git a/apps/browser/store/locales/en_IN/copy.resx b/apps/browser/store/locales/en_IN/copy.resx index 191198691d..82e4eb1d88 100644 --- a/apps/browser/store/locales/en_IN/copy.resx +++ b/apps/browser/store/locales/en_IN/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,41 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden – Free Password Manager</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>A secure and free password manager for all of your devices</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS &amp; WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -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. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose 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. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>A secure and free password manager for all of your devices</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Sync and access your vault from multiple devices</value> diff --git a/apps/browser/store/locales/es/copy.resx b/apps/browser/store/locales/es/copy.resx index dc7484777a..472697d825 100644 --- a/apps/browser/store/locales/es/copy.resx +++ b/apps/browser/store/locales/es/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,41 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden – Gestor de contraseñas gratuito</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>Un gestor de contraseñas seguro y gratuito para todos tus dispositivos</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>Bitwarden, Inc. es la empresa matriz de 8bit Solutions LLC. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NOMBRADO MEJOR ADMINISTRADOR DE CONTRASEÑAS POR THE VERGE, U.S. NEWS &amp; WORLD REPORT, CNET Y MÁS. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Administre, almacene, proteja y comparta contraseñas ilimitadas en dispositivos ilimitados desde cualquier lugar. Bitwarden ofrece soluciones de gestión de contraseñas de código abierto para todos, ya sea en casa, en el trabajo o en mientras estás de viaje. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Genere contraseñas seguras, únicas y aleatorias en función de los requisitos de seguridad de cada sitio web que frecuenta. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send transmite rápidamente información cifrada --- archivos y texto sin formato, directamente a cualquier persona. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden ofrece planes Teams y Enterprise para empresas para que pueda compartir contraseñas de forma segura con colegas. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -¿Por qué elegir Bitwarden? -Cifrado de clase mundial -Las contraseñas están protegidas con cifrado avanzado de extremo a extremo (AES-256 bits, salted hashing y PBKDF2 SHA-256) para que sus datos permanezcan seguros y privados. +More reasons to choose Bitwarden: -Generador de contraseñas incorporado -Genere contraseñas fuertes, únicas y aleatorias en función de los requisitos de seguridad de cada sitio web que frecuenta. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Traducciones Globales -Las traducciones de Bitwarden existen en 40 idiomas y están creciendo, gracias a nuestra comunidad global. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Aplicaciones multiplataforma -Proteja y comparta datos confidenciales dentro de su Caja Fuerte de Bitwarden desde cualquier navegador, dispositivo móvil o sistema operativo de escritorio, y más. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>Un gestor de contraseñas seguro y gratuito para todos tus dispositivos</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Sincroniza y accede a tu caja fuerte desde múltiples dispositivos</value> diff --git a/apps/browser/store/locales/et/copy.resx b/apps/browser/store/locales/et/copy.resx index 2014ec88a8..eccbeba1ed 100644 --- a/apps/browser/store/locales/et/copy.resx +++ b/apps/browser/store/locales/et/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,41 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden - Tasuta paroolihaldur</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>Tasuta ja turvaline paroolihaldur kõikidele sinu seadmetele</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS &amp; WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -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. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose 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. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>Tasuta ja turvaline paroolihaldur kõikidele Sinu seadmetele</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Sünkroniseeri ja halda oma kontot erinevates seadmetes</value> diff --git a/apps/browser/store/locales/eu/copy.resx b/apps/browser/store/locales/eu/copy.resx index e5b3d542e3..e4271e8ae3 100644 --- a/apps/browser/store/locales/eu/copy.resx +++ b/apps/browser/store/locales/eu/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,41 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden — Doaneko pasahitz kudeatzailea</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>Bitwarden, zure gailu guztietarako pasahitzen kudeatzaile seguru eta doakoa</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>Bitwarden, Inc. 8bit Solutions LLC-ren enpresa matrizea da. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -THE VERGE, U.S. NEWS &amp; WORLD REPORT ETA CNET ENPRESEK PASAHITZ-ADMINISTRATZAILE ONENA izendatu dute, besteak beste. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Gailu guztien artean pasahitz mugagabeak kudeatu, biltegiratu, babestu eta partekatzen ditu. Bitwardenek kode irekiko pasahitzak administratzeko irtenbideak eskaintzen ditu, bai etxean, bai lanean, bai bidaiatzen ari zaren bitartean. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Pasahitz sendoak, bakarrak eta ausazkoak sortzen ditu, webgune bakoitzaren segurtasun-baldintzetan oinarrituta. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send-ek azkar transmititzen du zifratutako informazioa --- artxiboak eta testu sinplea -- edozein pertsonari zuzenean. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden-ek Taldeak eta Enpresak planak eskaintzen ditu, enpresa bereko lankideek pasahitzak modu seguruan parteka ditzaten. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Zergatik aukeratu Bitwarden: -Mundu-mailako zifratzea -Pasahitzak muturretik muturrerako zifratze aurreratuarekin babestuta daude (AES-256 bit, salted hashtag eta PBKDF2 SHA-256), zure informazioa seguru eta pribatu egon dadin. +More reasons to choose Bitwarden: -Pasahitzen sortzailea -Pasahitz sendoak, bakarrak eta ausazkoak sortzen ditu, web gune bakoitzaren segurtasun-baldintzetan oinarrituta. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Itzulpenak -Bitwarden 40 hizkuntzatan dago, eta gero eta gehiago dira, gure komunitate globalari esker. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Plataforma anitzeko aplikazioak -Babestu eta partekatu zure Bitwarden kutxa gotorraren informazio konfidentziala edozein nabigatzailetatik, gailu mugikorretatik, mahaigaineko aplikaziotik eta gehiagotatik. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>Zure gailu guztietarako pasahitzen kudeatzaile seguru eta doakoa</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Sinkronizatu eta sartu zure kutxa gotorrean hainbat gailutatik</value> diff --git a/apps/browser/store/locales/fa/copy.resx b/apps/browser/store/locales/fa/copy.resx index 23cb3f3bf0..67095435ac 100644 --- a/apps/browser/store/locales/fa/copy.resx +++ b/apps/browser/store/locales/fa/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,40 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden - مدیریت کلمه عبور رایگان</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>یک مدیریت کننده کلمه عبور رایگان برای تمامی دستگاه‌هایتان</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>Bitwarden، Inc. شرکت مادر 8bit Solutions LLC است. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -به عنوان بهترین مدیر کلمه عبور توسط VERGE، US News &amp; WORLD REPORT، CNET و دیگران انتخاب شد. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -کلمه‌های عبور با تعداد نامحدود را در دستگاه‌های نامحدود از هر کجا مدیریت کنید، ذخیره کنید، ایمن کنید و به اشتراک بگذارید. Bitwarden راه حل های مدیریت رمز عبور منبع باز را به همه ارائه می دهد، چه در خانه، چه در محل کار یا در حال حرکت. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -بر اساس الزامات امنیتی برای هر وب سایتی که بازدید می کنید، کلمه‌های عبور قوی، منحصر به فرد و تصادفی ایجاد کنید. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send به سرعت اطلاعات رمزگذاری شده --- فایل ها و متن ساده - را مستقیماً به هر کسی منتقل می کند. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden برنامه‌های Teams و Enterprise را برای شرکت‌ها ارائه می‌دهد تا بتوانید به‌طور ایمن کلمه‌های را با همکاران خود به اشتراک بگذارید. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -چرا Bitwarden را انتخاب کنید: -رمزگذاری در کلاس جهانی -کلمه‌های عبور با رمزگذاری پیشرفته (AES-256 بیت، هشتگ سالت و PBKDF2 SHA-256) محافظت می‌شوند تا داده‌های شما امن و خصوصی بمانند. +More reasons to choose Bitwarden: -تولیدکننده کلمه عبور داخلی -بر اساس الزامات امنیتی برای هر وب سایتی که بازدید می کنید، کلمه‌های عبور قوی، منحصر به فرد و تصادفی ایجاد کنید. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -ترجمه های جهانی -ترجمه Bitwarden به 40 زبان وجود دارد و به لطف جامعه جهانی ما در حال رشد است. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -برنامه های کاربردی چند پلتفرمی -داده‌های حساس را در Bitwarden Vault خود از هر مرورگر، دستگاه تلفن همراه یا سیستم عامل دسکتاپ و غیره ایمن کنید و به اشتراک بگذارید.</value> +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! +</value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>یک مدیریت کننده کلمه عبور رایگان برای تمامی دستگاه‌هایتان</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>همگام‌سازی و دسترسی به گاوصندوق خود از دستگاه های مختلف</value> diff --git a/apps/browser/store/locales/fi/copy.resx b/apps/browser/store/locales/fi/copy.resx index 4440603239..42e914a13f 100644 --- a/apps/browser/store/locales/fi/copy.resx +++ b/apps/browser/store/locales/fi/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,41 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden – Ilmainen salasanahallinta</value> + <value>Bitwarden – Salasanahallinta</value> </data> <data name="Summary" xml:space="preserve"> - <value>Turvallinen ja ilmainen salasanahallinta kaikille laitteillesi</value> + <value>Kotona, töissä tai reissussa, Bitwarden suojaa helposti kaikki salasanasi, avainkoodisi ja arkaluonteiset tietosi.</value> </data> <data name="Description" xml:space="preserve"> - <value>Bitwarden, Inc. on 8bit Solutions LLC:n emoyhtiö. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NIMENNYT PARHAAKSI SALASANOJEN HALLINNAKSI MM. THE VERGE, U.S. NEWS &amp; WORLD REPORT JA CNET. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Hallinnoi, säilytä, suojaa ja jaa salasanoja rajattomalta laitemäärältä mistä tahansa. Bitwarden tarjoaa avoimeen lähdekoodin perustuvan salasanojen hallintaratkaisun kaikille, olitpa sitten kotona, töissä tai liikkeellä. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Luo usein käyttämillesi sivustoille automaattisesti vahvoja, yksilöllisiä ja satunnaisia salasanoja. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send -ominaisuudella lähetät tietoa nopeasti salattuna — tiedostoja ja tekstiä — suoraan kenelle tahansa. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Yritystoimintaan Bitwarden tarjoaa yrityksille Teams- ja Enterprise-tilaukset, jotta salasanojen jakaminen kollegoiden kesken on turvallista. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Miksi Bitwarden?: -Maailmanluokan salaus -Salasanat on suojattu tehokkaalla päästä päähän salauksella (AES-256 bit, suolattu hajautus ja PBKDF2 SHA-256), joten tietosi pysyvät turvassa ja yksityisinä. +More reasons to choose Bitwarden: -Sisäänrakennettu salasanageneraattori -Luo usein käyttämillesi sivustoille vahvoja, yksilöllisiä ja satunnaisia salasanoja. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Monikielinen -Bitwardenin sovelluksia on käännetty yli 40 kielelle ja määrä kasvaa jatkuvasti, kiitos kansainvälisen yhteisömme. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Alustariippumattomaton -Suojaa, käytä ja jaa Bitwarden-holvisi arkaluontoisia tietoja kaikilla selaimilla, mobiililaitteilla, pöytätietokoneilla ja muissa järjestelmissä. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>Turvallinen ja ilmainen salasanahallinta kaikille laitteillesi</value> + <value>Kotona, töissä tai reissussa, Bitwarden suojaa helposti kaikki salasanasi, avainkoodisi ja arkaluonteiset tietosi.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Synkronoi ja hallitse holviasi useilla laitteilla</value> diff --git a/apps/browser/store/locales/fil/copy.resx b/apps/browser/store/locales/fil/copy.resx index 4947fa6996..0f68a90bfa 100644 --- a/apps/browser/store/locales/fil/copy.resx +++ b/apps/browser/store/locales/fil/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,17 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden - Libreng Password Manager</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>Isang ligtas at libreng password manager para sa lahat ng iyong mga aparato.</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>Si Bitwarden, Inc. ang parent company ng 8bit Solutions LLC. Tinawag na Pinakamahusay na Password Manager ng The Verge, U.S. News &amp; World Report, CNET at iba pa. I-manage, i-store, i-secure at i-share ang walang limitasyong mga password sa walang limitasyong mga device mula sa kahit saan. Bitwarden nagbibigay ng mga open source na solusyon sa password management sa lahat, kahit sa bahay, sa trabaho o habang nasa daan. Lumikha ng mga matatas, natatanging, at mga random na password na naka-base sa mga pangangailangan ng seguridad para sa bawat website na madalas mong bisitahin. Ang Bitwarden Send ay nagpapadala ng maayos na naka-encrypt na impormasyon - mga file at plaintext - diretso sa sinuman. Ang Bitwarden ay nag-aalok ng mga Teams at Enterprise plans para sa m. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! + +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. + +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. + +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. + +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. + +Use Bitwarden to secure your workforce and share sensitive information with colleagues. + + +More reasons to choose Bitwarden: + +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. + +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. + +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>Isang ligtas at libreng password manager para sa lahat ng iyong mga aparato.</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>I-sync at i-access ang iyong kahadeyero mula sa maraming mga aparato</value> diff --git a/apps/browser/store/locales/fr/copy.resx b/apps/browser/store/locales/fr/copy.resx index 9d311fe7cf..9927f885d3 100644 --- a/apps/browser/store/locales/fr/copy.resx +++ b/apps/browser/store/locales/fr/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,41 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden – Gestion des mots de passe</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>Un gestionnaire de mots de passe sécurisé et gratuit pour tous vos appareils</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>Bitwarden, Inc. est la société mère de 8bit Solutions LLC. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NOMMÉ MEILLEUR GESTIONNAIRE DE MOTS DE PASSE PAR THE VERGE, U.S. NEWS &amp; WORLD REPORT, CNET, ET PLUS ENCORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Gérez, stockez, sécurisez et partagez un nombre illimité de mots de passe sur un nombre illimité d'appareils, où que vous soyez. Bitwarden fournit des solutions de gestion de mots de passe open source à tout le monde, que ce soit chez soi, au travail ou en déplacement. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Générez des mots de passe robustes, uniques et aléatoires basés sur des exigences de sécurité pour chaque site web que vous fréquentez. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send transmet rapidement des informations chiffrées --- fichiers et texte --- directement à quiconque. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden propose des plans Teams et Enterprise pour les sociétés afin que vous puissiez partager des mots de passe en toute sécurité avec vos collègues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Pourquoi choisir Bitwarden : -Un chiffrement de classe internationale -Les mots de passe sont protégés par un cryptage avancé de bout en bout (AES-256 bit, hachage salé et PBKDF2 SHA-256) afin que vos données restent sécurisées et privées. +More reasons to choose Bitwarden: -Générateur de mots de passe intégré -Générez des mots de passe forts, uniques et aléatoires en fonction des exigences de sécurité pour chaque site web que vous fréquentez. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Traductions internationales -Les traductions de Bitwarden existent dans 40 langues et ne cessent de croître, grâce à notre communauté globale. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Applications multiplateformes -Sécurisez et partagez des données sensibles dans votre coffre Bitwarden à partir de n'importe quel navigateur, appareil mobile ou système d'exploitation de bureau, et plus encore. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>Un gestionnaire de mots de passe sécurisé et gratuit pour tous vos appareils</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Synchroniser et accéder à votre coffre depuis plusieurs appareils</value> diff --git a/apps/browser/store/locales/gl/copy.resx b/apps/browser/store/locales/gl/copy.resx index 04e84f6677..0fdb224988 100644 --- a/apps/browser/store/locales/gl/copy.resx +++ b/apps/browser/store/locales/gl/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,41 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden – Xestor de contrasinais gratuíto</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>Un xestor de contrasinais seguro e gratuíto para todos os teus dispositivos</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>Bitwarden, Inc. é a empresa matriz de 8bit Solutions LLC. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NOMEADO MELLOR ADMINISTRADOR DE CONTRASINAIS POR THE VERGE, Ou.S. NEWS &amp; WORLD REPORT, CNET E MÁS. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -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. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Xere contrasinais seguros, únicas e aleatorias en función dos requisitos de seguridade de cada sitio web que frecuenta. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send transmite rapidamente información cifrada --- arquivos e texto sen formato, directamente a calquera persoa. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden ofrece plans Teams e Enterprise para empresas para que poida compartir contrasinais de forma segura con colegas. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Por que elixir Bitwarden? -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. +More reasons to choose Bitwarden: -Xerador de contrasinais incorporado -Xere contrasinais fortes, únicas e aleatorias en función dos requisitos de seguridade de cada sitio web que frecuenta. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Traducións Globais -As traducións de Bitwarden existen en 40 idiomas e están a crecer, grazas á nosa comunidade global. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -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. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>Un xestor de contrasinais seguro e gratuíto para todos os teus dispositivos</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Sincroniza e accede á túa caixa forte desde múltiples dispositivos</value> diff --git a/apps/browser/store/locales/he/copy.resx b/apps/browser/store/locales/he/copy.resx index cd980970fc..7f366f0e93 100644 --- a/apps/browser/store/locales/he/copy.resx +++ b/apps/browser/store/locales/he/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,41 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden – מנהל ססמאות חינמי</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>מנהל ססמאות חינמי ומאובטח עבור כל המכשירים שלך</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS &amp; WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -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. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose 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. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>מנהל סיסמאות חינמי ומאובטח עבור כל המכשירים שלך</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>סנכרון וגישה לכספת שלך ממגוון מכשירים</value> diff --git a/apps/browser/store/locales/hi/copy.resx b/apps/browser/store/locales/hi/copy.resx index 8db837a3c3..1ea7314d52 100644 --- a/apps/browser/store/locales/hi/copy.resx +++ b/apps/browser/store/locales/hi/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,41 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>बिटवार्डन - मुक्त कूटशब्द प्रबंधक</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>आपके सभी उपकरणों के लिए एक सुरक्षित और नि: शुल्क कूटशब्द प्रबंधक</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS &amp; WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -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. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose 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. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>आपके सभी उपकरणों के लिए एक सुरक्षित और नि: शुल्क पासवर्ड प्रबंधक</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>अनेक उपकरणों से अपने तिजोरी सिंक और एक्सेस करें</value> diff --git a/apps/browser/store/locales/hr/copy.resx b/apps/browser/store/locales/hr/copy.resx index 5ff2bcbe01..dff95b3796 100644 --- a/apps/browser/store/locales/hr/copy.resx +++ b/apps/browser/store/locales/hr/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,40 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden - besplatni upravitelj lozinki</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>Siguran i besplatan upravitelj lozinki za sve vaše uređaje</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>Bitwarden, Inc. je vlasnik tvrtke 8bit Solutions LLC. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -THE VERGE, U.S. NEWS &amp; WORLD REPORT, CNET I DRUGI ODABRALI SU BITWARDEN NAJBOLJIM UPRAVITELJEM LOZINKI. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Upravljajte, spremajte, osigurajte i dijelite neograničen broj lozinki na neograničenom broju uređaja bilo gdje. Bitwarden omogućuje upravljanje lozinkama, bazirano na otvorenom kodu, svima, bilo kod kuće, na poslu ili u pokretu. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generirajte jake, jedinstvene i nasumične lozinke bazirane na sigurnosnim zahtjevima za svaku web stranicu koju često posjećujete. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send omoguzćuje jednostavno i brzo slanje šifriranih podataka --- datoteki ili teksta -- direktno, bilo kome. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden nudi Teams i Enterprise planove za tvrtke kako biste sigurno mogli dijeliti lozinke s kolegama na poslu. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Zašto odabrati Bitwarden? -Svjetski priznata enkripcija -Lozinke su zaštićene naprednim end-to-end šifriranjem (AES-256 bit, salted hashtag i PBKDF2 SHA-256) kako bi vaši osobni podaci ostali sigurni i samo vaši. +More reasons to choose Bitwarden: -Ugrađen generator lozinki -Generirajte jake, jedinstvene i nasumične lozinke bazirane na sigurnosnim zahtjevima za svako web mjesto koje često posjećujete. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Svjetski dostupan -Bitwarden je, zahvaljujući našoj globalnoj zajednici, dostupan na više od 40 jezika. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Podržani svi OS -Osigurajte i sigurno dijelite osjetljive podatke sadržane u vašem Bitwarden trezoru iz bilo kojeg preglednika, mobilnog uređaja ili stolnog računala s bilo kojim OS.</value> +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! +</value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>Siguran i besplatan upravitelj lozinki za sve tvoje uređaje</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Sinkroniziraj i pristupi svojem trezoru s više uređaja</value> diff --git a/apps/browser/store/locales/hu/copy.resx b/apps/browser/store/locales/hu/copy.resx index 0b3761a8ad..3e6b8e42d4 100644 --- a/apps/browser/store/locales/hu/copy.resx +++ b/apps/browser/store/locales/hu/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,36 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden - Ingyenes jelszókezelő</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>Egy biztonságos és ingyenes jelszókezelő az összes eszközre.</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>A Bitwarden, Inc. a 8bit Solutions LLC anyavállalata. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -A VERGE, A US NEWS &amp; WORLD REPORT, a CNET ÉS MÁSOK LEGJOBB JELSZÓKEZELŐJE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Korlátlan számú jelszavak kezelése, tárolása, védelme és megosztása korlátlan eszközökön bárhonnan. A Bitwarden nyílt forráskódú jelszókezelési megoldásokat kínál mindenkinek, legyen az otthon, a munkahelyen vagy útközben. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Hozzunk létre erős, egyedi és véletlenszerű jelszavakat a biztonsági követelmények alapján minden webhelyre, amelyet gyakran látogatunk. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -A Bitwarden Send gyorsan továbbítja a titkosított információkat-fájlokat és egyszerű szöveget közvetlenül bárkinek. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -A Bitwarden csapatokat és vállalati terveket kínál a vállalatok számára, így biztonságosan megoszthatja jelszavait kollégáival. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Miért válasszuk a Bitwardent: -Világszínvonalú titkosítási jelszavak fejlett végpontok közötti titkosítással (AES-256 bit, titkosított hashtag és PBKDF2 SHA-256) védettek, így az adatok biztonságban és titokban maradnak. +More reasons to choose Bitwarden: -Beépített jelszógenerátor A biztonsági követelmények alapján erős, egyedi és véletlenszerű jelszavakat hozhat létre minden gyakran látogatott webhelyen. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Globális fordítások +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -A Bitwarden fordítások 40 nyelven léteznek és globális közösségünknek köszönhetően egyre bővülnek. Többplatformos alkalmazások Biztonságos és megoszthatja az érzékeny adatokat a Bitwarden Széfben bármely böngészőből, mobileszközről vagy asztali operációs rendszerből stb.</value> +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! +</value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>Egy biztonságos és ingyenes jelszókezelő az összes eszközre</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>A széf szinkronizálása és elérése több eszközön.</value> diff --git a/apps/browser/store/locales/id/copy.resx b/apps/browser/store/locales/id/copy.resx index b52252a342..b0791fa3b1 100644 --- a/apps/browser/store/locales/id/copy.resx +++ b/apps/browser/store/locales/id/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,41 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden - Pengelola Sandi Gratis</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>Pengelola sandi yang aman dan gratis untuk semua perangkat Anda</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS &amp; WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -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. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose 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. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>Pengelola sandi yang aman dan gratis untuk semua perangkat Anda</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Sinkronkan dan akses brankas Anda dari beberapa perangkat</value> diff --git a/apps/browser/store/locales/it/copy.resx b/apps/browser/store/locales/it/copy.resx index 56bf9a907c..bcbbe10512 100644 --- a/apps/browser/store/locales/it/copy.resx +++ b/apps/browser/store/locales/it/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,41 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden – Password Manager Gratis</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>Un password manager sicuro e gratis per tutti i tuoi dispositivi</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>Bitwarden, Inc. è la società madre di 8bit Solutions LLC. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NOMINATO MIGLIOR PASSWORD MANAGER DA THE VERGE, U.S. NEWS &amp; WORLD REPORT, CNET, E ALTRO. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Gestisci, archivia, proteggi, e condividi password illimitate su dispositivi illimitati da qualsiasi luogo. Bitwarden offre soluzioni di gestione delle password open-source a tutti, a casa, al lavoro, o in viaggio. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Genera password forti, uniche, e casuali in base ai requisiti di sicurezza per ogni sito web che frequenti. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send trasmette rapidamente informazioni crittate - via file e testo in chiaro - direttamente a chiunque. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offre piani Teams ed Enterprise per le aziende così puoi condividere le password in modo sicuro con i tuoi colleghi. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Perché Scegliere Bitwarden: -Crittografia Di Livello Mondiale -Le password sono protette con crittografia end-to-end avanzata (AES-256 bit, salted hashing, e PBKDF2 SHA-256) per tenere i tuoi dati al sicuro e privati. +More reasons to choose Bitwarden: -Generatore Di Password Integrato -Genera password forti, uniche e casuali in base ai requisiti di sicurezza per ogni sito web che frequenti. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Traduzioni Globali -Le traduzioni di Bitwarden esistono in 40 lingue e sono in crescita grazie alla nostra comunità globale. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Applicazioni Multipiattaforma -Proteggi e condividi i dati sensibili all'interno della tua cassaforte di Bitwarden da qualsiasi browser, dispositivo mobile, o sistema operativo desktop, e altro. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>Un password manager sicuro e gratis per tutti i tuoi dispositivi</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Sincronizza e accedi alla tua cassaforte da più dispositivi</value> diff --git a/apps/browser/store/locales/ja/copy.resx b/apps/browser/store/locales/ja/copy.resx index 13ce1bc4e9..67c479fcde 100644 --- a/apps/browser/store/locales/ja/copy.resx +++ b/apps/browser/store/locales/ja/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,41 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden - 無料パスワードマネージャー</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>あらゆる端末で使える、安全な無料パスワードマネージャー</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>Bitwarden, Inc.は8bit Solutions LLC.の親会社です。 + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -THE VERGEやU.S. NEWS、WORLD REPORT、CNETなどからベストパスワードマネージャーに選ばれました。 +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -端末や場所を問わずパスワードの管理・保存・保護・共有を無制限にできます。Bitwardenは自宅や職場、外出先でもパスワード管理をすべての人に提供し、プログラムコードは公開されています。 +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -よく利用するどのWebサイトでも、セキュリティ条件にそった強力でユニークなパスワードをランダムに生成することができます。 +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Sendは、暗号化した情報(ファイルや平文)をすぐに誰にでも直接送信することができます。 +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwardenは企業向けにTeamsとEnterpriseのプランを提供しており、パスワードを同僚と安全に共有することができます。 +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Bitwardenを選ぶ理由は? -・世界最高レベルの暗号化 -パスワードは高度なエンドツーエンド暗号化(AES-256 bit、salted hashing、PBKDF2 SHA-256)で保護されるので、データは安全に非公開で保たれます。 +More reasons to choose Bitwarden: -・パスワード生成機能 -よく利用するどのWebサイトでも、セキュリティ条件にそった強力でユニークなパスワードをランダムに生成することができます。 +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -・グローバルな翻訳 -Bitwardenは40ヶ国語に翻訳されており、グローバルなコミュニティのおかげで増え続けています。 +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -・クロスプラットフォームアプリケーション -あなたのBitwarden Vaultで、ブラウザ・モバイル機器・デスクトップOSなどの垣根を超えて、機密データを保護・共有することができます。 +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>あらゆる端末で使える、安全な無料パスワードマネージャー</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>複数の端末で保管庫に同期&アクセス</value> diff --git a/apps/browser/store/locales/ka/copy.resx b/apps/browser/store/locales/ka/copy.resx index 191198691d..82e4eb1d88 100644 --- a/apps/browser/store/locales/ka/copy.resx +++ b/apps/browser/store/locales/ka/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,41 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden – Free Password Manager</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>A secure and free password manager for all of your devices</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS &amp; WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -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. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose 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. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>A secure and free password manager for all of your devices</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Sync and access your vault from multiple devices</value> diff --git a/apps/browser/store/locales/km/copy.resx b/apps/browser/store/locales/km/copy.resx index 191198691d..82e4eb1d88 100644 --- a/apps/browser/store/locales/km/copy.resx +++ b/apps/browser/store/locales/km/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,41 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden – Free Password Manager</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>A secure and free password manager for all of your devices</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS &amp; WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -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. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose 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. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>A secure and free password manager for all of your devices</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Sync and access your vault from multiple devices</value> diff --git a/apps/browser/store/locales/kn/copy.resx b/apps/browser/store/locales/kn/copy.resx index 6928f557e4..f68f2c25da 100644 --- a/apps/browser/store/locales/kn/copy.resx +++ b/apps/browser/store/locales/kn/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,40 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>ಬಿಟ್ವರ್ಡ್ – ಉಚಿತ ಪಾಸ್ವರ್ಡ್ ನಿರ್ವಾಹಕ</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>ನಿಮ್ಮ ಎಲ್ಲಾ ಸಾಧನಗಳಿಗೆ ಸುರಕ್ಷಿತ ಮತ್ತು ಉಚಿತ ಪಾಸ್‌ವರ್ಡ್ ನಿರ್ವಾಹಕ</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>ಬಿಟ್ವಾರ್ಡೆನ್, ಇಂಕ್. 8 ಬಿಟ್ ಸೊಲ್ಯೂಷನ್ಸ್ ಎಲ್ಎಲ್ ಸಿ ಯ ಮೂಲ ಕಂಪನಿಯಾಗಿದೆ. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -ವರ್ಜ್, ಯು.ಎಸ್. ನ್ಯೂಸ್ &amp; ವರ್ಲ್ಡ್ ರಿಪೋರ್ಟ್, ಸಿನೆಟ್ ಮತ್ತು ಹೆಚ್ಚಿನದರಿಂದ ಉತ್ತಮ ಪಾಸ್‌ವರ್ಡ್ ವ್ಯವಸ್ಥಾಪಕ ಎಂದು ಹೆಸರಿಸಲಾಗಿದೆ. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -ಎಲ್ಲಿಂದಲಾದರೂ ಅನಿಯಮಿತ ಸಾಧನಗಳಲ್ಲಿ ಅನಿಯಮಿತ ಪಾಸ್‌ವರ್ಡ್‌ಗಳನ್ನು ನಿರ್ವಹಿಸಿ, ಸಂಗ್ರಹಿಸಿ, ಸುರಕ್ಷಿತಗೊಳಿಸಿ ಮತ್ತು ಹಂಚಿಕೊಳ್ಳಿ. ಮನೆಯಲ್ಲಿ, ಕೆಲಸದಲ್ಲಿ ಅಥವಾ ಪ್ರಯಾಣದಲ್ಲಿರಲಿ ಪ್ರತಿಯೊಬ್ಬರಿಗೂ ಬಿಟ್‌ವಾರ್ಡೆನ್ ಓಪನ್ ಸೋರ್ಸ್ ಪಾಸ್‌ವರ್ಡ್ ನಿರ್ವಹಣಾ ಪರಿಹಾರಗಳನ್ನು ನೀಡುತ್ತದೆ. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -ನೀವು ಆಗಾಗ್ಗೆ ಪ್ರತಿ ವೆಬ್‌ಸೈಟ್‌ಗೆ ಸುರಕ್ಷತಾ ಅವಶ್ಯಕತೆಗಳನ್ನು ಆಧರಿಸಿ ಬಲವಾದ, ಅನನ್ಯ ಮತ್ತು ಯಾದೃಚ್ pass ಿಕ ಪಾಸ್‌ವರ್ಡ್‌ಗಳನ್ನು ರಚಿಸಿ. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -ಬಿಟ್‌ವಾರ್ಡೆನ್ ಕಳುಹಿಸಿ ಎನ್‌ಕ್ರಿಪ್ಟ್ ಮಾಡಿದ ಮಾಹಿತಿಯನ್ನು ತ್ವರಿತವಾಗಿ ರವಾನಿಸುತ್ತದೆ --- ಫೈಲ್‌ಗಳು ಮತ್ತು ಸರಳ ಪಠ್ಯ - ನೇರವಾಗಿ ಯಾರಿಗಾದರೂ. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -ಬಿಟ್‌ವಾರ್ಡೆನ್ ಕಂಪೆನಿಗಳಿಗೆ ತಂಡಗಳು ಮತ್ತು ಎಂಟರ್‌ಪ್ರೈಸ್ ಯೋಜನೆಗಳನ್ನು ನೀಡುತ್ತದೆ ಆದ್ದರಿಂದ ನೀವು ಪಾಸ್‌ವರ್ಡ್‌ಗಳನ್ನು ಸಹೋದ್ಯೋಗಿಗಳೊಂದಿಗೆ ಸುರಕ್ಷಿತವಾಗಿ ಹಂಚಿಕೊಳ್ಳಬಹುದು. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -ಬಿಟ್‌ವಾರ್ಡೆನ್ ಅನ್ನು ಏಕೆ ಆರಿಸಬೇಕು: -ವಿಶ್ವ ದರ್ಜೆಯ ಗೂ ry ಲಿಪೀಕರಣ -ಪಾಸ್‌ವರ್ಡ್‌ಗಳನ್ನು ಸುಧಾರಿತ ಎಂಡ್-ಟು-ಎಂಡ್ ಎನ್‌ಕ್ರಿಪ್ಶನ್ (ಎಇಎಸ್ -256 ಬಿಟ್, ಉಪ್ಪುಸಹಿತ ಹ್ಯಾಶ್‌ಟ್ಯಾಗ್ ಮತ್ತು ಪಿಬಿಕೆಡಿಎಫ್ 2 ಎಸ್‌ಎಚ್‌ಎ -256) ನೊಂದಿಗೆ ರಕ್ಷಿಸಲಾಗಿದೆ ಆದ್ದರಿಂದ ನಿಮ್ಮ ಡೇಟಾ ಸುರಕ್ಷಿತ ಮತ್ತು ಖಾಸಗಿಯಾಗಿರುತ್ತದೆ. +More reasons to choose Bitwarden: -ಅಂತರ್ನಿರ್ಮಿತ ಪಾಸ್ವರ್ಡ್ ಜನರೇಟರ್ -ನೀವು ಆಗಾಗ್ಗೆ ಪ್ರತಿ ವೆಬ್‌ಸೈಟ್‌ಗೆ ಸುರಕ್ಷತಾ ಅವಶ್ಯಕತೆಗಳನ್ನು ಆಧರಿಸಿ ಬಲವಾದ, ಅನನ್ಯ ಮತ್ತು ಯಾದೃಚ್ pass ಿಕ ಪಾಸ್‌ವರ್ಡ್‌ಗಳನ್ನು ರಚಿಸಿ. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -ಜಾಗತಿಕ ಅನುವಾದಗಳು -ಬಿಟ್ವಾರ್ಡೆನ್ ಅನುವಾದಗಳು 40 ಭಾಷೆಗಳಲ್ಲಿ ಅಸ್ತಿತ್ವದಲ್ಲಿವೆ ಮತ್ತು ಬೆಳೆಯುತ್ತಿವೆ, ನಮ್ಮ ಜಾಗತಿಕ ಸಮುದಾಯಕ್ಕೆ ಧನ್ಯವಾದಗಳು. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -ಕ್ರಾಸ್ ಪ್ಲಾಟ್‌ಫಾರ್ಮ್ ಅಪ್ಲಿಕೇಶನ್‌ಗಳು -ಯಾವುದೇ ಬ್ರೌಸರ್, ಮೊಬೈಲ್ ಸಾಧನ, ಅಥವಾ ಡೆಸ್ಕ್‌ಟಾಪ್ ಓಎಸ್ ಮತ್ತು ಹೆಚ್ಚಿನವುಗಳಿಂದ ನಿಮ್ಮ ಬಿಟ್‌ವಾರ್ಡನ್ ವಾಲ್ಟ್‌ನಲ್ಲಿ ಸೂಕ್ಷ್ಮ ಡೇಟಾವನ್ನು ಸುರಕ್ಷಿತಗೊಳಿಸಿ ಮತ್ತು ಹಂಚಿಕೊಳ್ಳಿ.</value> +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! +</value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>ನಿಮ್ಮ ಎಲ್ಲಾ ಸಾಧನಗಳಿಗೆ ಸುರಕ್ಷಿತ ಮತ್ತು ಉಚಿತ ಪಾಸ್‌ವರ್ಡ್ ನಿರ್ವಾಹಕ</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>ಅನೇಕ ಸಾಧನಗಳಿಂದ ನಿಮ್ಮ ವಾಲ್ಟ್ ಅನ್ನು ಸಿಂಕ್ ಮಾಡಿ ಮತ್ತು ಪ್ರವೇಶಿಸಿ</value> diff --git a/apps/browser/store/locales/ko/copy.resx b/apps/browser/store/locales/ko/copy.resx index 0fb5dd713f..fdfb93ad6a 100644 --- a/apps/browser/store/locales/ko/copy.resx +++ b/apps/browser/store/locales/ko/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,41 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden - 무료 비밀번호 관리자</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>당신의 모든 기기에서 사용할 수 있는, 안전한 무료 비밀번호 관리자</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>Bitwarden, Inc.은 8bit Solutions LLC.의 모회사입니다. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -VERGE, U.S. NEWS, WORLD REPORT, CNET 등에서 최고의 비밀번호 관리자라고 평가했습니다! +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -관리하고, 보관하고, 보호하고, 어디에서든 어떤 기기에서나 무제한으로 비밀번호를 공유하세요. Bitwarden은 모두에게 오픈소스 비밀번호 관리 솔루션을 제공합니다. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -강하고, 독특하고, 랜덤한 비밀번호를 모든 웹사이트의 보안 요구사항에 따라 생성할 수 있습니다. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send는 빠르게 암호화된 파일과 텍스트를 모두에게 전송할 수 있습니다. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden은 회사들을 위해 팀과 기업 플랜을 제공해서 동료에게 안전하게 비밀번호를 공유할 수 있습니다. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Bitwarden을 선택하는 이유: -세계 최고의 암호화 -비밀번호는 고급 종단간 암호화 (AES-256 bit, salted hashtag, 그리고 PBKDF2 SHA-256)을 이용하여 보호되기 때문에 데이터를 안전하게 보관할 수 있습니다. +More reasons to choose Bitwarden: -내장 비밀번호 생성기 -강하고, 독특하고, 랜덤한 비밀번호를 모든 웹사이트의 보안 요구사항에 따라 생성할 수 있습니다. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -언어 지원 -Bitwarden 번역은 전 세계의 커뮤니티 덕분에 40개의 언어를 지원하고 더 성장하고 있습니다. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -크로스 플랫폼 애플리케이션 -Bitwarden 보관함에 있는 민감한 정보를 어떠한 브라우저, 모바일 기기, 데스크톱 OS 등을 이용하여 보호하고 공유하세요. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>당신의 모든 기기에서 사용할 수 있는, 안전한 무료 비밀번호 관리자</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>여러 기기에서 보관함에 접근하고 동기화할 수 있습니다.</value> diff --git a/apps/browser/store/locales/lt/copy.resx b/apps/browser/store/locales/lt/copy.resx index 92009c5c6d..d83c6ca99a 100644 --- a/apps/browser/store/locales/lt/copy.resx +++ b/apps/browser/store/locales/lt/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,41 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden – nemokamas slaptažodžių tvarkyklė</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>Saugi ir nemokama slaptažodžių tvarkyklė visiems įrenginiams</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>Bitwarden, Inc. yra patronuojančioji 8bit Solutions LLC įmonė. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -GERIAUSIU SLAPTAŽODŽIŲ TVARKYTOJU PRIPAŽINTAS THE VERGE, U.S. NEWS &amp; WORLD REPORT, CNET IR KT. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Tvarkyk, laikyk, saugok ir bendrink neribotą skaičių slaptažodžių neribotuose įrenginiuose iš bet kurios vietos. Bitwarden teikia atvirojo kodo slaptažodžių valdymo sprendimus visiems – tiek namuose, tiek darbe, ar keliaujant. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generuok stiprius, unikalius ir atsitiktinius slaptažodžius pagal saugos reikalavimus kiekvienai lankomai svetainei. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send greitai perduoda užšifruotą informaciją – failus ir paprastą tekstą – tiesiogiai bet kam. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden siūlo komandoms ir verslui planus įmonėms, kad galėtum saugiai dalytis slaptažodžiais su kolegomis. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Kodėl rinktis Bitwarden: -Pasaulinės klasės šifravimas -Slaptažodžiai yra saugomi su pažangiu šifravimu nuo galo iki galo (AES-256 bitų, sūdytu šifravimu ir PBKDF2 SHA-256), todėl tavo duomenys išliks saugūs ir privatūs. +More reasons to choose Bitwarden: -Integruotas slaptažodžių generatorius -Generuok stiprius, unikalius ir atsitiktinius slaptažodžius pagal saugos reikalavimus kiekvienai dažnai lankomai svetainei. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Visuotiniai vertimai -Mūsų pasaulinės bendruomenės dėka Bitwarden vertimai egzistuoja 40 kalbose ir vis daugėja. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Įvairių platformų programos -Apsaugok ir bendrink neskelbtinus duomenis savo Bitwarden Vault iš bet kurios naršyklės, mobiliojo įrenginio ar darbalaukio OS ir kt. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>Saugi ir nemokama slaptažodžių tvarkyklė visiems įrenginiams</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Pasiekite savo saugyklą iš kelių įrenginių</value> diff --git a/apps/browser/store/locales/lv/copy.resx b/apps/browser/store/locales/lv/copy.resx index aec5e836c1..e64cc2eb3a 100644 --- a/apps/browser/store/locales/lv/copy.resx +++ b/apps/browser/store/locales/lv/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,41 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden – Bezmaksas Paroļu Pārvaldnieks</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>Drošs un bezmaksas paroļu pārvaldnieks priekš visām jūsu ierīcēm.</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>Bitwarden, Inc. ir 8bit Solutions LLC mātesuzņēmums. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -THE VERGE, U.S. NEWS &amp; WORLD REPORT, CNET UN CITI ATZINA PAR LABĀKO PAROĻU PĀRVALDNIEKU. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Pārvaldi, uzglabā, aizsargā un kopīgo neierobežotu skaitu paroļu neierobežotā skaitā ierīču no jebkuras vietas. Bitwarden piedāvā atvērtā koda paroļu pārvaldības risinājumus ikvienam - gan mājās, gan darbā, gan ceļā. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Ģenerē spēcīgas, unikālas un nejaušas paroles, pamatojoties uz drošības prasībām, katrai bieži apmeklētai vietnei. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send ātri pārsūta šifrētu informāciju - failus un atklātu tekstu - tieši jebkuram. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden piedāvā Teams un Enterprise plānus uzņēmumiem, lai tu varētu droši kopīgot paroles ar kolēģiem. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Kāpēc izvēlēties Bitwarden: -Pasaules klases šifrēšana -Paroles tiek aizsargātas ar modernu end-to-end šifrēšanu (AES-256 bitu, sālītu šifrēšanu un PBKDF2 SHA-256), lai tavi dati paliktu droši un privāti. +More reasons to choose Bitwarden: -Iebūvēts paroļu ģenerators -Ģenerē spēcīgas, unikālas un nejaušas paroles, pamatojoties uz drošības prasībām katrai bieži apmeklētai vietnei. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Globālie tulkojumi -Bitwarden tulkojumi ir pieejami 40 valodās, un to skaits turpina pieaugt, pateicoties mūsu globālajai kopienai. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Starpplatformu lietojumprogrammas -Nodrošini un kopīgo sensitīvus datus savā Bitwarden Seifā no jebkuras pārlūkprogrammas, mobilās ierīces vai darbvirsmas operētājsistēmas un daudz ko citu. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>Drošs un bezmaksas paroļu pārvaldnieks priekš visām jūsu ierīcēm.</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Sinhronizē un piekļūsti savai glabātavai no vairākām ierīcēm</value> diff --git a/apps/browser/store/locales/ml/copy.resx b/apps/browser/store/locales/ml/copy.resx index cf9b631227..e22993d5b7 100644 --- a/apps/browser/store/locales/ml/copy.resx +++ b/apps/browser/store/locales/ml/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,27 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden - സൗജന്യ പാസ്സ്‌വേഡ് മാനേജർ</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>നിങ്ങളുടെ എല്ലാ ഉപകരണങ്ങൾക്കും സുരക്ഷിതവും സൗജന്യവുമായ പാസ്‌വേഡ് മാനേജർ</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>നിങ്ങളുടെ എല്ലാ ലോഗിനുകളും പാസ്‌വേഡുകളും സംഭരിക്കുന്നതിനുള്ള ഏറ്റവും എളുപ്പവും സുരക്ഷിതവുമായ മാർഗ്ഗമാണ് Bitwarden, ഒപ്പം നിങ്ങളുടെ എല്ലാ ഉപകരണങ്ങളും തമ്മിൽ സമന്വയിപ്പിക്കുകയും ചെയ്യുന്നു. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -പാസ്‌വേഡ് മോഷണം ഗുരുതരമായ പ്രശ്‌നമാണ്. നിങ്ങൾ ഉപയോഗിക്കുന്ന വെബ്‌സൈറ്റുകളും അപ്ലിക്കേഷനുകളും എല്ലാ ദിവസവും ആക്രമണത്തിലാണ്. സുരക്ഷാ ലംഘനങ്ങൾ സംഭവിക്കുകയും നിങ്ങളുടെ പാസ്‌വേഡുകൾ മോഷ്‌ടിക്കപ്പെടുകയും ചെയ്യുന്നു. അപ്ലിക്കേഷനുകളിലും വെബ്‌സൈറ്റുകളിലും ഉടനീളം സമാന പാസ്‌വേഡുകൾ നിങ്ങൾ വീണ്ടും ഉപയോഗിക്കുമ്പോൾ ഹാക്കർമാർക്ക് നിങ്ങളുടെ ഇമെയിൽ, ബാങ്ക്, മറ്റ് പ്രധാനപ്പെട്ട അക്കൗണ്ടുകൾ എന്നിവ എളുപ്പത്തിൽ ആക്‌സസ്സുചെയ്യാനാകും. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -നിങ്ങളുടെ എല്ലാ ഉപകരണങ്ങളിലും സമന്വയിപ്പിക്കുന്ന ഒരു എൻ‌ക്രിപ്റ്റ് ചെയ്ത വാൾട്ടിൽ Bitwarden നിങ്ങളുടെ എല്ലാ ലോഗിനുകളും സംഭരിക്കുന്നു. നിങ്ങളുടെ ഉപകരണം വിടുന്നതിനുമുമ്പ് ഇത് പൂർണ്ണമായും എൻ‌ക്രിപ്റ്റ് ചെയ്‌തിരിക്കുന്നതിനാൽ, നിങ്ങളുടെ ഡാറ്റ നിങ്ങൾക്ക് മാത്രമേ ആക്‌സസ് ചെയ്യാൻ കഴിയൂ . Bitwarden ടീമിന് പോലും നിങ്ങളുടെ ഡാറ്റ വായിക്കാൻ കഴിയില്ല. നിങ്ങളുടെ ഡാറ്റ AES-256 ബിറ്റ് എൻ‌ക്രിപ്ഷൻ, സാൾട്ടിങ് ഹാഷിംഗ്, PBKDF2 SHA-256 എന്നിവ ഉപയോഗിച്ച് അടച്ചിരിക്കുന്നു. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -100% ഓപ്പൺ സോഴ്‌സ് സോഫ്റ്റ്വെയറാണ് Bitwarden . Bitwarden സോഴ്‌സ് കോഡ് GitHub- ൽ ഹോസ്റ്റുചെയ്‌തിരിക്കുന്നു, മാത്രമല്ല എല്ലാവർക്കും ഇത് അവലോകനം ചെയ്യാനും ഓഡിറ്റുചെയ്യാനും ബിറ്റ് വാർഡൻ കോഡ്ബേസിലേക്ക് സംഭാവന ചെയ്യാനും സ്വാതന്ത്ര്യമുണ്ട്. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. + +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. + +Use Bitwarden to secure your workforce and share sensitive information with colleagues. +More reasons to choose Bitwarden: +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. + +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>നിങ്ങളുടെ എല്ലാ ഉപകരണങ്ങൾക്കും സുരക്ഷിതവും സൗജന്യവുമായ പാസ്‌വേഡ് മാനേജർ.</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>ഒന്നിലധികം ഉപകരണങ്ങളിൽ നിന്ന് നിങ്ങളുടെ വാൾട് സമന്വയിപ്പിച്ച് ആക്‌സസ്സുചെയ്യുക diff --git a/apps/browser/store/locales/mr/copy.resx b/apps/browser/store/locales/mr/copy.resx index 191198691d..82e4eb1d88 100644 --- a/apps/browser/store/locales/mr/copy.resx +++ b/apps/browser/store/locales/mr/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,41 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden – Free Password Manager</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>A secure and free password manager for all of your devices</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS &amp; WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -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. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose 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. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>A secure and free password manager for all of your devices</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Sync and access your vault from multiple devices</value> diff --git a/apps/browser/store/locales/my/copy.resx b/apps/browser/store/locales/my/copy.resx index 191198691d..82e4eb1d88 100644 --- a/apps/browser/store/locales/my/copy.resx +++ b/apps/browser/store/locales/my/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,41 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden – Free Password Manager</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>A secure and free password manager for all of your devices</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS &amp; WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -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. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose 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. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>A secure and free password manager for all of your devices</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Sync and access your vault from multiple devices</value> diff --git a/apps/browser/store/locales/nb/copy.resx b/apps/browser/store/locales/nb/copy.resx index 74a8558db6..26a09cc855 100644 --- a/apps/browser/store/locales/nb/copy.resx +++ b/apps/browser/store/locales/nb/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,41 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden — Fri passordbehandling</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>En sikker og fri passordbehandler for alle dine PCer og mobiler</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS &amp; WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -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. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose 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. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>En sikker og fri passordbehandler for alle dine PCer og mobiler</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Synkroniser og få tilgang til ditt hvelv fra alle dine enheter</value> diff --git a/apps/browser/store/locales/ne/copy.resx b/apps/browser/store/locales/ne/copy.resx index 191198691d..82e4eb1d88 100644 --- a/apps/browser/store/locales/ne/copy.resx +++ b/apps/browser/store/locales/ne/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,41 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden – Free Password Manager</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>A secure and free password manager for all of your devices</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS &amp; WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -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. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose 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. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>A secure and free password manager for all of your devices</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Sync and access your vault from multiple devices</value> diff --git a/apps/browser/store/locales/nl/copy.resx b/apps/browser/store/locales/nl/copy.resx index e0779ba777..44dd02b439 100644 --- a/apps/browser/store/locales/nl/copy.resx +++ b/apps/browser/store/locales/nl/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,40 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden - Gratis wachtwoordbeheer</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>Een veilige en gratis oplossing voor wachtwoordbeheer voor al je apparaten</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>Bitwarden, Inc. is het moederbedrijf van 8bit Solutions LLC. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -BESTE WACHTWOORDBEHEERDER VOLGENS THE VERGE, U.S. NEWS &amp; WORLD REPORT, CNET EN ANDEREN. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Beheer, bewaar, beveilig en deel een onbeperkt aantal wachtwoorden op een onbeperkt aantal apparaten, waar je ook bent. Bitwarden levert open source wachtwoordbeheeroplossingen voor iedereen, of dat nu thuis, op het werk of onderweg is. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Genereer sterke, unieke en willekeurige wachtwoorden op basis van beveiligingsvereisten voor elke website die je bezoekt. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send verzendt snel versleutelde informatie --- bestanden en platte tekst -- rechtstreeks naar iedereen. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden biedt Teams- en Enterprise-abonnementen voor bedrijven, zodat je veilig wachtwoorden kunt delen met collega's. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Waarom Bitwarden: -Versleuteling van wereldklasse -Wachtwoorden worden beschermd met geavanceerde end-to-end-codering (AES-256 bit, salted hashtag en PBKDF2 SHA-256) zodat jouw gegevens veilig en privé blijven. +More reasons to choose Bitwarden: -Ingebouwde wachtwoordgenerator -Genereer sterke, unieke en willekeurige wachtwoorden op basis van beveiligingsvereisten voor elke website die je bezoekt. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Wereldwijde vertalingen -Bitwarden-vertalingen bestaan ​​in 40 talen en groeien dankzij onze wereldwijde community. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Platformoverschrijdende toepassingen -Beveilig en deel gevoelige gegevens binnen uw Bitwarden Vault vanuit elke browser, mobiel apparaat of desktop-besturingssysteem, en meer.</value> +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! +</value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>Een veilige en gratis oplossing voor wachtwoordbeheer voor al uw apparaten</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Synchroniseer en gebruik je kluis op meerdere apparaten</value> diff --git a/apps/browser/store/locales/nn/copy.resx b/apps/browser/store/locales/nn/copy.resx index 191198691d..82e4eb1d88 100644 --- a/apps/browser/store/locales/nn/copy.resx +++ b/apps/browser/store/locales/nn/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,41 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden – Free Password Manager</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>A secure and free password manager for all of your devices</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS &amp; WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -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. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose 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. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>A secure and free password manager for all of your devices</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Sync and access your vault from multiple devices</value> diff --git a/apps/browser/store/locales/or/copy.resx b/apps/browser/store/locales/or/copy.resx index 191198691d..82e4eb1d88 100644 --- a/apps/browser/store/locales/or/copy.resx +++ b/apps/browser/store/locales/or/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,41 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden – Free Password Manager</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>A secure and free password manager for all of your devices</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS &amp; WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -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. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose 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. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>A secure and free password manager for all of your devices</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Sync and access your vault from multiple devices</value> diff --git a/apps/browser/store/locales/pl/copy.resx b/apps/browser/store/locales/pl/copy.resx index 5b3941cb7e..60709c7d4d 100644 --- a/apps/browser/store/locales/pl/copy.resx +++ b/apps/browser/store/locales/pl/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,40 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden - darmowy menedżer haseł</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>Bezpieczny i darmowy menedżer haseł dla wszystkich Twoich urządzeń</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>Bitwarden, Inc. jest macierzystą firmą 8bit Solutions LLC. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAZWANY NAJLEPSZYM MENEDŻEREM HASEŁ PRZEZ THE VERGE, US NEWS &amp; WORLD REPORT, CNET I WIĘCEJ. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Zarządzaj, przechowuj, zabezpieczaj i udostępniaj nieograniczoną liczbę haseł na nieograniczonej liczbie urządzeń z każdego miejsca. Bitwarden dostarcza rozwiązania do zarządzania hasłami z otwartym kodem źródłowym każdemu, niezależnie od tego, czy jest w domu, w pracy, czy w podróży. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generuj silne, unikalne i losowe hasła w oparciu o wymagania bezpieczeństwa dla każdej odwiedzanej strony. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Funkcja Bitwarden Send szybko przesyła zaszyfrowane informacje --- pliki i zwykły tekst -- bezpośrednio do każdego. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden oferuje plany dla zespołów i firm, dzięki czemu możesz bezpiecznie udostępniać hasła współpracownikom. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Dlaczego warto wybrać Bitwarden: -Szyfrowanie światowej klasy -Hasła są chronione za pomocą zaawansowanego szyfrowania typu end-to-end (AES-256 bitów, dodatkowy ciąg zaburzający i PBKDF2 SHA-256), dzięki czemu Twoje dane pozostają bezpieczne i prywatne. +More reasons to choose Bitwarden: -Wbudowany generator haseł -Generuj silne, unikalne i losowe hasła w oparciu o wymagania bezpieczeństwa dla każdej odwiedzanej strony. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Przetłumaczone aplikacje -Tłumaczenia Bitwarden są dostępne w 40 językach i rosną dzięki naszej globalnej społeczności. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Aplikacje wieloplatformowe -Zabezpiecz i udostępniaj poufne dane w swoim sejfie Bitwarden z dowolnej przeglądarki, urządzenia mobilnego, systemu operacyjnego i nie tylko.</value> +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! +</value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>Bezpieczny i darmowy menedżer haseł dla wszystkich Twoich urządzeń</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Synchronizacja i dostęp do sejfu z różnych urządzeń</value> diff --git a/apps/browser/store/locales/pt_BR/copy.resx b/apps/browser/store/locales/pt_BR/copy.resx index 48111fa814..8b99c436d0 100644 --- a/apps/browser/store/locales/pt_BR/copy.resx +++ b/apps/browser/store/locales/pt_BR/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,41 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden - Gerenciador de Senhas Gratuito</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>Um gerenciador de senhas gratuito e seguro para todos os seus dispositivos</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>Bitwarden, Inc. é a empresa matriz da 8bit Solutions LLC. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NOMEADA MELHOR GERENCIADORA DE SENHAS PELA VERGE, U.S. NEWS &amp; WORLD REPORT, CNET, E MUITO MAIS. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Gerenciar, armazenar, proteger e compartilhar senhas ilimitadas através de dispositivos ilimitados de qualquer lugar. Bitwarden fornece soluções de gerenciamento de senhas de código aberto para todos, seja em casa, no trabalho ou em viagem. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Gere senhas fortes, únicas e aleatórias com base nos requisitos de segurança para cada site que você frequenta. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -A Bitwarden Send transmite rapidamente informações criptografadas --- arquivos e texto em formato de placa -- diretamente para qualquer pessoa. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden oferece equipes e planos empresariais para empresas para que você possa compartilhar senhas com colegas com segurança. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Por que escolher Bitwarden: -Criptografia de Classe Mundial -As senhas são protegidas com criptografia avançada de ponta a ponta (AES-256 bit, salted hashing e PBKDF2 SHA-256) para que seus dados permaneçam seguros e privados. +More reasons to choose Bitwarden: -Gerador de senhas embutido -Gerar senhas fortes, únicas e aleatórias com base nos requisitos de segurança para cada site que você freqüenta. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Traduções globais -As traduções Bitwarden existem em 40 idiomas e estão crescendo, graças à nossa comunidade global. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Aplicações multiplataforma -Proteja e compartilhe dados sensíveis dentro de seu Bitwarden Vault a partir de qualquer navegador, dispositivo móvel ou SO desktop, e muito mais. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>Um gerenciador de senhas gratuito e seguro para todos os seus dispositivos</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Sincronize e acesse o seu cofre através de múltiplos dispositivos</value> diff --git a/apps/browser/store/locales/pt_PT/copy.resx b/apps/browser/store/locales/pt_PT/copy.resx index 845a94a3ca..d310629612 100644 --- a/apps/browser/store/locales/pt_PT/copy.resx +++ b/apps/browser/store/locales/pt_PT/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,41 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden - Gestor de Palavras-passe Gratuito</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>Um gestor de palavras-passe seguro e gratuito para todos os seus dispositivos</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>A Bitwarden, Inc. é a empresa-mãe da 8bit Solutions LLC. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NOMEADO O MELHOR GESTOR DE PALAVRAS-PASSE PELO THE VERGE, U.S. NEWS &amp; WORLD REPORT, CNET E MUITO MAIS. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Gerir, armazenar, proteger e partilhar palavras-passe ilimitadas em dispositivos ilimitados a partir de qualquer lugar. O Bitwarden fornece soluções de gestão de palavras-passe de código aberto para todos, seja em casa, no trabalho ou onde estiver. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Gera palavras-passe fortes, únicas e aleatórias com base em requisitos de segurança para todos os sites que frequenta. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -O Bitwarden Send transmite rapidamente informações encriptadas - ficheiros e texto simples - diretamente a qualquer pessoa. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -O Bitwarden oferece os planos Equipas e Empresarial destinados a empresas, para que possa partilhar de forma segura as palavras-passe com os seus colegas. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Razões para escolher o Bitwarden: -Encriptação de classe mundial -As palavras-passe são protegidas com encriptação avançada de ponta a ponta (AES-256 bit, salted hashtag e PBKDF2 SHA-256) para que os seus dados permaneçam seguros e privados. +More reasons to choose Bitwarden: -Gerador de palavras-passe incorporado -Gera palavras-passe fortes, únicas e aleatórias com base nos requisitos de segurança para todos os sites que frequenta. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Traduções globais -O Bitwarden está traduzido em 40 idiomas e está a crescer, graças à nossa comunidade global. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Aplicações multiplataforma -Proteja e partilhe dados confidenciais no seu cofre Bitwarden a partir de qualquer navegador, dispositivo móvel ou sistema operativo de computador, e muito mais. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>Um gestor de palavras-passe seguro e gratuito para todos os seus dispositivos</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Sincronize e aceda ao seu cofre através de vários dispositivos</value> diff --git a/apps/browser/store/locales/ro/copy.resx b/apps/browser/store/locales/ro/copy.resx index 0e12b289af..7b0070fad2 100644 --- a/apps/browser/store/locales/ro/copy.resx +++ b/apps/browser/store/locales/ro/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,41 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden - Manager de parole gratuit</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>Un manager de parole sigur și gratuit pentru toate dispozitivele dvs.</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>Bitwarden, Inc. este compania mamă a 8bit Solutions LLC. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NUMIT CEL MAI BUN MANAGER DE PAROLE DE CĂTRE THE VERGE, U.S. NEWS &amp; WORLD REPORT, CNET ȘI MULȚI ALȚII. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Gestionați, stocați, securizați și partajați un număr nelimitat de parole pe un număr nelimitat de dispozitive, de oriunde. Bitwarden oferă soluții open source de gestionare a parolelor pentru toată lumea, fie că se află acasă, la serviciu sau în mișcare. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generați parole puternice, unice și aleatorii, bazate pe cerințe de securitate pentru fiecare site web pe care îl frecventați. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send transmite rapid informații criptate --- fișiere și text simple -- direct către oricine. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden oferă planuri Teams și Enterprise pentru companii, astfel încât să puteți partaja în siguranță parolele cu colegii. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -De ce să alegeți Bitwarden: -Criptare de clasă mondială -Parolele sunt protejate cu criptare avansată end-to-end (AES-256 bit, salted hashing și PBKDF2 SHA-256), astfel încât datele dvs. să rămână sigure și private. +More reasons to choose Bitwarden: -Generator de parole încorporat -Generați parole puternice, unice și aleatorii, bazate pe cerințele de securitate pentru fiecare site web pe care îl frecventați. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Traduceri la nivel mondial -Bitwarden este deja tradus în 40 de limbi și numărul lor crește, datorită comunității noastre mondiale. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Aplicații multi-platformă -Protejați și partajați date sensibile în seiful Bitwarden de pe orice browser, dispozitiv mobil sau sistem de operare desktop și multe altele. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>Un manager de parole sigur și gratuit, pentru toate dispozitivele dvs.</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Sincronizează și accesează seiful dvs. de pe multiple dispozitive</value> diff --git a/apps/browser/store/locales/ru/copy.resx b/apps/browser/store/locales/ru/copy.resx index 4e48ecbc88..212a899f76 100644 --- a/apps/browser/store/locales/ru/copy.resx +++ b/apps/browser/store/locales/ru/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,40 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden – бесплатный менеджер паролей</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>Защищенный и бесплатный менеджер паролей для всех ваших устройств</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>Bitwarden, Inc. является материнской компанией 8bit Solutions LLC. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -НАЗВАН ЛУЧШИМ ДИСПЕТЧЕРОМ ПАРОЛЕЙ VERGE, US NEWS &amp; WORLD REPORT, CNET И МНОГИМИ ДРУГИМИ. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Управляйте, храните, защищайте и делитесь неограниченным количеством паролей на неограниченном количестве устройств из любого места. Bitwarden предоставляет решения с открытым исходным кодом по управлению паролями для всех, дома, на работе или в дороге. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Создавайте надежные, уникальные и случайные пароли на основе требований безопасности для каждого посещаемого вами сайта. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send быстро передает зашифрованную информацию - файлы и простой текст - напрямую кому угодно. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden предлагает для компаний планы Teams и Enterprise, чтобы вы могли безопасно делиться паролями с коллегами. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Почему выбирают Bitwarden: -Шифрование мирового класса -Пароли защищены передовым сквозным шифрованием (AES-256 bit, соленый хэштег и PBKDF2 SHA-256), поэтому ваши данные остаются в безопасности и конфиденциальности. +More reasons to choose Bitwarden: -Встроенный генератор паролей -Создавайте надежные, уникальные и случайные пароли на основе требований безопасности для каждого посещаемого вами сайта. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. - Глобальные переводы - Переводы Bitwarden существуют на 40 языках и постоянно растут благодаря нашему глобальному сообществу. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. - Кросс-платформенные приложения - Защищайте и делитесь конфиденциальными данными в вашем Bitwarden Vault из любого браузера, мобильного устройства, настольной ОС и т. д.</value> +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! +</value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>Защищенный и бесплатный менеджер паролей для всех ваших устройств</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Синхронизация и доступ к хранилищу с нескольких устройств</value> diff --git a/apps/browser/store/locales/si/copy.resx b/apps/browser/store/locales/si/copy.resx index 191198691d..82e4eb1d88 100644 --- a/apps/browser/store/locales/si/copy.resx +++ b/apps/browser/store/locales/si/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,41 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden – Free Password Manager</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>A secure and free password manager for all of your devices</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS &amp; WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -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. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose 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. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>A secure and free password manager for all of your devices</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Sync and access your vault from multiple devices</value> diff --git a/apps/browser/store/locales/sk/copy.resx b/apps/browser/store/locales/sk/copy.resx index ba2a2a5a07..de7fa7dee3 100644 --- a/apps/browser/store/locales/sk/copy.resx +++ b/apps/browser/store/locales/sk/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,41 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden – Bezplatný správca hesiel</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>Bezpečný a bezplatný správca hesiel pre všetky vaše zariadenia</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>Bitwarden, Inc. je materská spoločnosť spoločnosti 8bit Solutions LLC. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -OHODNOTENÝ AKO NAJLEPŠÍ SPRÁVCA HESIEL V THE VERGE, U.S. NEWS &amp; WORLD REPORT, CNET A ĎALŠÍMI. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Spravujte, ukladajte, zabezpečte a zdieľajte neobmedzený počet hesiel naprieč neobmedzeným počtom zariadení odkiaľkoľvek. Bitwarden ponúka open source riešenie na správu hesiel komukoľvek, kdekoľvek doma, v práci alebo na ceste. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Vygenerujte si silné, unikátne a náhodné heslá podľa bezpečnostných požiadaviek na každej stránke, ktorú navštevujete. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send rýchlo prenesie šifrované informácie -- súbory a text -- priamo komukoľvek. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden ponúka Teams a Enterprise paušály pre firmy, aby ste mohli bezpečne zdieľať hesla s kolegami. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Prečo si vybrať Bitwarden: -Svetová trieda v šifrovaní -Heslá sú chránené pokročilým end-to-end šifrovaním (AES-256 bit, salted hash a PBKDF2 SHA-256), takže Vaše dáta zostanú bezpečné a súkromné. +More reasons to choose Bitwarden: -Vstavaný generátor hesiel -Vygenerujte si silné, unikátne a náhodné heslá podľa bezpečnostných požiadaviek na každej stránke, ktorú navštevujete. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Svetová lokalizácia -Vďaka našej globálnej komunite má Bitwarden neustále rastúcu lokalizáciu už do 40 jazykov. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Aplikácie pre rôzne platformy -Zabezpečte a zdieľajte súkromné dáta prostredníctvom Bitwarden trezora z ktoréhokoľvek prehliadača, mobilného zariadenia, alebo stolného počítača a ďalších. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>Bezpečný a bezplatný správca hesiel pre všetky vaše zariadenia</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Synchronizujte a pristupujte k vášmu trezoru z viacerých zariadení</value> diff --git a/apps/browser/store/locales/sl/copy.resx b/apps/browser/store/locales/sl/copy.resx index 83288e3872..80886de48a 100644 --- a/apps/browser/store/locales/sl/copy.resx +++ b/apps/browser/store/locales/sl/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,40 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden - brezplačni upravljalnik gesel</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>Varen in brezplačen upravljalnik gesel za vse vaše naprave</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>Bitwarden, Inc. je matično podjetje podjetja 8bit Solutions LLC. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAJBOŠJI UPRAVLJALNIK GESEL PO MNEJU THE VERGE, U.S. NEWS &amp; WORLD REPORT, CNET IN DRUGIH. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Upravljajte, shranjujte, varujte in delite neomejeno število gesel na neomejenem številu naprav, kjerkoli. Bitwarden ponuja odprtokodne rešitve za upravljanje gesel vsem, tako doma kot v službi ali na poti. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Ustvarite močna, edinstvena in naključna gesla, skladna z varnostnimi zahtevami za vsako spletno mesto, ki ga obiščete. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Z Bitwarden Send hitro prenesite šifrirane informacije --- datoteke in navadno besedilo -- neposredno komurkoli. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden ponuja storitvi za organizacije Teams in Enterprise, s katerima lahko gesla varno delite s sodelavci. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Zakaj izbrati Bitwarden: -Vrhunsko šifriranje -Gesla so zaščitena z naprednim šifriranjem (AES-256, soljene hash-vrednosti in PBKDF2 SHA-256), tako da vaši podatki ostanejo varni in zasebni. +More reasons to choose Bitwarden: -Vgrajeni generator gesel -Ustvarite močna, edinstvena in naključna gesla v skladu z varnostnimi zahtevami za vsako spletno mesto, ki ga obiščete. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Prevodi za ves svet -Bitwarden je preveden že v 40 jezikov, naša globalna skupnost pa ves čas posodabljan in ustvarja nove prevede. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Deluje na vseh platformah -Varujte in delite svoje občutljive podatke znotraj vašega Bitwarden trezorja v katerem koli brskalniku, mobilni napravi, namiznem računalniku in drugje.</value> +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! +</value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>Varen in brezplačen upravljalnik gesel za vse vaše naprave</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Sinhronizirajte svoj trezor gesel in dostopajte do njega z več naprav</value> diff --git a/apps/browser/store/locales/sr/copy.resx b/apps/browser/store/locales/sr/copy.resx index 9bfe799035..9c34d5812a 100644 --- a/apps/browser/store/locales/sr/copy.resx +++ b/apps/browser/store/locales/sr/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,41 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden - Бесплатни Менаџер Лозинке</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>Сигурни и бесплатни менаџер лозинке за сва Ваша уређаја</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>Bitwarden, Inc. је матична компанија фирме 8bit Solutions LLC. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -Именован као најбољи управљач лозинкама од стране новинских сајтова као што су THE VERGE, U.S. NEWS &amp; WORLD REPORT, CNET, и других. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Управљајте, чувајте, обезбедите, и поделите неограничен број лозинки са неограниченог броја уређаја где год да се налазите. Bitwarden свима доноси решења за управљање лозинкама која су отвореног кода, било да сте код куће, на послу, или на путу. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Генеришите јаке, јединствене, и насумичне лозинке у зависности од безбедносних захтева за сваки сајт који често посећујете. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send брзо преноси шифроване информације--- датотеке и обичан текст-- директно и свима. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden нуди планове за компаније и предузећа како бисте могли безбедно да делите лозинке са вашим колегама. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Зашто изабрати Bitwarden: -Шифровање светске класе -Лозинке су заштићене напредним шифровањем од једног до другог краја (AES-256 bit, salted hashing, и PBKDF2 SHA-256) како би ваши подаци остали безбедни и приватни. +More reasons to choose Bitwarden: -Уграђен генератор лозинки -Генеришите јаке, јединствене, и насумичне лозинке у зависности од безбедносних захтева за сваки сајт који често посећујете. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Глобално преведен -Bitwarden преводи постоје за 40 језика и стално се унапређују, захваљујући нашој глобалној заједници. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Вишеплатформне апликације -Обезбедите и поделите осетљиве податке у вашем Bitwarden сефу из било ког претраживача, мобилног уређаја, или desktop оперативног система, и других. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>Сигурни и бесплатни менаџер лозинке за сва Ваша уређаја</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Синхронизујте и приступите сефу са више уређаја</value> diff --git a/apps/browser/store/locales/sv/copy.resx b/apps/browser/store/locales/sv/copy.resx index 8b3cb2a402..6406ab013e 100644 --- a/apps/browser/store/locales/sv/copy.resx +++ b/apps/browser/store/locales/sv/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,41 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden - Gratis lösenordshanterare</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>En säker och gratis lösenordshanterare för alla dina enheter</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>Bitwarden, Inc. är moderbolag till 8bit Solutions LLC. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -UTNÄMND TILL DEN BÄSTA LÖSENORDSHANTERAREN AV THE VERGE, U.S. NEWS &amp; WORLD REPORT, CNET MED FLERA. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Hantera, lagra, säkra och dela ett obegränsat antal lösenord mellan ett obegränsat antal enheter var som helst ifrån. Bitwarden levererar lösningar för lösenordshantering med öppen källkod till alla, vare sig det är hemma, på jobbet eller på språng. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generera starka, unika och slumpmässiga lösenord baserat på säkerhetskrav för varje webbplats du besöker. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send överför snabbt krypterad information --- filer och klartext -- direkt till vem som helst. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden erbjuder abonnemang för team och företag så att du säkert kan dela lösenord med kollegor. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Varför välja Bitwarden: -Kryptering i världsklass -Lösenord skyddas med avancerad end-to-end-kryptering (AES-256 bitar, saltad hashtag och PBKDF2 SHA-256) så att dina data förblir säkra och privata. +More reasons to choose Bitwarden: -Inbyggd lösenordsgenerator -Generera starka, unika och slumpmässiga lösenord baserat på säkerhetskrav för varje webbplats du besöker. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Globala översättningar -Översättningar av Bitwarden finns på 40 språk och antalet växer tack vare vår globala gemenskap. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Plattformsoberoende program -Säkra och dela känsliga data i ditt Bitwardenvalv från alla webbläsare, mobiler och datorer. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>En säker och gratis lösenordshanterare för alla dina enheter</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Synkronisera och kom åt ditt valv från flera enheter</value> diff --git a/apps/browser/store/locales/te/copy.resx b/apps/browser/store/locales/te/copy.resx index 191198691d..82e4eb1d88 100644 --- a/apps/browser/store/locales/te/copy.resx +++ b/apps/browser/store/locales/te/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,41 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden – Free Password Manager</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>A secure and free password manager for all of your devices</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS &amp; WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -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. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose 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. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>A secure and free password manager for all of your devices</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Sync and access your vault from multiple devices</value> diff --git a/apps/browser/store/locales/th/copy.resx b/apps/browser/store/locales/th/copy.resx index 9c8965b01f..f784b1884b 100644 --- a/apps/browser/store/locales/th/copy.resx +++ b/apps/browser/store/locales/th/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,41 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden – โปรแกรมจัดการรหัสผ่านฟรี</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>โปรแกรมจัดการรหัสผ่านที่ปลอดภัยและฟรี สำหรับอุปกรณ์ทั้งหมดของคุณ</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>Bitwarden, Inc. เป็นบริษัทแม่ของ 8bit Solutions LLC + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -ได้รับการระบุชื่อเป็น โปรแกรมจัดการรหัสผ่านที่ดีที่สุด โดย The Verge, U.S. News &amp; World Report, CNET, และที่อื่นๆ +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -สามารถจัดการ จัดเก็บ ปกป้อง และแชร์รหัสผ่านไม่จำกัดจำนวนระหว่างอุปกรณ์ต่างๆ โดยไม่จำกัดจำนวนจากที่ไหนก็ได้ Bitwarden เสนอโซลูชันจัดการรหัสผ่านโอเพนซอร์สให้กับทุกคน ไม่ว่าจะอยู่ที่บ้าน ที่ทำงาน หรือนอกสถานที่ +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -สามารถส่มสร้างรหัสผ่านที่ปลอดภัยและไม่ซ้ำกัน ตามเงื่อนไขความปลอดภัยที่กำหนดได้ สำหรับเว็บไซต์ทุกแห่งที่คุณใช้งานบ่อย +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send สามารถส่งข้อมูลที่ถูกเข้ารหัส --- ไฟล์ หรือ ข้อความ -- ตรงไปยังใครก็ได้ได้อย่างรวดเร็ว +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden มีแผนแบบ Teams และ Enterprise สำหรับบริษัทต่างๆ ซึางคุณสามารถแชร์รหัสผ่านกับเพื่อนร่วมงานได้อย่างปลอดภัย +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -ทำไมควรเลือก Bitwarden: -การเข้ารหัสมาตรฐานโลก -รหัสผ่านจะได้รับการปกป้องด้วยการเข้ารหัสชั้นสูง (AES-256 บิต, salted hashtag, และ PBKDF2 SHA-256) แบบต้นทางถึงปลายทาง เพื่อให้ข้อมูลของคุณปลอดภัยและเป็นส่วนตัว +More reasons to choose Bitwarden: -มีตัวช่วยส่มสร้างรหัสผ่าน -สามารถสุ่มสร้างรหัสผ่านที่ปลอดภัยและไม่ซ้ำกัน ตามเงื่อนไขความปลอดภัยที่กำหนดได้ สำหรับเว็บไซต์ทุกแห่งที่คุณใช้งานบ่อย +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -แปลเป็นภาษาต่างๆ ทั่วโลก -Bitwarden ได้รับการแปลเป็นภาษาต่างๆ กว่า 40 ภาษา และกำลังเพิ่มขึ้นเรื่อยๆ ด้วยความสนับสนุนจากชุมชนผู้ใช้งานทั่วโลก +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -แอปพลิเคชันข้ามแพลตฟอร์ม -ปกป้องและแชร์ข้อมูลอ่อนไหวในตู้เซฟ Bitwarden จากเว็บเบราว์เซอร์ อุปกรณ์มือถือ หรือเดสท็อป หรือช่องทางอื่นๆ +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>โปรแกรมจัดการรหัสผ่านที่ปลอดภัยและฟรี สำหรับอุปกรณ์ทั้งหมดของคุณ</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>ซิงค์และเข้าถึงตู้นิรภัยของคุณจากหลายอุปกรณ์</value> diff --git a/apps/browser/store/locales/tr/copy.resx b/apps/browser/store/locales/tr/copy.resx index 1fc3e2a34b..539aad3aee 100644 --- a/apps/browser/store/locales/tr/copy.resx +++ b/apps/browser/store/locales/tr/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,41 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden - Ücretsiz Parola Yöneticisi</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>Tüm aygıtlarınız için güvenli ve ücretsiz bir parola yöneticisi</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>Bitwarden, Inc., 8bit Solutions LLC’nin ana şirketidir. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -THE VERGE, U.S. NEWS &amp; WORLD REPORT, CNET VE BİRÇOK MEDYA KURULUŞUNA GÖRE EN İYİ PAROLA YÖNETİCİSİ. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Sınırsız sayıda parolayı istediğiniz kadar cihazda yönetin, saklayın, koruyun ve paylaşın. Bitwarden; herkesin evde, işte veya yolda kullanabileceği açık kaynaklı parola yönetim çözümleri sunuyor. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Sık kullandığınız web siteleri için güvenlik gereksinimlerinize uygun, güçlü, benzersiz ve rastgele parolalar oluşturabilirsiniz. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send, şifrelenmiş bilgileri (dosyalar ve düz metinler) herkese hızlı bir şekilde iletmenizi sağlıyor. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden, parolaları iş arkadaşlarınızla güvenli bir şekilde paylaşabilmeniz için şirketlere yönelik Teams ve Enterprise paketleri de sunuyor. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Neden Bitwarden? -Üst düzey şifreleme -Parolalarınız gelişmiş uçtan uca şifreleme (AES-256 bit, salted hashing ve PBKDF2 SHA-256) ile korunuyor, böylece verileriniz güvende ve gizli kalıyor. +More reasons to choose Bitwarden: -Dahili parola oluşturucu -Sık kullandığınız web siteleri için güvenlik gereksinimlerinize uygun, güçlü, benzersiz ve rastgele parolalar oluşturabilirsiniz. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Çeviriler -Bitwarden 40 dilde kullanılabiliyor ve gönüllü topluluğumuz sayesinde çeviri sayısı giderek artıyor. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Her platformla uyumlu uygulamalar -Bitwarden kasanızdaki hassas verilere her tarayıcıdan, mobil cihazdan veya masaüstü işletim sisteminden ulaşabilir ve onları paylaşabilirsiniz. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>Tüm cihazarınız için güvenli ve ücretsiz bir parola yöneticisi</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Hesabınızı senkronize ederek kasanıza tüm cihazlarınızdan ulaşın</value> diff --git a/apps/browser/store/locales/uk/copy.resx b/apps/browser/store/locales/uk/copy.resx index d59cd7f103..5a7de18363 100644 --- a/apps/browser/store/locales/uk/copy.resx +++ b/apps/browser/store/locales/uk/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,41 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden – Безплатний менеджер паролів</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>Захищений, безплатний менеджер паролів для всіх ваших пристроїв</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>8bit Solutions LLC є дочірньою компанією Bitwarden, Inc. + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -НАЙКРАЩИЙ МЕНЕДЖЕР ПАРОЛІВ ЗА ВЕРСІЄЮ THE VERGE, U.S. NEWS &amp; WORLD REPORT, CNET, А ТАКОЖ ІНШИХ ВИДАНЬ. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Зберігайте, захищайте, керуйте і надавайте доступ до паролів на різних пристроях де завгодно. Bitwarden пропонує рішення для керування паролями на основі відкритого програмного коду особистим та корпоративним користувачам на всіх пристроях. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Генеруйте надійні, випадкові та унікальні паролі, які відповідають вимогам безпеки, для кожного вебсайту та сервісу. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Швидко відправляйте будь-кому зашифровану інформацію, як-от файли чи звичайний текст, за допомогою функції Bitwarden Send. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden пропонує командні та корпоративні тарифні плани для компаній, щоб ви могли безпечно обмінюватися паролями з колегами. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Чому варто обрати Bitwarden: -Всесвітньо визнані стандарти шифрування -Паролі захищаються з використанням розширеного наскрізного шифрування (AES-256 bit, хешування з сіллю та PBKDF2 SHA-256), тому ваші дані завжди захищені та приватні. +More reasons to choose Bitwarden: -Вбудований генератор паролів -Генеруйте надійні, випадкові та унікальні паролі, які відповідають вимогам безпеки, для кожного вебсайту та сервісу. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Переклад багатьма мовами -Завдяки нашій глобальній спільноті, Bitwarden перекладено 40 мовами, і їх кількість зростає. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Програми для різних платформ -Зберігайте і діліться важливими даними, а також користуйтеся іншими можливостями у вашому сховищі Bitwarden в будь-якому браузері, мобільному пристрої, чи комп'ютерній операційній системі. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>Захищений, безплатний менеджер паролів для всіх ваших пристроїв</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Синхронізуйте й отримуйте доступ до свого сховища на різних пристроях</value> diff --git a/apps/browser/store/locales/vi/copy.resx b/apps/browser/store/locales/vi/copy.resx index 220d50bdfa..e0403d1f32 100644 --- a/apps/browser/store/locales/vi/copy.resx +++ b/apps/browser/store/locales/vi/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,41 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden - Trình quản lý mật khẩu miễn phí</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>Một trình quản lý mật khẩu an toàn và miễn phí cho mọi thiết bị của bạn</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>Bitwarden, Inc là công ty mẹ của 8bit Solutions LLC + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -ĐƯỢC ĐÁNH GIÁ LÀ TRÌNH QUẢN LÝ MẬT KHẨU TỐT NHẤT BỞI NHÀ BÁO LỚN NHƯ THE VERGE, CNET, U.S. NEWS &amp; WORLD REPORT VÀ HƠN NỮA +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Quản lý, lưu trữ, bảo mật và chia sẻ mật khẩu không giới hạn trên các thiết bị không giới hạn mọi lúc, mọi nơi. Bitwarden cung cấp các giải pháp quản lý mật khẩu mã nguồn mở cho tất cả mọi người, cho dù ở nhà, tại cơ quan hay khi đang di chuyển. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Tạo mật khẩu mạnh, không bị trùng và ngẫu nhiên dựa trên các yêu cầu bảo mật cho mọi trang web bạn thường xuyên sử dụng. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Tính năng 'Bitwarden Send' nhanh chóng truyền thông tin được mã hóa --- tệp và văn bản - trực tiếp đến bất kỳ ai. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden cung cấp các gói 'Nhóm' và 'Doanh nghiệp' cho các công ty để bạn có thể chia sẻ mật khẩu với đồng nghiệp một cách an toàn. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Tại sao bạn nên chọn Bitwarden: -Mã hóa tốt nhất thế giới -Mật khẩu được bảo vệ bằng mã hóa đầu cuối (end-to-end encryption) tiên tiến như AES-256 bit, salted hashtag, và PBKDF2 SHA-256 nên dữ liệu của bạn luôn an toàn và riêng tư. +More reasons to choose Bitwarden: -Trình tạo mật khẩu tích hợp -Tạo mật khẩu mạnh, không bị trùng lặp, và ngẫu nhiên dựa trên các yêu cầu bảo mật cho mọi trang web mà bạn thường xuyên sử dụng. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Bản dịch ngôn ngữ từ cộng đồng -Bitwarden đã có bản dịch 40 ngôn ngữ và đang phát triển nhờ vào cộng đồng toàn cầu của chúng tôi. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Ứng dụng đa nền tảng -Bảo mật và chia sẻ dữ liệu nhạy cảm trong kho lưu trữ Bitwarden của bạn từ bất kỳ trình duyệt, điện thoại thông minh hoặc hệ điều hành máy tính nào, và hơn thế nữa. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>Một trình quản lý mật khẩu an toàn và miễn phí cho mọi thiết bị của bạn</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Đồng bộ hóa và truy cập vào kho lưu trữ của bạn từ nhiều thiết bị</value> diff --git a/apps/browser/store/locales/zh_CN/copy.resx b/apps/browser/store/locales/zh_CN/copy.resx index e424ef743a..94543f8f6f 100644 --- a/apps/browser/store/locales/zh_CN/copy.resx +++ b/apps/browser/store/locales/zh_CN/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,40 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden – 免费密码管理器</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>安全免费的跨平台密码管理器</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>Bitwarden, Inc. 是 8bit Solutions LLC 的母公司。 + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -被 THE VERGE、U.S. NEWS &amp; WORLD REPORT、CNET 等评为最佳的密码管理器。 +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -从任何地方,不限制设备,管理、存储、保护和共享无限的密码。Bitwarden 为每个人提供开源的密码管理解决方案,无论是在家里,在工作中,还是在旅途中。 +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -基于安全要求,为您经常访问的每个网站生成强大、唯一和随机的密码。 +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send 快速传输加密的信息---文件和文本---直接给任何人。 +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden 为公司提供团队和企业计划,因此您可以安全地与同事共享密码。 +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -为何选择 Bitwarden: -世界级的加密技术 -密码受到先进的端到端加密(AES-256 位、盐化标签和 PBKDF2 SHA-256)的保护,为您的数据保持安全和隐密。 +More reasons to choose Bitwarden: -内置密码生成器 -基于安全要求,为您经常访问的每个网站生成强大、唯一和随机的密码。 +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -全球翻译 -Bitwarden 的翻译有 40 种语言,而且还在不断增加,感谢我们的全球社区。 +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -跨平台应用程序 -从任何浏览器、移动设备或桌面操作系统,以及更多的地方,在您的 Bitwarden 密码库中保护和分享敏感数据。</value> +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! +</value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>安全免费的跨平台密码管理器</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>从多台设备同步和访问密码库</value> diff --git a/apps/browser/store/locales/zh_TW/copy.resx b/apps/browser/store/locales/zh_TW/copy.resx index be39fdca06..ab37ed5f7b 100644 --- a/apps/browser/store/locales/zh_TW/copy.resx +++ b/apps/browser/store/locales/zh_TW/copy.resx @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <root> - <!-- - Microsoft ResX Schema - + <!-- + Microsoft ResX Schema + Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes associated with the data types. - + Example: - + ... ado.net/XML headers & schema ... <resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="version">2.0</resheader> @@ -26,36 +26,36 @@ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <comment>This is a comment</comment> </data> - - There are any number of "resheader" rows that contain simple + + There are any number of "resheader" rows that contain simple name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can read any of the formats listed below. - + mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : and then encoded with base64 encoding. - + mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with + value : The object must be serialized with : System.Runtime.Serialization.Formatters.Soap.SoapFormatter : and then encoded with base64 encoding. mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array + value : The object must be serialized into a byte array : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> @@ -118,40 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden – 免費密碼管理工具</value> + <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>安全、免費、跨平台的密碼管理工具</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="Description" xml:space="preserve"> - <value>Bitwarden, Inc. 是 8bit Solutions LLC 的母公司。 + <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -被 THE VERGE、U.S. NEWS &amp; WORLD REPORT、CNET 等評為最佳的密碼管理器。 +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -從任何地方,不限制設備,管理、存儲、保護和共享無限的密碼。Bitwarden 為每個人提供開源的密碼管理解決方案,無論是在家裡,在工作中,還是在旅途中。 +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -基於安全要求,為您經常訪問的每個網站生成強大、唯一和隨機的密碼。 +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send 快速傳輸加密的信息---文檔和文本---直接給任何人。 +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden 為公司提供團隊和企業計劃,因此您可以安全地與同事共享密碼。 +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -為何選擇 Bitwarden: -世界級的加密技術 -密碼受到先進的端到端加密(AES-256 位、鹽化標籤和 PBKDF2 SHA-256)的保護,為您的資料保持安全和隱密。 +More reasons to choose Bitwarden: -內置密碼生成器 -基於安全要求,為您經常訪問的每個網站生成強大、唯一和隨機的密碼。 +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -全球翻譯 -Bitwarden 的翻譯有 40 種語言,而且還在不斷增加,感謝我們的全球社區。 +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -跨平台應用程式 -從任何瀏覽器、行動裝置或桌面作業系統,以及更多的地方,在您的 Bitwarden 密碼庫中保護和分享敏感資料。</value> +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! +</value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>安全、免費、跨平台的密碼管理工具</value> + <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>在多部裝置上同步和存取密碼庫</value> From 14cb4bc5aae3adf91d31423528aa6e164c32c806 Mon Sep 17 00:00:00 2001 From: Cesar Gonzalez <cesar.a.gonzalezcs@gmail.com> Date: Fri, 19 Apr 2024 14:55:34 -0500 Subject: [PATCH 241/351] [PM-7581] Validate cache state from external contexts within LocalBackedSessionStorage (#8842) * [PM-7581] Validate cache state from external contexts within LocalBackedSessionStorage * [PM-7581] Continuing with exploring refining the LocalBackedSessionStorage * [PM-7558] Fix Vault Load Times * [PM-7558] Committing before reworking LocalBackedSessionStorage to function without extending the MemoryStorageService * [PM-7558] Working through refinement of LocalBackedSessionStorage * [PM-7558] Reverting some changes * [PM-7558] Refining implementation and removing unnecessary params from localBackedSessionStorage * [PM-7558] Fixing logic for getting the local session state * [PM-7558] Adding a method to avoid calling bypass cache when a key is known to be a null value * [PM-7558] Fixing tests in a temporary manner * [PM-7558] Removing unnecessary chagnes that affect mv2 * [PM-7558] Removing unnecessary chagnes that affect mv2 * [PM-7558] Adding partition for LocalBackedSessionStorageService * [PM-7558] Wrapping duplicate cache save early return within isDev call * [PM-7558] Wrapping duplicate cache save early return within isDev call * [PM-7558] Wrapping duplicate cache save early return within isDev call --- .../browser/src/background/main.background.ts | 20 +- apps/browser/src/platform/background.ts | 13 +- .../storage-service.factory.ts | 11 +- .../platform/listeners/on-install-listener.ts | 5 + ...cal-backed-session-storage.service.spec.ts | 109 ++++++++-- .../local-backed-session-storage.service.ts | 196 ++++++++++++++---- .../foreground-memory-storage.service.ts | 8 +- 7 files changed, 277 insertions(+), 85 deletions(-) diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index c627c0032b..622a115067 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -226,6 +226,7 @@ import { BackgroundPlatformUtilsService } from "../platform/services/platform-ut import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service"; import { BackgroundDerivedStateProvider } from "../platform/state/background-derived-state.provider"; import { BackgroundMemoryStorageService } from "../platform/storage/background-memory-storage.service"; +import { ForegroundMemoryStorageService } from "../platform/storage/foreground-memory-storage.service"; import { fromChromeRuntimeMessaging } from "../platform/utils/from-chrome-runtime-messaging"; import VaultTimeoutService from "../services/vault-timeout/vault-timeout.service"; import FilelessImporterBackground from "../tools/background/fileless-importer.background"; @@ -394,13 +395,26 @@ export default class MainBackground { ), ); + this.platformUtilsService = new BackgroundPlatformUtilsService( + this.messagingService, + (clipboardValue, clearMs) => this.clearClipboard(clipboardValue, clearMs), + async () => this.biometricUnlock(), + self, + ); + const mv3MemoryStorageCreator = (partitionName: string) => { + if (this.popupOnlyContext) { + return new ForegroundMemoryStorageService(partitionName); + } + // TODO: Consider using multithreaded encrypt service in popup only context return new LocalBackedSessionStorageService( + this.logService, new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, false), this.keyGenerationService, new BrowserLocalStorageService(), new BrowserMemoryStorageService(), + this.platformUtilsService, partitionName, ); }; @@ -469,12 +483,6 @@ export default class MainBackground { this.biometricStateService = new DefaultBiometricStateService(this.stateProvider); this.userNotificationSettingsService = new UserNotificationSettingsService(this.stateProvider); - this.platformUtilsService = new BackgroundPlatformUtilsService( - this.messagingService, - (clipboardValue, clearMs) => this.clearClipboard(clipboardValue, clearMs), - async () => this.biometricUnlock(), - self, - ); this.tokenService = new TokenService( this.singleUserStateProvider, diff --git a/apps/browser/src/platform/background.ts b/apps/browser/src/platform/background.ts index 9c3510178c..a48c420e77 100644 --- a/apps/browser/src/platform/background.ts +++ b/apps/browser/src/platform/background.ts @@ -5,16 +5,11 @@ import MainBackground from "../background/main.background"; import { BrowserApi } from "./browser/browser-api"; const logService = new ConsoleLogService(false); +if (BrowserApi.isManifestVersion(3)) { + startHeartbeat().catch((error) => logService.error(error)); +} const bitwardenMain = ((self as any).bitwardenMain = new MainBackground()); -bitwardenMain - .bootstrap() - .then(() => { - // Finished bootstrapping - if (BrowserApi.isManifestVersion(3)) { - startHeartbeat().catch((error) => logService.error(error)); - } - }) - .catch((error) => logService.error(error)); +bitwardenMain.bootstrap().catch((error) => logService.error(error)); /** * Tracks when a service worker was last alive and extends the service worker diff --git a/apps/browser/src/platform/background/service-factories/storage-service.factory.ts b/apps/browser/src/platform/background/service-factories/storage-service.factory.ts index 19d5a9c140..83e8a780a6 100644 --- a/apps/browser/src/platform/background/service-factories/storage-service.factory.ts +++ b/apps/browser/src/platform/background/service-factories/storage-service.factory.ts @@ -17,6 +17,11 @@ import { KeyGenerationServiceInitOptions, keyGenerationServiceFactory, } from "./key-generation-service.factory"; +import { logServiceFactory, LogServiceInitOptions } from "./log-service.factory"; +import { + platformUtilsServiceFactory, + PlatformUtilsServiceInitOptions, +} from "./platform-utils-service.factory"; export type DiskStorageServiceInitOptions = FactoryOptions; export type SecureStorageServiceInitOptions = FactoryOptions; @@ -25,7 +30,9 @@ export type MemoryStorageServiceInitOptions = FactoryOptions & EncryptServiceInitOptions & KeyGenerationServiceInitOptions & DiskStorageServiceInitOptions & - SessionStorageServiceInitOptions; + SessionStorageServiceInitOptions & + LogServiceInitOptions & + PlatformUtilsServiceInitOptions; export function diskStorageServiceFactory( cache: { diskStorageService?: AbstractStorageService } & CachedServices, @@ -63,10 +70,12 @@ export function memoryStorageServiceFactory( return factory(cache, "memoryStorageService", opts, async () => { if (BrowserApi.isManifestVersion(3)) { return new LocalBackedSessionStorageService( + await logServiceFactory(cache, opts), await encryptServiceFactory(cache, opts), await keyGenerationServiceFactory(cache, opts), await diskStorageServiceFactory(cache, opts), await sessionStorageServiceFactory(cache, opts), + await platformUtilsServiceFactory(cache, opts), "serviceFactories", ); } diff --git a/apps/browser/src/platform/listeners/on-install-listener.ts b/apps/browser/src/platform/listeners/on-install-listener.ts index ef206301e3..adf575a17a 100644 --- a/apps/browser/src/platform/listeners/on-install-listener.ts +++ b/apps/browser/src/platform/listeners/on-install-listener.ts @@ -23,6 +23,11 @@ export async function onInstallListener(details: chrome.runtime.InstalledDetails stateServiceOptions: { stateFactory: new StateFactory(GlobalState, Account), }, + platformUtilsServiceOptions: { + win: self, + biometricCallback: async () => false, + clipboardWriteCallback: async () => {}, + }, }; const environmentService = await environmentServiceFactory(cache, opts); diff --git a/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts b/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts index 7740a22071..a4581e6ac1 100644 --- a/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts +++ b/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts @@ -2,6 +2,8 @@ import { mock, MockProxy } from "jest-mock-extended"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { AbstractMemoryStorageService, AbstractStorageService, @@ -11,16 +13,26 @@ 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 { BrowserApi } from "../browser/browser-api"; + import { LocalBackedSessionStorageService } from "./local-backed-session-storage.service"; -describe("LocalBackedSessionStorage", () => { +describe.skip("LocalBackedSessionStorage", () => { + const sendMessageWithResponseSpy: jest.SpyInstance = jest.spyOn( + BrowserApi, + "sendMessageWithResponse", + ); + let encryptService: MockProxy<EncryptService>; let keyGenerationService: MockProxy<KeyGenerationService>; let localStorageService: MockProxy<AbstractStorageService>; let sessionStorageService: MockProxy<AbstractMemoryStorageService>; + let logService: MockProxy<LogService>; + let platformUtilsService: MockProxy<PlatformUtilsService>; - let cache: Map<string, any>; + let cache: Record<string, unknown>; const testObj = { a: 1, b: 2 }; + const stringifiedTestObj = JSON.stringify(testObj); const key = new SymmetricCryptoKey(Utils.fromUtf8ToArray("00000000000000000000000000000000")); let getSessionKeySpy: jest.SpyInstance; @@ -40,20 +52,24 @@ describe("LocalBackedSessionStorage", () => { }; beforeEach(() => { + sendMessageWithResponseSpy.mockResolvedValue(null); + logService = mock<LogService>(); encryptService = mock<EncryptService>(); keyGenerationService = mock<KeyGenerationService>(); localStorageService = mock<AbstractStorageService>(); sessionStorageService = mock<AbstractMemoryStorageService>(); sut = new LocalBackedSessionStorageService( + logService, encryptService, keyGenerationService, localStorageService, sessionStorageService, + platformUtilsService, "test", ); - cache = sut["cache"]; + cache = sut["cachedSession"]; keyGenerationService.createKeyWithPurpose.mockResolvedValue({ derivedKey: key, @@ -64,19 +80,27 @@ describe("LocalBackedSessionStorage", () => { getSessionKeySpy = jest.spyOn(sut, "getSessionEncKey"); getSessionKeySpy.mockResolvedValue(key); - sendUpdateSpy = jest.spyOn(sut, "sendUpdate"); - sendUpdateSpy.mockReturnValue(); + // sendUpdateSpy = jest.spyOn(sut, "sendUpdate"); + // sendUpdateSpy.mockReturnValue(); }); describe("get", () => { - it("should return from cache", async () => { - cache.set("test", testObj); - const result = await sut.get("test"); - expect(result).toStrictEqual(testObj); + describe("in local cache or external context cache", () => { + it("should return from local cache", async () => { + cache["test"] = stringifiedTestObj; + const result = await sut.get("test"); + expect(result).toStrictEqual(testObj); + }); + + it("should return from external context cache when local cache is not available", async () => { + sendMessageWithResponseSpy.mockResolvedValue(stringifiedTestObj); + const result = await sut.get("test"); + expect(result).toStrictEqual(testObj); + }); }); describe("not in cache", () => { - const session = { test: testObj }; + const session = { test: stringifiedTestObj }; beforeEach(() => { mockExistingSessionKey(key); @@ -117,8 +141,8 @@ describe("LocalBackedSessionStorage", () => { it("should set retrieved values in cache", async () => { await sut.get("test"); - expect(cache.has("test")).toBe(true); - expect(cache.get("test")).toEqual(session.test); + expect(cache["test"]).toBeTruthy(); + expect(cache["test"]).toEqual(session.test); }); it("should use a deserializer if provided", async () => { @@ -148,13 +172,56 @@ describe("LocalBackedSessionStorage", () => { }); describe("remove", () => { + describe("existing cache value is null", () => { + it("should not save null if the local cached value is already null", async () => { + cache["test"] = null; + await sut.remove("test"); + expect(sendUpdateSpy).not.toHaveBeenCalled(); + }); + + it("should not save null if the externally cached value is already null", async () => { + sendMessageWithResponseSpy.mockResolvedValue(null); + await sut.remove("test"); + expect(sendUpdateSpy).not.toHaveBeenCalled(); + }); + }); + it("should save null", async () => { + cache["test"] = stringifiedTestObj; + await sut.remove("test"); expect(sendUpdateSpy).toHaveBeenCalledWith({ key: "test", updateType: "remove" }); }); }); describe("save", () => { + describe("currently cached", () => { + it("does not save the value a local cached value exists which is an exact match", async () => { + cache["test"] = stringifiedTestObj; + await sut.save("test", testObj); + expect(sendUpdateSpy).not.toHaveBeenCalled(); + }); + + it("does not save the value if a local cached value exists, even if the keys not in the same order", async () => { + cache["test"] = JSON.stringify({ b: 2, a: 1 }); + await sut.save("test", testObj); + expect(sendUpdateSpy).not.toHaveBeenCalled(); + }); + + it("does not save the value a externally cached value exists which is an exact match", async () => { + sendMessageWithResponseSpy.mockResolvedValue(stringifiedTestObj); + await sut.save("test", testObj); + expect(sendUpdateSpy).not.toHaveBeenCalled(); + expect(cache["test"]).toBe(stringifiedTestObj); + }); + + it("saves the value if the currently cached string value evaluates to a falsy value", async () => { + cache["test"] = "null"; + await sut.save("test", testObj); + expect(sendUpdateSpy).toHaveBeenCalledWith({ key: "test", updateType: "save" }); + }); + }); + describe("caching", () => { beforeEach(() => { localStorageService.get.mockResolvedValue(null); @@ -167,21 +234,21 @@ describe("LocalBackedSessionStorage", () => { }); it("should remove key from cache if value is null", async () => { - cache.set("test", {}); - const cacheSetSpy = jest.spyOn(cache, "set"); - expect(cache.has("test")).toBe(true); + cache["test"] = {}; + // const cacheSetSpy = jest.spyOn(cache, "set"); + expect(cache["test"]).toBe(true); await sut.save("test", null); // Don't remove from cache, just replace with null - expect(cache.get("test")).toBe(null); - expect(cacheSetSpy).toHaveBeenCalledWith("test", null); + expect(cache["test"]).toBe(null); + // expect(cacheSetSpy).toHaveBeenCalledWith("test", null); }); it("should set cache if value is non-null", async () => { - expect(cache.has("test")).toBe(false); - const setSpy = jest.spyOn(cache, "set"); + expect(cache["test"]).toBe(false); + // const setSpy = jest.spyOn(cache, "set"); await sut.save("test", testObj); - expect(cache.get("test")).toBe(testObj); - expect(setSpy).toHaveBeenCalledWith("test", testObj); + expect(cache["test"]).toBe(stringifiedTestObj); + // expect(setSpy).toHaveBeenCalledWith("test", stringifiedTestObj); }); }); diff --git a/apps/browser/src/platform/services/local-backed-session-storage.service.ts b/apps/browser/src/platform/services/local-backed-session-storage.service.ts index 3f01e4169e..146eb11b2b 100644 --- a/apps/browser/src/platform/services/local-backed-session-storage.service.ts +++ b/apps/browser/src/platform/services/local-backed-session-storage.service.ts @@ -1,8 +1,10 @@ -import { Observable, Subject, filter, map, merge, share, tap } from "rxjs"; +import { Subject } from "rxjs"; import { Jsonify } from "type-fest"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { AbstractMemoryStorageService, AbstractStorageService, @@ -13,57 +15,77 @@ import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { MemoryStorageOptions } from "@bitwarden/common/platform/models/domain/storage-options"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; -import { fromChromeEvent } from "../browser/from-chrome-event"; +import { BrowserApi } from "../browser/browser-api"; import { devFlag } from "../decorators/dev-flag.decorator"; import { devFlagEnabled } from "../flags"; +import { MemoryStoragePortMessage } from "../storage/port-messages"; +import { portName } from "../storage/port-name"; export class LocalBackedSessionStorageService extends AbstractMemoryStorageService implements ObservableStorageService { - private cache = new Map<string, unknown>(); private updatesSubject = new Subject<StorageUpdate>(); - - private commandName = `localBackedSessionStorage_${this.name}`; - private encKey = `localEncryptionKey_${this.name}`; - private sessionKey = `session_${this.name}`; - - updates$: Observable<StorageUpdate>; + private commandName = `localBackedSessionStorage_${this.partitionName}`; + private encKey = `localEncryptionKey_${this.partitionName}`; + private sessionKey = `session_${this.partitionName}`; + private cachedSession: Record<string, unknown> = {}; + private _ports: Set<chrome.runtime.Port> = new Set([]); + private knownNullishCacheKeys: Set<string> = new Set([]); constructor( + private logService: LogService, private encryptService: EncryptService, private keyGenerationService: KeyGenerationService, private localStorage: AbstractStorageService, private sessionStorage: AbstractStorageService, - private name: string, + private platformUtilsService: PlatformUtilsService, + private partitionName: string, ) { super(); - const remoteObservable = fromChromeEvent(chrome.runtime.onMessage).pipe( - filter(([msg]) => msg.command === this.commandName), - map(([msg]) => msg.update as StorageUpdate), - tap((update) => { - if (update.updateType === "remove") { - this.cache.set(update.key, null); - } else { - this.cache.delete(update.key); - } - }), - share(), - ); + BrowserApi.addListener(chrome.runtime.onConnect, (port) => { + if (port.name !== `${portName(chrome.storage.session)}_${partitionName}`) { + return; + } - remoteObservable.subscribe(); + this._ports.add(port); - this.updates$ = merge(this.updatesSubject.asObservable(), remoteObservable); + const listenerCallback = this.onMessageFromForeground.bind(this); + port.onDisconnect.addListener(() => { + this._ports.delete(port); + port.onMessage.removeListener(listenerCallback); + }); + port.onMessage.addListener(listenerCallback); + // Initialize the new memory storage service with existing data + this.sendMessageTo(port, { + action: "initialization", + data: Array.from(Object.keys(this.cachedSession)), + }); + }); + this.updates$.subscribe((update) => { + this.broadcastMessage({ + action: "subject_update", + data: update, + }); + }); } get valuesRequireDeserialization(): boolean { return true; } + get updates$() { + return this.updatesSubject.asObservable(); + } + async get<T>(key: string, options?: MemoryStorageOptions<T>): Promise<T> { - if (this.cache.has(key)) { - return this.cache.get(key) as T; + if (this.cachedSession[key] != null) { + return this.cachedSession[key] as T; + } + + if (this.knownNullishCacheKeys.has(key)) { + return null; } return await this.getBypassCache(key, options); @@ -71,7 +93,8 @@ export class LocalBackedSessionStorageService async getBypassCache<T>(key: string, options?: MemoryStorageOptions<T>): Promise<T> { const session = await this.getLocalSession(await this.getSessionEncKey()); - if (session == null || !Object.keys(session).includes(key)) { + if (session[key] == null) { + this.knownNullishCacheKeys.add(key); return null; } @@ -80,8 +103,8 @@ export class LocalBackedSessionStorageService value = options.deserializer(value as Jsonify<T>); } - this.cache.set(key, value); - return this.cache.get(key) as T; + void this.save(key, value); + return value as T; } async has(key: string): Promise<boolean> { @@ -89,41 +112,48 @@ export class LocalBackedSessionStorageService } async save<T>(key: string, obj: T): Promise<void> { + // This is for observation purposes only. At some point, we don't want to write to local session storage if the value is the same. + if (this.platformUtilsService.isDev()) { + const existingValue = this.cachedSession[key] as T; + if (this.compareValues<T>(existingValue, obj)) { + this.logService.warning(`Possible unnecessary write to local session storage. Key: ${key}`); + this.logService.warning(obj as any); + } + } + if (obj == null) { return await this.remove(key); } - this.cache.set(key, obj); + this.knownNullishCacheKeys.delete(key); + this.cachedSession[key] = obj; await this.updateLocalSessionValue(key, obj); - this.sendUpdate({ key, updateType: "save" }); + this.updatesSubject.next({ key, updateType: "save" }); } async remove(key: string): Promise<void> { - this.cache.set(key, null); + this.knownNullishCacheKeys.add(key); + delete this.cachedSession[key]; await this.updateLocalSessionValue(key, null); - this.sendUpdate({ key, updateType: "remove" }); - } - - sendUpdate(storageUpdate: StorageUpdate) { - this.updatesSubject.next(storageUpdate); - void chrome.runtime.sendMessage({ - command: this.commandName, - update: storageUpdate, - }); + this.updatesSubject.next({ key, updateType: "remove" }); } private async updateLocalSessionValue<T>(key: string, obj: T) { const sessionEncKey = await this.getSessionEncKey(); const localSession = (await this.getLocalSession(sessionEncKey)) ?? {}; localSession[key] = obj; - await this.setLocalSession(localSession, sessionEncKey); + void this.setLocalSession(localSession, sessionEncKey); } async getLocalSession(encKey: SymmetricCryptoKey): Promise<Record<string, unknown>> { - const local = await this.localStorage.get<string>(this.sessionKey); + if (Object.keys(this.cachedSession).length > 0) { + return this.cachedSession; + } + this.cachedSession = {}; + const local = await this.localStorage.get<string>(this.sessionKey); if (local == null) { - return null; + return this.cachedSession; } if (devFlagEnabled("storeSessionDecrypted")) { @@ -135,9 +165,11 @@ export class LocalBackedSessionStorageService // Error with decryption -- session is lost, delete state and key and start over await this.setSessionEncKey(null); await this.localStorage.remove(this.sessionKey); - return null; + return this.cachedSession; } - return JSON.parse(sessionJson); + + this.cachedSession = JSON.parse(sessionJson); + return this.cachedSession; } async setLocalSession(session: Record<string, unknown>, key: SymmetricCryptoKey) { @@ -192,4 +224,76 @@ export class LocalBackedSessionStorageService await this.sessionStorage.save(this.encKey, input); } } + + private compareValues<T>(value1: T, value2: T): boolean { + if (value1 == null && value2 == null) { + return true; + } + + if (value1 && value2 == null) { + return false; + } + + if (value1 == null && value2) { + return false; + } + + if (typeof value1 !== "object" || typeof value2 !== "object") { + return value1 === value2; + } + + if (JSON.stringify(value1) === JSON.stringify(value2)) { + return true; + } + + return Object.entries(value1).sort().toString() === Object.entries(value2).sort().toString(); + } + + private async onMessageFromForeground( + message: MemoryStoragePortMessage, + port: chrome.runtime.Port, + ) { + if (message.originator === "background") { + return; + } + + let result: unknown = null; + + switch (message.action) { + case "get": + case "getBypassCache": + case "has": { + result = await this[message.action](message.key); + break; + } + case "save": + await this.save(message.key, JSON.parse((message.data as string) ?? null) as unknown); + break; + case "remove": + await this.remove(message.key); + break; + } + + this.sendMessageTo(port, { + id: message.id, + key: message.key, + data: JSON.stringify(result), + }); + } + + protected broadcastMessage(data: Omit<MemoryStoragePortMessage, "originator">) { + this._ports.forEach((port) => { + this.sendMessageTo(port, data); + }); + } + + private sendMessageTo( + port: chrome.runtime.Port, + data: Omit<MemoryStoragePortMessage, "originator">, + ) { + port.postMessage({ + ...data, + originator: "background", + }); + } } diff --git a/apps/browser/src/platform/storage/foreground-memory-storage.service.ts b/apps/browser/src/platform/storage/foreground-memory-storage.service.ts index 1e5220002a..b3ac8de55e 100644 --- a/apps/browser/src/platform/storage/foreground-memory-storage.service.ts +++ b/apps/browser/src/platform/storage/foreground-memory-storage.service.ts @@ -21,12 +21,16 @@ export class ForegroundMemoryStorageService extends AbstractMemoryStorageService } updates$; - constructor() { + constructor(private partitionName?: string) { super(); this.updates$ = this.updatesSubject.asObservable(); - this._port = chrome.runtime.connect({ name: portName(chrome.storage.session) }); + let name = portName(chrome.storage.session); + if (this.partitionName) { + name = `${name}_${this.partitionName}`; + } + this._port = chrome.runtime.connect({ name }); this._backgroundResponses$ = fromChromeEvent(this._port.onMessage).pipe( map(([message]) => message), filter((message) => message.originator === "background"), From 36ea3301ec6d4292830a9bbeddbe606290a732ae Mon Sep 17 00:00:00 2001 From: Victoria League <vleague@bitwarden.com> Date: Fri, 19 Apr 2024 16:11:12 -0400 Subject: [PATCH 242/351] [CL-218] Add new icons for extension refresh (#8805) --- .../src/scss/bwicons/fonts/bwi-font.svg | 21 +++++++++++++----- .../src/scss/bwicons/fonts/bwi-font.ttf | Bin 76108 -> 79272 bytes .../src/scss/bwicons/fonts/bwi-font.woff | Bin 76184 -> 79348 bytes .../src/scss/bwicons/fonts/bwi-font.woff2 | Bin 33420 -> 34628 bytes .../src/scss/bwicons/styles/style.scss | 11 +++++++++ libs/components/src/stories/icons.mdx | 11 +++++++++ 6 files changed, 38 insertions(+), 5 deletions(-) diff --git a/libs/angular/src/scss/bwicons/fonts/bwi-font.svg b/libs/angular/src/scss/bwicons/fonts/bwi-font.svg index e815389126..bc0a348fee 100644 --- a/libs/angular/src/scss/bwicons/fonts/bwi-font.svg +++ b/libs/angular/src/scss/bwicons/fonts/bwi-font.svg @@ -5,11 +5,11 @@ <json> <![CDATA[ { - "fontFamily": "bwi-font", + "copyright": "Bitwarden, Inc.", + "description": "Font generated by IcoMoon.", "designer": "Bitwarden, Inc.", "designerURL": "", - "description": "Font generated by IcoMoon.", - "copyright": "Bitwarden, Inc.", + "fontFamily": "bwi-font", "majorVersion": 1, "minorVersion": 0, "version": "Version 1.0", @@ -32,7 +32,7 @@ <glyph unicode="&#xe903;" glyph-name="share-square" data-tags="bw-share-square" horiz-adv-x="1280" d="M1120 364.416c-8.486 0-16.627-3.372-22.624-9.373-6.003-6.001-9.376-14.14-9.376-22.627v-354.24c0.173-10.982-4.013-21.587-11.642-29.491s-18.080-12.467-29.062-12.685h-814.59c-10.983 0.218-21.432 4.781-29.060 12.685s-11.815 18.509-11.644 29.491v811.648c-0.172 10.984 4.015 21.588 11.644 29.492s18.077 12.465 29.060 12.684h348.544c8.487 0 16.627 3.371 22.627 9.373s9.373 14.14 9.373 22.627c0 8.487-3.372 16.626-9.373 22.627s-14.14 9.373-22.627 9.373h-348.544c-27.956-0.22-54.684-11.524-74.314-31.43s-30.56-46.789-30.39-74.746v-811.648c-0.17-27.955 10.759-54.842 30.39-74.746s46.358-31.213 74.314-31.43h814.59c27.962 0.218 54.688 11.526 74.317 31.43s30.56 46.79 30.387 74.746v354.24c0 8.487-3.366 16.626-9.37 22.627s-14.144 9.373-22.63 9.373zM532.488 198.4c0-8.486 3.372-16.627 9.373-22.624 6.001-6.003 14.14-9.376 22.627-9.376s16.627 3.373 22.627 9.376c6.001 5.997 9.373 14.138 9.373 22.624-0.96 371.201 334.207 491.073 459.519 523.009l-49.728-102.72c-3.699-7.638-4.211-16.434-1.427-24.451s8.64-14.601 16.275-18.301c7.642-3.7 16.435-4.215 24.454-1.43 8.013 2.785 14.598 8.64 18.298 16.278l83.968 173.376c1.837 3.782 2.906 7.888 3.149 12.083s-0.339 8.398-1.715 12.368c-1.382 3.97-3.526 7.629-6.323 10.769-2.79 3.14-6.176 5.699-9.958 7.532l-174.272 84.544c-7.635 3.709-16.435 4.231-24.461 1.453-8.019-2.779-14.611-8.631-18.323-16.269-3.706-7.638-4.23-16.437-1.453-24.461s8.634-14.615 16.269-18.323l106.624-51.648c-141.376-38.592-506.175-177.536-504.895-583.809z" /> <glyph unicode="&#xe904;" glyph-name="hashtag" data-tags="bw-hashtag" d="M990.049 586.607l-235.904 0.448 42.624 269.504c0.704 4.417 0.525 8.928-0.512 13.277-1.043 4.349-2.931 8.45-5.562 12.069-2.624 3.619-5.939 6.686-9.754 9.024-3.808 2.339-8.045 3.903-12.461 4.605s-8.928 0.527-13.28-0.515c-4.346-1.042-8.448-2.93-12.064-5.557-7.315-5.305-12.218-13.296-13.632-22.216l-44.416-280-279.296 0.576 42.112 265.92c0.898 4.504 0.877 9.143-0.061 13.639s-2.774 8.756-5.399 12.525c-2.625 3.769-5.983 6.969-9.874 9.409s-8.236 4.068-12.771 4.788c-4.536 0.72-9.171 0.517-13.626-0.598s-8.64-3.117-12.303-5.888c-3.663-2.771-6.728-6.253-9.012-10.237s-3.741-8.389-4.282-12.95l-43.712-276.48-248.832 0.512c-9.047 0-17.724-3.594-24.121-9.991s-9.991-15.074-9.991-24.121c0-9.047 3.594-17.724 9.991-24.121s15.074-9.991 24.121-9.991l238.144-0.448-44.8-284.035-246.976 0.512c-9.047 0-17.724-3.59-24.121-9.99-6.397-6.394-9.991-15.072-9.991-24.122 0-9.043 3.594-17.722 9.991-24.115 6.397-6.4 15.074-9.997 24.121-9.997l236.288-0.512-40.512-256c-1.42-8.922 0.762-18.048 6.066-25.37s13.297-12.237 22.222-13.67c1.649-0.282 3.319-0.429 4.992-0.448 8.117 0 15.967 2.899 22.13 8.186 6.163 5.28 10.232 12.595 11.47 20.614l42.24 266.56 279.36-0.576-39.936-252.48c-1.421-8.922 0.762-18.048 6.067-25.37s13.293-12.237 22.221-13.67c1.779-0.275 3.578-0.422 5.376-0.448 8.109 0.026 15.949 2.938 22.106 8.218 6.157 5.274 10.234 12.576 11.494 20.582l41.6 262.976 248.384-0.512c9.050 0 17.722 3.597 24.122 9.997 6.394 6.394 9.99 15.072 9.99 24.115 0 9.050-3.597 17.728-9.99 24.122-6.4 6.4-15.072 9.99-24.122 9.99l-237.696 0.512 44.8 284.035 246.528-0.512c9.050 0 17.722 3.594 24.122 9.991 6.394 6.397 9.99 15.074 9.99 24.121s-3.597 17.724-9.99 24.121c-6.4 6.397-15.072 9.991-24.122 9.991h0.448zM629.473 234.989l-279.424 0.576 44.8 284.035 279.36-0.576-44.736-284.035z" /> <glyph unicode="&#xe905;" glyph-name="clone" data-tags="bw-clone" horiz-adv-x="1280" d="M1047.808 723.584h-43.008c-4.71 0-9.222 1.866-12.557 5.188s-5.216 7.832-5.235 12.54v50.496c0 27.567-10.925 54.011-30.381 73.539s-45.862 30.551-73.427 30.653h-650.048c-27.868-0.068-54.574-11.168-74.279-30.873s-30.805-46.412-30.873-74.279v-642.176c0-27.642 10.975-54.15 30.513-73.696 19.538-19.552 46.040-30.541 73.679-30.56h43.008c4.708 0 9.224-1.869 12.558-5.19s5.217-7.827 5.234-12.538v-50.496c0-27.565 10.924-54.010 30.381-73.542 19.457-19.526 45.86-30.547 73.427-30.65h650.048c27.866 0.070 54.573 11.168 74.278 30.874s30.803 46.413 30.874 74.278v642.176c-0.083 27.613-11.085 54.073-30.605 73.604s-45.971 30.55-73.587 30.652zM232.832 113.92c-9.206 0.032-18.026 3.706-24.536 10.214s-10.182 15.328-10.216 24.538v643.136c0.017 9.206 3.686 18.029 10.201 24.532s15.345 10.156 24.551 10.156h649.216c9.286-0.017 18.189-3.709 24.762-10.268 6.566-6.56 10.278-15.455 10.31-24.74v-50.176c0-2.333-0.461-4.644-1.357-6.799s-2.208-4.113-3.859-5.759c-1.651-1.647-3.616-2.952-5.773-3.839s-4.467-1.34-6.803-1.331h-501.376c-27.873-0.084-54.58-11.199-74.283-30.915s-30.802-46.428-30.869-74.301v-486.4c0-4.717-1.875-9.242-5.211-12.582-3.337-3.334-7.862-5.21-12.581-5.21l-42.176-0.256zM1081.92-23.808c-0.051-9.197-3.731-17.997-10.24-24.493s-15.315-10.163-24.512-10.195h-649.216c-9.285 0.019-18.187 3.706-24.758 10.266s-10.28 15.456-10.314 24.742v642.816c0 9.217 3.661 18.056 10.179 24.573s15.357 10.179 24.573 10.179h649.216c9.299 0 18.221-3.695 24.8-10.273s10.272-15.498 10.272-24.799v-642.816z" /> -<glyph unicode="&#xe906;" glyph-name="list-alt" data-tags="bw-list-alt" d="M849.286 497.92h-547.199c-5.941 0-11.638 2.36-15.839 6.561s-6.561 9.898-6.561 15.839c0 5.94 2.36 11.638 6.561 15.839s9.898 6.561 15.839 6.561h547.199c5.939 0 11.642-2.36 15.84-6.561s6.56-9.898 6.56-15.839c0-5.941-2.362-11.638-6.56-15.839s-9.901-6.561-15.84-6.561v0zM849.286 361.601h-547.199c-5.941 0-11.638 2.36-15.839 6.561s-6.561 9.898-6.561 15.839c0 5.94 2.36 11.638 6.561 15.839s9.898 6.561 15.839 6.561h547.199c5.939 0 11.642-2.36 15.84-6.561s6.56-9.898 6.56-15.839c0-5.941-2.362-11.638-6.56-15.839s-9.901-6.561-15.84-6.561v0zM849.286 225.664h-547.199c-5.941 0-11.638 2.362-15.839 6.56s-6.561 9.901-6.561 15.84c0 5.939 2.36 11.638 6.561 15.839s9.898 6.561 15.839 6.561h547.199c5.939 0 11.642-2.36 15.84-6.561s6.56-9.9 6.56-15.839c0-5.939-2.362-11.642-6.56-15.84s-9.901-6.56-15.84-6.56v0zM960 672v-576h-896v576h896zM960 736h-896c-16.974 0-33.252-6.743-45.255-18.745s-18.745-28.281-18.745-45.255v-576c0-16.973 6.743-33.254 18.745-45.254s28.281-18.746 45.255-18.746h896c16.973 0 33.254 6.746 45.254 18.746s18.746 28.282 18.746 45.254v576c0 16.974-6.746 33.252-18.746 45.255s-28.282 18.745-45.254 18.745v0zM164.483 493.697c14.704 0 26.624 11.919 26.624 26.624s-11.92 26.624-26.624 26.624c-14.704 0-26.624-11.92-26.624-26.624s11.92-26.624 26.624-26.624zM164.483 223.616c14.704 0 26.624 11.923 26.624 26.624 0 14.704-11.92 26.624-26.624 26.624s-26.624-11.92-26.624-26.624c0-14.701 11.92-26.624 26.624-26.624zM164.483 358.656c14.704 0 26.624 11.92 26.624 26.624s-11.92 26.624-26.624 26.624c-14.704 0-26.624-11.919-26.624-26.624s11.92-26.624 26.624-26.624z" /> +<glyph unicode="&#xe906;" glyph-name="list-alt" data-tags="bw-list-alt" d="M849.28 518.417h-547.2c-5.952 0-11.648 2.794-15.84 7.74s-6.56 11.667-6.56 18.689c0 7.023 2.368 13.744 6.56 18.69s9.888 7.74 15.84 7.74h547.2c5.952 0 11.648-2.794 15.84-7.74s6.56-11.667 6.56-18.69c0-7.023-2.368-13.743-6.56-18.689s-9.888-7.74-15.84-7.74zM849.28 357.572h-547.2c-5.952 0-11.648 2.794-15.84 7.74s-6.56 11.667-6.56 18.69c0 7.023 2.368 13.743 6.56 18.689s9.888 7.74 15.84 7.74h547.2c5.952 0 11.648-2.794 15.84-7.74s6.56-11.667 6.56-18.689c0-7.023-2.368-13.744-6.56-18.69s-9.888-7.74-15.84-7.74zM849.28 197.178h-547.2c-5.952 0-11.648 2.797-15.84 7.744s-6.56 11.667-6.56 18.688c0 7.021 2.368 13.747 6.56 18.688 4.192 4.947 9.888 7.744 15.84 7.744h547.2c5.952 0 11.648-2.797 15.84-7.744 4.192-4.941 6.56-11.667 6.56-18.688s-2.368-13.741-6.56-18.688c-4.192-4.947-9.888-7.744-15.84-7.744zM960 723.814v-679.622h-896v679.622h896zM960 799.328h-896c-16.96 0-33.248-7.967-45.248-22.125s-18.752-33.377-18.752-53.388v-679.622c0-20.013 6.752-39.232 18.752-53.389 12-14.163 28.288-22.125 45.248-22.125h896c16.96 0 33.248 7.962 45.248 22.125 12 14.157 18.752 33.376 18.752 53.389v679.622c0 20.011-6.752 39.229-18.752 53.388s-28.288 22.125-45.248 22.125zM164.48 513.432c14.72 0 26.624 14.046 26.624 31.414s-11.936 31.414-26.624 31.414c-14.72 0-26.624-14.083-26.624-31.414s11.936-31.414 26.624-31.414zM164.48 194.765c14.72 0 26.624 14.080 26.624 31.411s-11.936 31.416-26.624 31.416c-14.688 0-26.624-14.046-26.624-31.416 0-17.331 11.936-31.411 26.624-31.411zM164.48 354.099c14.72 0 26.624 14.045 26.624 31.414s-11.936 31.414-26.624 31.414c-14.72 0-26.624-14.046-26.624-31.414s11.936-31.414 26.624-31.414z" /> <glyph unicode="&#xe907;" glyph-name="id-card" data-tags="bw-id-card" d="M394.821 380.034c17.933 14.902 30.842 34.964 36.975 57.459s5.19 46.333-2.698 68.275c-7.889 21.941-22.341 40.922-41.394 54.362s-41.781 20.689-65.097 20.761c-23.316 0.072-46.089-7.037-65.224-20.36s-33.704-32.214-41.727-54.106c-8.023-21.892-9.112-45.724-3.118-68.257s18.78-42.673 36.62-57.686c-27.476-11.622-51.745-29.697-70.751-52.691s-32.189-50.232-38.433-79.407c-1.561-7.309-1.457-14.88 0.305-22.15 1.762-7.264 5.135-14.042 9.871-19.834 4.621-5.728 10.471-10.349 17.117-13.517s13.92-4.806 21.283-4.787h265.856c7.363-0.019 14.637 1.619 21.283 4.787s12.496 7.789 17.117 13.517c4.732 5.792 8.107 12.57 9.878 19.834s1.896 14.835 0.362 22.15c-6.211 28.995-19.279 56.077-38.108 78.982s-42.872 40.965-70.116 52.668v0zM322.245 535.362c13.595 0 26.884-4.031 38.188-11.584s20.113-18.288 25.316-30.847c5.203-12.56 6.564-26.381 3.912-39.714s-9.199-25.581-18.812-35.194c-9.613-9.613-21.86-16.159-35.194-18.812s-27.154-1.291-39.714 3.912c-12.56 5.202-23.295 14.012-30.847 25.316s-11.585 24.593-11.585 38.188c0.017 18.225 7.265 35.699 20.151 48.585s30.36 20.134 48.585 20.151v0zM454.405 232.896h-265.856l-4.736 6.4c6.817 31.429 24.187 59.576 49.22 79.761s56.223 31.193 88.38 31.193c32.158 0 63.347-11.008 88.38-31.193s42.403-48.332 49.22-79.761l-4.608-6.4zM610.752 491.645h160.576c5.651 0.427 10.931 2.973 14.778 7.126 3.853 4.154 5.997 9.609 5.997 15.274s-2.144 11.12-5.997 15.274c-3.846 4.154-9.126 6.699-14.778 7.126h-160.576c-5.645-0.428-10.925-2.973-14.778-7.126s-5.99-9.609-5.99-15.274c0-5.665 2.138-11.12 5.99-15.274s9.133-6.7 14.778-7.126v0zM840.89 276.602h-230.4c-5.651-0.428-10.931-2.973-14.778-7.126-3.853-4.154-5.99-9.609-5.99-15.274s2.138-11.117 5.99-15.277c3.846-4.154 9.126-6.694 14.778-7.123h230.4c5.645 0.429 10.925 2.97 14.778 7.123 3.853 4.16 5.99 9.613 5.99 15.277s-2.138 11.12-5.99 15.274c-3.853 4.154-9.133 6.699-14.778 7.126v0zM840.896 406.524h-230.4c-5.946 0-11.642-2.36-15.84-6.561-4.205-4.201-6.56-9.899-6.56-15.839s2.355-11.638 6.56-15.839c4.198-4.201 9.894-6.561 15.84-6.561h230.4c5.939 0 11.635 2.36 15.84 6.561 4.198 4.201 6.56 9.898 6.56 15.839s-2.362 11.638-6.56 15.839c-4.205 4.2-9.901 6.561-15.84 6.561v0zM960 736h-896c-16.974 0-33.252-6.743-45.255-18.745s-18.745-28.281-18.745-45.255v-576c0-16.973 6.743-33.254 18.745-45.254s28.281-18.746 45.255-18.746h896c16.973 0 33.254 6.746 45.254 18.746s18.746 28.282 18.746 45.254v576c0 16.974-6.746 33.252-18.746 45.255s-28.282 18.745-45.254 18.745v0zM928 96h-832c-8.487 0-16.627 3.373-22.627 9.37-6.001 6.003-9.373 14.144-9.373 22.63v512c0 8.487 3.372 16.627 9.373 22.627s14.14 9.373 22.627 9.373h836.8c5.632-1.28 26.752-7.424 27.2-27.648v-516.352c0-8.486-3.373-16.627-9.37-22.63-6.003-5.997-14.144-9.37-22.63-9.37z" /> <glyph unicode="&#xe908;" glyph-name="credit-card" data-tags="bw-credit-card" d="M937.727 736h-851.455c-24.134-1.389-46.742-12.257-62.901-30.237s-24.559-41.617-23.371-65.763v-512c-1.188-24.147 7.212-47.782 23.371-65.76 16.159-17.984 38.767-28.851 62.901-30.24h851.199c24.179 1.325 46.854 12.166 63.066 30.15 16.218 17.99 24.653 41.664 23.462 65.85v512c1.19 24.145-7.213 47.782-23.366 65.763-16.16 17.98-38.771 28.849-62.906 30.237v0zM86.528 672h851.199c8.026-0.495 15.533-4.131 20.902-10.119 5.363-5.989 8.154-13.85 7.77-21.881v-57.6c0.371-6.93-2.022-13.724-6.656-18.893-4.627-5.17-11.117-8.294-18.048-8.691h-859.455c-6.917 0.413-13.39 3.545-18.006 8.712s-7.001 11.952-6.634 18.872v57.6c-0.385 8.031 2.407 15.892 7.773 21.881s12.874 9.624 20.899 10.119h0.256zM937.727 96.384h-851.455c-7.956 0.493-15.405 4.077-20.76 9.984-5.355 5.901-8.194 13.664-7.912 21.632v334.848c-0.367 6.93 2.027 13.724 6.657 18.893s11.119 8.294 18.047 8.691h859.519c6.931-0.397 13.421-3.521 18.048-8.691 4.634-5.169 7.027-11.963 6.656-18.893v-334.848c0.384-8.051-2.426-15.93-7.814-21.926-5.395-5.99-12.934-9.613-20.986-10.074v0.384zM860.991 214.080h-150.592c-5.434-0.717-10.426-3.379-14.042-7.501s-5.606-9.421-5.606-14.899c0-5.485 1.99-10.778 5.606-14.899s8.608-6.79 14.042-7.501h150.848c5.44 0.71 10.432 3.379 14.048 7.501s5.606 9.414 5.606 14.899c0 5.478-1.99 10.778-5.606 14.899s-8.608 6.784-14.048 7.501h-0.256z" /> <glyph unicode="&#xe909;" glyph-name="globe" data-tags="bw-globe" d="M930.184 88.52c59.091 83.476 93.816 185.424 93.816 295.48 0 282.77-229.23 512-512 512s-512-229.23-512-512c0-282.77 229.23-512 512-512 171.145 0 322.676 83.971 415.642 212.961 1.006 1.061 1.935 2.19 2.784 3.385l-0.244 0.172zM832 70.465c-81.314-82.98-194.645-134.465-320-134.465-63.715 0-124.324 13.3-179.2 37.277-21.299 9.305-16.91 15.104-8.811 34.296 4.699 11.135 9.864 23.375 15.909 35.455 7.697 15.384 15.951 28.647 24.705 37.352 1.515 1.506 5.85 4.090 15.912 6.251 9.594 2.061 21.82 3.232 36.457 4.041 7.999 0.441 16.73 0.765 25.91 1.107 23.127 0.857 49.1 1.822 73.377 5.041 18.186 2.411 32.81 7.18 43.754 14.867 11.515 8.090 18.012 18.78 20.357 30.439 2.201 10.946 0.499 21.475-1.724 29.742-2.002 7.449-4.857 14.5-6.892 19.526l-0.636 1.576c-2.829 7.031-5.681 12.705-8.416 18.146-7.269 14.461-13.71 27.272-16.655 59.502-1.725 18.865 5.205 40.067 14.539 62.454 1.612 3.869 3.379 7.96 5.154 12.069 2.911 6.745 5.842 13.534 8.14 19.441 3.665 9.421 7.42 20.586 7.42 31.075 0 10.317-3.304 21.324-7.716 31.197-4.547 10.174-10.922 20.594-18.507 30.022-14.277 17.747-36.8 36.675-63.32 36.675-25.509 0-58.336-9.242-86.206-20.164-14.232-5.576-27.831-11.849-39.35-18.129-11.162-6.086-21.606-12.84-28.564-19.649-53.851-52.712-85.779-67.957-104.871-69.051-16.066-0.92-27.702 8.251-42.636 28.821-3.615 4.979-7.175 10.267-10.97 15.922l-0.392 0.584c-3.615 5.387-7.449 11.099-11.449 16.669-8.234 11.46-17.975 23.477-30.22 32.897-12.569 9.669-27.737 16.57-46.282 17.735-18.619 1.17-16.969 0.766-11.862 20.012 17.102 64.456 48.212 123.209 89.874 172.8 17.744 21.12 17.664 25.824 25.696 0 2.826-9.089 6.315-17.189 10.452-24.354 13.282-23.009 32.491-35.040 53.665-39.274 23.5-4.699 49.65 3.385 73.011 11.849 7.214 2.614 14.36 5.345 21.509 8.077l0.014 0.005c17.91 6.845 35.845 13.701 54.921 18.73 14.987 3.95 33.651 3.106 55.514-0.701 15.329-2.67 30.671-6.465 46.395-10.355 6.852-1.696 13.78-3.41 20.807-5.056 21.987-5.15 45.349-9.722 65.804-8.086 21.715 1.74 42.479 10.815 54.159 34.177 7.324 14.647 7.19 28.631 1.341 41.224-5.014 10.794-13.872 19.486-20.296 25.762-15.024 14.68-23.82 23.441-23.82 37.6 0 6.876 2.105 11.022 5.205 14.43 3.661 4.024 9.342 7.656 17.537 11.61 3.42 1.649 6.892 3.181 10.645 4.837l2.059 0.907c2.996 1.326 6.186 2.754 9.356 4.287 12.957 6.272 15.582 6.545 29.4 0.866 97.087-39.905 177.069-112.926 225.88-205.005 19.9-37.539-23.677-14.946-52.631-24.824-19.726-6.729-34.947-20.159-47.387-40.581-7.062-11.595-16.431-23.545-26.812-36.786l-0.020-0.027c-1.799-2.294-3.627-4.627-5.48-7.002-12.034-15.435-24.982-32.635-33.635-50.425-8.729-17.951-14.191-38.742-8.181-60.53 6.017-21.809 22.392-40.526 48.469-56.835 5.739-3.589 13.009-6.157 18.219-8l1.165-0.412c6.247-2.22 11.264-4.076 15.49-6.445 4.069-2.281 6.132-4.361 7.22-6.245 0.935-1.619 2.139-4.769 1.271-11.285-2.747-20.62-3.99-40.064-2.999-56.736 0.951-15.994 4.090-32.37 12.604-44.354 6.811-9.585 6.907-23.137 6.256-43.137l-0.019-0.567c-0.275-8.441-0.622-19.069 1.25-28.412 4.542-22.661 7.657-21.567-10.861-40.465zM866.675 205.289c-2.111 2.97-4.741 10.371-5.575 24.389-0.792 13.336 0.165 30.132 2.73 49.386 1.79 13.422-0.212 25.394-6.080 35.556-5.714 9.897-14.047 16.255-21.705 20.545-7.437 4.166-15.407 6.997-21.262 9.077l-0.142 0.051c-6.867 2.44-10.086 3.665-11.877 4.785-21.38 13.371-29.264 25.247-31.812 34.49-2.555 9.262-0.891 19.975 5.699 33.526 6.666 13.71 17.224 28.009 29.385 43.61 1.69 2.169 3.417 4.369 5.166 6.596 10.292 13.111 21.304 27.141 29.656 40.852 8.811 14.466 17.496 20.975 26.991 24.214 10.419 3.555 24.157 4.002 44.691 1.595 22.156-2.599 24.315-5.701 30.406-27.11 11.107-39.039 17.054-80.25 17.054-122.85 0-85.431-23.912-165.277-65.41-233.211-24.052-39.375-12.296 32.52-27.914 54.5zM579.575 826.937c38.434-5.815 25.674-2.582 5.221-25.070-9.301-10.226-15.197-23.502-15.197-40.267 0-30.954 20.262-50.482 32.855-62.617l0.011-0.011c0.879-0.847 1.72-1.659 2.515-2.436 7.179-7.012 10.719-11.054 12.306-14.471 0.751-1.617 1.417-3.324-0.859-7.877-3.991-7.981-10.349-12.067-22.88-13.071-13.791-1.105-31.8 2.002-53.981 7.196-6.016 1.407-12.354 2.976-18.882 4.59-16.281 4.027-33.767 8.351-50.487 11.262-23.575 4.106-48.821 6.082-71.89 0.002-21.224-5.595-42.181-13.605-60.704-20.686-6.697-2.559-13.074-4.997-19.032-7.155-24.369-8.827-40.82-12.615-52.402-10.297-10.83 2.166-20.422 7.795-27.94 20.817-4.525 7.837-8.469 18.681-11.049 33.505-3.355 19.265-4.226 26.485 11.049 38.294 75.709 58.529 170.672 93.355 273.771 93.355 22.97 0 45.536-1.729 67.575-5.062zM64.719 409.6c1.169 20.754-0.175 32.761 23.686 31.262 9.787-0.615 17.806-4.1 25.276-9.846 7.795-5.996 14.944-14.424 22.446-24.866 3.636-5.062 7.17-10.326 10.884-15.859l0.261-0.389c3.742-5.577 7.684-11.439 11.779-17.079 15.905-21.91 38.464-46.744 75.906-44.6 34.414 1.97 75.305 26.86 129.537 79.946 3.111 3.045 9.817 7.779 20.086 13.376 9.911 5.404 22.022 11.016 34.976 16.091 26.501 10.384 53.952 17.516 72.197 17.516 8.65 0 21.3-7.304 33.401-22.345 5.604-6.967 10.221-14.58 13.369-21.621 3.281-7.34 4.374-12.73 4.374-15.527 0-2.626-1.19-7.856-4.807-17.152-2.12-5.447-4.482-10.911-7.111-16.985-1.816-4.199-3.761-8.689-5.836-13.669-9.351-22.427-20.007-51.5-17.336-80.727 3.664-40.064 13.882-60.309 21.676-75.747 2.287-4.532 4.367-8.651 6.010-12.736l0.606-1.504c2.131-5.284 4.106-10.179 5.465-15.237 1.487-5.532 1.707-9.486 1.161-12.201-0.404-2.005-1.325-4.156-4.785-6.587-4.032-2.832-11.94-6.261-26.727-8.222-22.38-2.967-45.3-3.815-67.716-4.642-9.666-0.359-19.241-0.711-28.642-1.231-15.040-0.831-29.742-2.119-42.405-4.84-12.195-2.619-25.446-7.141-34.922-16.565-13.227-13.154-23.709-30.889-31.97-47.399-6.662-13.314-12.452-27.045-17.18-38.255l-0.005-0.012c-6.836-16.21-11.089-20.109-27.279-9.144-118.929 80.545-197.091 216.745-197.091 371.2 0 8.591 0.242 17.127 0.719 25.6z" /> @@ -154,7 +154,7 @@ <glyph unicode="&#xe97d;" glyph-name="arrow-circle-down" data-tags="arrow-circle-down" d="M64.886 384.002c0 87.354 26.212 172.721 75.339 245.336s118.963 129.223 200.673 162.647c81.711 33.424 171.611 42.173 258.324 25.116s166.413-59.092 228.926-120.86c62.541-61.769 105.101-140.456 122.37-226.099s8.384-174.46-25.43-255.134c-33.842-80.702-91.157-149.647-164.68-198.196s-159.985-74.41-248.402-74.41c-118.543 0.138-232.196 46.699-316.003 129.499s-130.978 195.022-131.118 312.101zM903.236 384.002c0 76.424-22.944 151.138-65.923 214.673-42.978 63.535-104.095 113.077-175.578 142.306-71.483 29.256-150.148 36.901-226.047 21.997s-145.593-51.722-200.31-105.736c-54.716-54.041-91.966-122.875-107.057-197.837s-7.35-152.656 22.272-223.256c29.621-70.601 79.755-130.962 144.084-173.411s139.977-65.108 217.357-65.108c103.732 0.11 203.161 40.876 276.515 113.298s114.603 170.651 114.715 273.102l-0.028-0.028zM653.057 229.399l-110.455-102.383c-9.467-8.69-21.997-13.306-34.886-12.87s-25.061 5.902-33.893 15.219l-102.12 108.040c-4.664 5.519-7.038 12.597-6.596 19.783s3.643 13.935 8.97 18.853c5.327 4.918 12.365 7.596 19.624 7.541s14.242-2.896 19.458-7.896l63.646-67.327c0.718-0.465 1.546-0.739 2.401-0.792 0.856-0.055 1.711 0.082 2.484 0.438 0.8 0.356 1.463 0.874 1.987 1.558 0.524 0.682 0.856 1.447 0.994 2.295l1.38 421.227c0.331 7.24 3.45 14.072 8.749 19.099s12.337 7.814 19.651 7.814c7.314 0 14.352-2.787 19.651-7.814s8.418-11.859 8.749-19.099l-1.546-418.112c3.036-9.235 11.316-1.858 11.316-1.858l61.769 57.326c5.575 4.645 12.724 6.967 19.982 6.53s14.076-3.606 19.044-8.881c4.968-5.274 7.673-12.241 7.618-19.427s-2.926-14.1-7.976-19.264z" /> <glyph unicode="&#xe97e;" glyph-name="undo" data-tags="undo" d="M69.695 605.356c5.916 3.346 12.942 4.267 19.569 2.594s12.316-5.8 15.873-11.517l51.197-84.382c0.882-1.45 2.191-2.621 3.726-3.375s3.27-1.087 4.977-0.948c1.707 0.139 3.356 0.781 4.75 1.784s2.417 2.399 3.042 3.988c32.368 88.313 96.224 162.183 179.818 207.915 94.006 52.341 205.105 66.591 309.69 39.765 104.188-25.655 193.756-90.796 248.992-181.145 27.107-43.975 44.799-92.887 52.023-143.778s3.84-102.674-9.984-152.255c-13.795-49.943-37.659-96.679-70.169-137.447-32.512-40.768-72.985-74.762-119.092-99.97-94.005-52.341-205.133-66.591-309.69-39.765-86.951 21.5-164.232 70.634-219.696 139.707-2.076 2.565-3.584 5.522-4.465 8.673-0.882 3.151-1.11 6.442-0.655 9.704s1.536 6.358 3.243 9.174c1.707 2.817 3.954 5.27 6.628 7.195 5.489 3.988 12.345 5.745 19.114 4.88s12.941-4.267 17.237-9.509c48.411-60.317 115.849-103.205 191.764-121.971 91.303-23.424 188.294-10.959 270.382 34.689 40.19 22.058 75.488 51.728 103.818 87.337s49.121 76.378 61.153 119.963c12.059 43.278 15.018 88.508 8.732 132.93-6.285 44.422-21.758 87.142-45.423 125.569-48.155 78.693-126.146 135.44-216.908 157.888-91.303 23.396-188.352 10.959-270.439-34.689-71.392-39.095-126.374-101.671-155.243-176.683-0.598-1.729-0.655-3.57-0.171-5.327s1.536-3.291 2.986-4.433c1.451-1.144 3.186-1.841 5.035-1.98s3.698 0.279 5.291 1.199l88.686 49.023c6.057 3.43 13.226 4.378 19.995 2.677s12.601-5.912 16.241-11.767c1.649-2.817 2.701-5.912 3.128-9.118s0.171-6.469-0.711-9.592c-0.881-3.123-2.417-6.024-4.465-8.561s-4.608-4.629-7.481-6.19l-162.126-90.070c-5.944-3.151-12.913-3.96-19.456-2.231-3.27 0.808-6.342 2.231-9.045 4.211-2.701 1.98-4.977 4.433-6.712 7.279l-94.317 154.765c-1.735 2.789-2.844 5.912-3.328 9.118s-0.284 6.525 0.569 9.676c0.853 3.151 2.36 6.106 4.38 8.673 2.048 2.565 4.608 4.712 7.509 6.303l0.028 0.028z" /> <glyph unicode="&#xe97f;" glyph-name="bolt" data-tags="bolt" d="M448.516 795.501c7.573 12.793 21.336 20.64 36.204 20.64h298.561c35.466 0 55.019-41.191 32.595-68.671l-202.206-247.769h86.171c36.547 0 55.711-43.393 31.098-70.408l-422.23-463.373c-32.024-35.144-88.63 1.491-69.683 45.102l134.125 308.712h-132.447c-32.589 0-52.803 35.457-36.205 63.5l244.021 412.265zM494.999 756.041l-222.675-376.205h128.316c30.293 0 50.656-31.050 38.585-58.835l-101.814-234.343 321.606 352.941h-83.335c-35.467 0-55.020 41.191-32.595 68.67l202.205 247.77h-250.294z" /> -<glyph unicode="&#xe980;" glyph-name="puzzle" data-tags="puzzle" d="M605.009-1.28h-20.535c-8.31 0-143.271 0-402.207 0-35.465 0-69.476 14.028-94.553 38.998s-39.166 58.837-39.166 94.151l1.624 580.15c0.325 6.707 2.118 13.265 5.252 19.211s7.535 11.14 12.895 15.218c32.952 27.581 103.060 30.244 214.428 7.609 33.524-6.848 56.448-24.157 62.944-47.554 4.297-15.312 3.247-39.374-26.459-66.574-11.41-10.296-20.057-23.266-25.156-37.734s-6.488-29.973-4.043-45.113c2.446-15.137 8.65-29.427 18.050-41.572s21.696-21.759 35.774-27.971c14.078-6.211 29.491-8.824 44.841-7.599s30.148 6.246 43.053 14.609c12.905 8.365 23.509 19.806 30.847 33.284 7.339 13.481 11.178 28.571 11.174 43.905 0 25.203-11.748 49.17-35.913 73.042-0.956 0.951-4.298 3.424-6.782 5.23-14.136 10.367-38.205 27.772-31.902 54.021 5.349 24.442 32.665 39.089 93.699 50.977 137.538 25.394 198.858-15.501 218.82-34.334s12.322-48.504 1.911-90.066-24.26-97.294-14.518-152.171c5.348-30.339 11.94-40.801 14.518-43.938 4.986 3.181 9.545 6.985 13.562 11.317l0.573 0.57c32.57 32.812 69.057 49.456 107.166 49.456 23.346 0.039 46.332-5.722 66.875-16.763s37.998-27.010 50.776-46.465c12.781-19.453 20.483-41.775 22.413-64.941s-1.978-46.446-11.366-67.73-23.962-39.897-42.401-54.156c-18.437-14.257-40.157-23.71-63.19-27.499s-46.652-1.797-68.715 5.8c-22.065 7.595-41.877 20.552-57.641 37.697-11.079 11.887-17.862 13.315-19.104 12.934s-9.551-8.464-13.658-26.534c-18.243-88.449-2.674-138 10.697-167.672 11.175-24.919 5.731-42.037-0.859-52.119-17.766-26.535-59.696-29.674-103.727-29.674zM98.024 709.261l-1.528-577.392c0-22.702 9.057-44.474 25.177-60.526s37.986-25.070 60.785-25.070c262.375 0 394.088 0 402.683 0 23.878 0 72.686-0.761 83.002 7.704-0.449 2.358-1.254 4.635-2.387 6.753-25.598 57.063-29.609 119.453-13.85 197.060 6.877 33.382 24.165 56.208 47.756 62.676 15.473 4.279 39.638 3.232 66.859-26.344 10.369-11.351 23.418-19.945 37.967-25s30.134-6.417 45.343-3.956 29.561 8.664 41.753 18.046c12.193 9.383 21.839 21.648 28.065 35.684s8.835 29.397 7.588 44.689c-1.247 15.294-6.308 30.035-14.724 42.887s-19.924 23.408-33.475 30.712c-13.552 7.303-28.72 11.12-44.129 11.106-25.12 0-48.999-11.603-72.876-35.378l-0.574-0.57c-9.55-9.511-33.334-33.288-61.893-24.252-31.328 9.51-42.12 51.358-47.755 81.792-11.462 65.052 3.915 126.967 15.186 172.141 4.535 15.155 7.668 30.69 9.359 46.412-13.181 10.652-61.702 40.611-173.93 19.022-17.411-2.55-34.485-7.016-50.908-13.315 2.77-2.283 5.731-4.47 7.737-5.897 4.231-2.883 8.252-6.061 12.035-9.511 33.239-33.096 50.241-68.952 50.241-106.994-0.022-23.22-5.855-46.068-16.973-66.48s-27.169-37.741-46.703-50.423c-19.533-12.682-41.933-20.317-65.174-22.213s-46.59 2.004-67.937 11.351c-21.347 9.346-40.017 23.842-54.323 42.179s-23.798 39.933-27.616 62.839c-3.818 22.905-1.842 46.399 5.75 68.353 7.59 21.956 20.557 41.681 37.726 57.392v0c11.94 10.936 13.372 17.689 12.99 19.021s-8.501 9.511-26.648 13.505c-141.36 28.723-170.587 6.182-174.599 1.998z" /> +<glyph unicode="&#xe980;" glyph-name="puzzle" data-tags="puzzle" d="M101.12-64.154c-44.16 0-74.88 7.68-90.24 23.68l-8.96 8.96-1.92 669.438c0 34.56 14.080 67.84 38.4 92.159 24.96 24.32 58.24 37.76 92.8 37.76h465.281c79.36 1.28 106.24-5.12 119.040-25.6l9.6-16-8.96-21.76c-24.96-55.039-28.8-117.759-11.52-202.879 5.12-23.040 10.88-27.52 10.88-27.52 10.24-2.56 42.24 19.2 58.24 30.080 29.44 19.2 60.16 39.68 90.24 39.68 88.32 0 160-71.68 160-160-1.92-90.24-74.24-160.001-160-160.001-34.56 0-67.2 19.841-96 37.121-14.72 8.96-42.88 24.96-49.92 23.68 0 0-7.68-5.761-13.44-36.481-12.16-67.2 5.12-133.116 17.28-181.757 0.64-1.92 1.92-5.12 3.2-8.32 5.12-12.16 10.88-27.52 8.32-55.040 0-5.76-2.56-18.56-16.64-30.080-30.72-24.96-110.72-40.96-222.080-20.48-48.64 8.32-76.8 14.080-85.76 39.68-8.32 22.4 6.4 39.040 15.36 49.92 10.88 12.8 23.040 27.52 23.040 53.76 0 53.117-42.88 95.997-96 95.997s-96-42.88-96-95.997c0-19.84 11.52-35.84 21.76-49.92 11.52-16 25.6-35.2 16.64-58.88-9.6-24.96-39.68-31.36-51.84-34.56-56.32-11.52-103.68-17.28-141.44-17.28l0.64 0.64zM65.92 2.406c18.56-3.84 62.080-7.040 156.16 11.52-13.44 18.56-30.080 45.44-30.080 81.92 0 88.316 71.68 159.996 160 159.996s160-71.68 160-159.996c0-34.56-12.16-59.52-24.32-76.8 5.12-1.28 11.52-1.92 18.56-3.2 87.68-16.64 144-5.76 163.84 3.2 0 6.4-1.92 10.24-4.48 16-1.92 4.48-3.2 8.96-5.12 14.080-14.080 55.68-33.28 129.917-18.56 211.198 5.12 28.8 16.64 68.48 50.56 83.2 36.48 16.64 73.6-5.76 108.8-27.52 21.76-12.8 46.080-28.16 62.72-28.16 51.84 0 94.72 42.24 96 93.44 0 55.68-42.88 98.56-96 98.56-10.88 0-36.48-16.64-55.040-28.8-36.48-24.32-78.080-51.84-117.76-35.84-24.96 10.24-40.96 33.92-49.28 73.6-17.28 83.84-16 152.96 5.12 214.401-10.24 0-24.32 1.279-44.16 0h-471.68c-17.92 0-35.2-7.040-48-19.2s-19.2-28.8-19.2-46.080l1.92-635.518zM670.080 8.806c0 0 0-2.56 0-3.2 0 1.28 0 2.56 0 3.2z" /> <glyph unicode="&#xe981;" glyph-name="error" data-tags="error" d="M272.7 631.363c4.077 1.62 8.41 2.425 12.767 2.391 4.445-0.033 8.775-0.946 12.765-2.608 3.973-1.657 7.666-4.091 10.795-7.235l183.667-179.385c7.284-7.114 18.916-7.107 26.19 0.014l183.395 179.484c6.342 6.003 14.763 9.246 23.393 9.181 8.691-0.065 17.046-3.491 23.258-9.565 6.224-6.084 9.892-14.444 9.961-23.319 0.068-8.842-3.424-17.233-9.534-23.436l-0.118-0.119-182.091-178.318c-7.501-7.346-7.5-19.424 0.005-26.769l184.318-180.38c3.071-2.938 5.7-6.547 7.526-10.735 1.753-4.017 2.721-8.416 2.756-12.912 0.035-4.507-0.87-8.901-2.543-12.921l-0.019-0.050c-1.708-4.071-4.237-7.795-7.398-10.885-3.13-3.059-6.877-5.514-11.019-7.158-4.103-1.636-8.452-2.425-12.767-2.391-4.445 0.033-8.777 0.946-12.765 2.608-3.977 1.658-7.67 4.094-10.803 7.242l-185.534 181.554c-7.278 7.122-18.911 7.125-26.193 0.009l-185.89-181.662c-6.305-6.186-14.793-9.593-23.543-9.593-8.79 0-17.228 3.423-23.509 9.531-6.354 6.178-10.012 14.682-10.012 23.631 0 4.337 0.849 8.726 2.63 12.893 1.701 3.977 4.167 7.625 7.333 10.72l184.715 180.487c7.521 7.347 7.521 19.446 0 26.795l-182.5 178.323c-3.035 2.924-5.632 6.503-7.441 10.655-1.738 3.986-2.71 8.342-2.756 12.841-0.060 4.554 0.853 9.005 2.547 13.044 1.702 4.060 4.212 7.77 7.398 10.885 3.13 3.059 6.877 5.514 11.019 7.158zM1024 384c0 282.769-229.231 512-512 512s-512-229.231-512-512c0-282.769 229.231-512 512-512s512 229.231 512 512zM961.561 384c0-248.287-201.275-449.561-449.561-449.561s-449.561 201.275-449.561 449.561c0 248.287 201.275 449.561 449.561 449.561s449.561-201.275 449.561-449.561z" /> <glyph unicode="&#xe982;" glyph-name="folder-closed-f" data-tags="bw-folder-closed-f" d="M589.227 683.038h340.031c25.254-0.32 49.35-10.594 67.084-28.583 17.733-17.987 27.687-42.248 27.659-67.503v-554.809c0.065-12.768-2.431-25.447-7.331-37.254-4.903-11.811-12.134-22.507-21.222-31.495-8.703-8.703-19.078-15.591-30.439-20.295s-23.558-7.107-35.878-7.107l-834.264 0.639c-25.285 0.191-49.485 10.369-67.279 28.39s-27.717 42.311-27.589 67.629v703.321c-0.128 25.285 9.762 49.547 27.525 67.567s41.927 28.233 67.212 28.457h364.868c25.285-0.192 49.485-10.37 67.279-28.39s27.717-42.317 27.589-67.63v-16.515c-0.191-9.411 3.361-18.532 9.858-25.382s15.46-10.818 24.901-11.042zM459.727 772.401l-364.868 0.037c-9.409-0.224-18.339-4.195-24.837-11.041s-10.019-15.972-9.795-25.382v-100.182c-0.051-0.737-0.079-1.479-0.079-2.23v-601.598c0-17.671 14.331-31.999 32-31.999s31.999 14.331 31.999 31.999v494.078c0 17.677 14.331 31.999 31.999 31.999h255.999c0.598 0 1.193-0.017 1.787-0.051h527.030c3.008-0.037 6.023 0.576 8.806 1.697s5.343 2.817 7.488 4.929c2.144 2.112 3.873 4.672 5.024 7.429s1.792 5.763 1.792 8.773v6.295c0.224 9.41-3.299 18.503-9.823 25.321s-15.465 10.755-24.873 10.979h-340.031c-25.285 0.289-49.485 10.498-67.244 28.486s-27.749 42.249-27.749 67.535v16.515c0.224 9.445-3.299 18.564-9.795 25.381s-15.426 10.789-24.837 11.042z" /> <glyph unicode="&#xe983;" glyph-name="providers" data-tags="bw-providers" d="M795.946 895.999c-68.137 0-123.373-55.236-123.373-123.373 0-26.887 8.6-51.764 23.2-72.032 5.421-7.527 5.928-17.864 0.072-25.057l-78.409-96.295c-5.865-7.204-16.129-8.871-24.347-4.54-33.264 17.525-71.159 27.444-111.37 27.444-65.26 0-124.418-26.127-167.584-68.491-6.851-6.723-17.598-7.916-25.311-2.203l-43.869 32.484c-7.393 5.474-9.491 15.502-6.223 24.099 5.178 13.62 8.013 28.396 8.013 43.833 0 68.137-55.236 123.373-123.373 123.373s-123.373-55.237-123.373-123.373c0-68.138 55.236-123.373 123.373-123.373 25.493 0 49.178 7.732 68.842 20.977 7.179 4.836 16.702 5.327 23.659 0.175l46.671-34.559c7.637-5.655 9.676-16.143 5.398-24.629-16.308-32.342-25.491-68.889-25.491-107.582 0-60.534 22.479-115.818 59.545-157.959 6.345-7.212 6.945-18.022 0.806-25.412l-52.717-63.466c-5.597-6.739-15.107-8.483-23.293-5.359-13.659 5.211-28.482 8.066-43.972 8.066-68.137 0-123.373-55.236-123.373-123.373s55.236-123.373 123.373-123.373c68.138 0 123.373 55.236 123.373 123.373 0 25.451-7.706 49.1-20.912 68.745-5.074 7.547-5.342 17.651 0.469 24.646l51.951 62.544c6.069 7.305 16.644 8.761 24.876 4.024 35.082-20.183 75.765-31.727 119.143-31.727 67.152 0 127.843 27.664 171.299 72.217 6.851 7.025 17.879 8.346 25.73 2.459l102.412-76.81c7.559-5.669 9.482-16.073 5.8-24.775-6.25-14.768-9.706-31.005-9.706-48.049 0-68.138 55.236-123.373 123.373-123.373s123.373 55.236 123.373 123.373c0 68.137-55.237 123.373-123.373 123.373-23.816 0-46.054-6.749-64.91-18.436-7.147-4.43-16.358-4.684-23.084 0.36l-109.29 81.968c-7.431 5.573-9.506 15.76-5.509 24.144 14.846 31.149 23.156 66.013 23.156 102.818 0 63.709-24.9 121.603-65.499 164.48-6.747 7.126-7.628 18.199-1.432 25.809l78.978 96.995c5.415 6.651 14.649 8.563 22.759 5.773 12.588-4.33 26.095-6.681 40.152-6.681 68.138 0 123.373 55.237 123.373 123.373s-55.236 123.373-123.373 123.373zM709.641 772.626c0 47.665 38.639 86.305 86.305 86.305s86.306-38.64 86.306-86.305c0-47.665-38.641-86.305-86.306-86.305s-86.305 38.639-86.305 86.305zM110.305 546.545c-41.677 6.139-73.666 42.052-73.666 85.435 0 47.696 38.665 86.361 86.361 86.361 13.198 0 25.705-2.961 36.891-8.254 29.41-13.755 49.785-43.609 49.785-78.22 0-47.665-38.641-86.306-86.306-86.306-4.441 0-8.804 0.336-13.065 0.983zM481.718 540.459c83.906 0 154.219-58.19 172.787-136.417 3.378-13.734 5.169-28.091 5.169-42.867 0-98.799-80.093-178.892-178.892-178.892s-178.892 80.093-178.892 178.892c0 78.88 51.053 145.837 121.919 169.629 18.151 6.258 37.633 9.655 57.908 9.655zM814.321 48.648c0 20.601 7.219 39.515 19.264 54.353 1.489 1.178 2.854 2.565 4.047 4.156 0.415 0.551 0.798 1.117 1.154 1.691 15.678 16.103 37.592 26.103 61.842 26.103 47.665 0 86.305-38.639 86.305-86.305s-38.639-86.306-86.305-86.306c-47.665 0-86.306 38.641-86.306 86.306zM96.513-4.628c0 47.665 38.64 86.305 86.305 86.305s86.306-38.639 86.306-86.305c0-47.665-38.641-86.305-86.306-86.305s-86.305 38.639-86.305 86.305z" /> @@ -187,6 +187,17 @@ <glyph unicode="&#xe99e;" glyph-name="caret-down" data-tags="bw-caret-down" d="M489.532 139.168c19.357-19.731 27.798-16.256 46.654 3.2l410.214 392.428c26.611 26.574 19.264 40.194-19.213 40.194l-831.921 0.001c-36.2 0-46.248-13.736-19.926-40.019l414.191-395.804zM568.546 84.8c-34.086-27.731-80.554-27.731-114.64 0l-415.959 398.167c-71.587 68.42-35.957 157.033 57.32 157.033h831.921c93.274 0 132.41-95.934 57.318-157.033l-415.96-398.167z" /> <glyph unicode="&#xe99f;" glyph-name="passkey" data-tags="passkey" d="M171.866 100.531c-28.794 0-52.141 23.341-52.141 52.134s23.347 52.134 52.141 52.134c28.794 0 52.134-23.341 52.134-52.134s-23.341-52.134-52.134-52.134zM336.23 423.597c-71.622 45.381-119.174 125.34-119.174 216.403 0 141.385 114.618 256 256.003 256s256-114.615 256-256c0-91.064-47.547-171.022-119.17-216.403 61.672-22.859 115.894-60.723 158.11-109.085 11.29-12.579 21.253-23.994 28.108-33.718l114.25 0.104 113.644-109.313-160-142.784-64 64-64-64-64 64-64-61.44-191.164 0.262c-42.392-59.162-113.22-97.222-192.632-95.571-126.49 2.63-226.842 105.030-224.154 228.717 2.694 123.683 107.418 221.813 233.907 219.18 7.2-0.15 14.323-0.623 21.344-1.406 6.304 4.097 13.485 8.49 21.619 13.196 18.816 10.858 38.643 20.2 59.309 27.859zM422.251 280.453l289.060 0.263c-8.424 9.26-19.715 20.508-36.449 33.795-55.383 43.498-125.517 69.489-201.804 69.489-47.366 0-92.361-10.020-132.924-28.029 33.408-18.139 61.646-44.193 82.116-75.517zM281.056 640c0-106.038 85.965-192 192.003-192s192 85.962 192 192-85.961 192-192 192c-106.038 0-192.003-85.962-192.003-192zM383.994 95.667l198.295-0.269 90.625 86.995 63.086-63.085 64 64 66.5-66.496 63.35 56.531-45.255 43.533-496.955-0.454-18.963 29.018c-28.486 43.592-78.259 73.319-136.051 74.522-92.531 1.926-166.694-69.562-168.589-156.589-1.894-86.976 69.011-161.408 161.498-163.334 57.805-1.203 108.877 26.438 139.277 68.858l19.181 26.771z" /> <glyph unicode="&#xe9a0;" glyph-name="lock-encrypted" data-tags="bw-lock-encrypted" d="M496 368c0-14.216-6.18-26.989-16-35.778v-44.222c0-17.673-14.327-32-32-32s-32 14.327-32 32v44.222c-9.82 8.789-16 21.562-16 35.778 0 26.509 21.491 48 48 48s48-21.491 48-48zM352 0c17.673 0 32 14.33 32 32s-14.327 32-32 32c-17.673 0-32-14.33-32-32s14.327-32 32-32zM480 0c17.673 0 32 14.33 32 32s-14.327 32-32 32c-17.673 0-32-14.33-32-32s14.327-32 32-32zM640 32c0-17.67-14.327-32-32-32s-32 14.33-32 32c0 17.67 14.327 32 32 32s32-14.33 32-32zM736 0c17.67 0 32 14.33 32 32s-14.33 32-32 32c-17.67 0-32-14.33-32-32s14.33-32 32-32zM896 32c0-17.67-14.33-32-32-32s-32 14.33-32 32c0 17.67 14.33 32 32 32s32-14.33 32-32zM192 640c0 139.201 115.547 256 256 256 140.401 0 256-116.204 256-256v-99.201c73.030-14.824 128-63.393 128-140.799v-208h32c88.365 0 160-71.635 160-160s-71.635-160-160-160h-512c-36.026 0-69.272 11.904-96.015 32h-31.985c-88.365 0-160 55.635-160 144v352c0 77.407 54.968 125.975 128 140.799v99.201zM352 192h416v208c0 53.020-42.982 80-96 80h-448c-53.020 0-96-26.98-96-80v-352c0-46.323 32.81-68.986 76.46-78.010-8.025 19.072-12.46 40.019-12.46 62.010 0 88.365 71.635 160 160 160zM448 832c-106.336 0-192-89.362-192-192v-96h384v96c0 103.132-85.612 192-192 192zM352 128c-53.020 0-96-42.982-96-96s42.98-96 96-96h512c53.018 0 96 42.982 96 96s-42.982 96-96 96h-512z" /> +<glyph unicode="&#xe9a1;" glyph-name="msp" data-tags="msp" d="M800 832c-53.12 0-96-42.88-96-96 0-13.44 2.56-26.24 7.68-37.76l-140.8-120.32c-40.32 38.4-94.72 62.72-155.52 62.72-48 0-92.16-14.72-128.64-40.32l-108.8 152.32c8.32 14.080 12.8 30.72 12.8 48 0 53.12-42.88 96-96 96s-94.72-43.52-94.72-96.64 42.88-96 96-96c12.16 0 23.68 1.92 33.92 6.4l110.72-154.88c-30.72-38.4-48.64-86.4-48.64-139.52 0-59.52 23.040-113.28 60.8-153.6l-57.6-76.8c-10.88 4.48-22.4 6.4-35.2 6.4-53.12 0-96-42.88-96-96s42.88-96 96-96 96 42.88 96 96c0 17.28-4.48 33.28-12.16 47.36l59.52 79.36c33.28-19.2 71.68-30.72 113.28-30.72 31.36 0 61.44 6.4 88.32 17.92l85.76-128.64c-8.96-14.72-14.080-31.36-14.080-49.92 0-53.12 42.88-96 96-96s96 42.88 96 96-42.88 96-96 96c-11.52 0-22.4-1.92-32.64-5.76l-80.64 120.96c49.28 40.96 81.28 103.040 81.28 172.8 0 40.32-10.88 78.080-29.44 110.72l144.64 124.16c13.44-7.040 28.8-10.88 44.8-10.88 53.12 0 96 42.88 96 96s-42.88 96-96 96zM96 768c-17.92 0-32 14.080-32 32s14.080 32 32 32 32-14.080 32-32-14.080-32-32-32zM160 64c-17.92 0-32 14.080-32 32s14.080 32 32 32 32-14.080 32-32-14.080-32-32-32zM672 64c17.92 0 32-14.080 32-32s-14.080-32-32-32-32 14.080-32 32 14.080 32 32 32zM416 253.44c-88.96 0-161.28 72.32-161.28 161.28 0 78.080 55.68 142.72 129.28 158.080 10.24 1.92 21.12 3.2 32 3.2 50.56 0 94.72-23.68 124.16-59.52l3.84-4.48s0 0 0-0.64c4.48-5.76 8.32-12.8 12.16-19.2 0-0.64 0.64-1.28 1.28-2.56 12.16-22.4 19.2-48 19.2-74.88 0-88.96-72.32-161.28-161.28-161.28zM800 704c-17.92 0-32 14.080-32 32s14.080 32 32 32 32-14.080 32-32-14.080-32-32-32z" /> +<glyph unicode="&#xe9a2;" glyph-name="desktop-alt" data-tags="desktop-alt" d="M960 704h-896c-35.2 0-64-28.8-64-64v-512c0-35.2 28.8-64 64-64h320v-64h-32c-17.92 0-32-14.080-32-32s14.080-32 32-32h320c17.92 0 32 14.080 32 32s-14.080 32-32 32h-32v64h320c35.2 0 64 28.8 64 64v512c0 35.2-28.8 64-64 64zM576 0h-128v64h128v-64zM960 128h-896v512h896v-512z" /> +<glyph unicode="&#xe9a3;" glyph-name="browser-alt" data-tags="browser-alt" d="M736 448c17.92 0 32-14.080 32-32v-192c0-17.92-14.080-32-32-32s-32 14.080-32 32v114.56l-169.6-169.6c-12.8-12.8-32.64-12.8-45.44 0s-12.8 32.64 0 45.44l169.6 169.6h-114.56c-17.92 0-32 14.080-32 32s14.080 32 32 32h192zM0 704v-640c0-35.2 28.8-64 64-64h896c35.2 0 64 28.8 64 64v640c0 35.2-28.8 64-64 64h-896c-35.2 0-64-28.8-64-64zM64 704h896v-64h-896v64zM960 576v-512h-896v512h896z" /> +<glyph unicode="&#xe9a4;" glyph-name="mobile-alt" data-tags="mobile-alt" d="M448 736c0 17.92 14.080 32 32 32h64c17.92 0 32-14.080 32-32s-14.080-32-32-32h-64c-17.92 0-32 14.080-32 32zM384 896h256c106.24 0 192-85.76 192-192v-640c0-106.24-85.76-192-192-192h-256c-106.24 0-192 85.76-192 192v640c0 106.24 85.76 192 192 192zM640 832h-256c-70.4 0-128-57.6-128-128v-640c0-70.4 57.6-128 128-128h256c70.4 0 128 57.6 128 128v640c0 70.4-57.6 128-128 128z" /> +<glyph unicode="&#xe9a5;" glyph-name="x-twitter" data-tags="x-twitter" d="M608.342 462.848l380.499 433.152h-90.154l-330.538-376.022-263.809 376.022h-304.341l399.018-568.662-399.018-454.186h90.155l348.844 397.184 278.656-397.184h304.339l-413.651 589.696zM484.822 322.346l-362.156 507.094h138.496l637.484-892.97h-138.496l-275.328 385.875z" /> +<glyph unicode="&#xe9a6;" glyph-name="wand" data-tags="bw-wand" d="M779.633 465.359c4.932-1.024 9.081-3.703 11.981-7.328 0.213-0.225 0.508-0.532 0.893-0.926 1.43-1.47 3.617-3.678 6.447-6.513 5.64-5.636 13.57-13.496 22.467-22.274 17.781-17.543 39.285-38.642 53.662-52.716 8.135-7.963 8.204-20.943 0.164-28.996-8.045-8.049-21.16-8.122-29.295-0.16-14.397 14.098-35.942 35.234-53.776 52.83-8.913 8.794-16.925 16.732-22.667 22.475-2.855 2.859-5.21 5.235-6.849 6.918-0.791 0.811-1.548 1.602-2.138 2.245-0.225 0.246-0.758 0.831-1.303 1.507-0.119 0.147-1.147 1.413-2.073 3.113-0.291 0.537-0.827 1.569-1.315 2.937-0.352 0.995-1.442 4.182-1.114 8.393 0.414 5.308 3.379 12.935 11.641 16.888 6.214 2.974 11.772 1.917 13.275 1.606zM468.406 776.495c4.932-1.024 9.081-3.704 11.981-7.329 0.217-0.227 0.508-0.532 0.893-0.928 1.43-1.468 3.621-3.678 6.451-6.509 5.636-5.639 13.57-13.497 22.467-22.275 17.777-17.544 39.281-38.641 53.658-52.717 8.135-7.962 8.208-20.943 0.164-28.994s-21.16-8.122-29.29-0.16c-14.402 14.096-35.946 35.235-53.78 52.831-8.909 8.793-16.925 16.732-22.663 22.473-2.859 2.859-5.214 5.235-6.853 6.917-0.786 0.813-1.544 1.602-2.134 2.246-0.225 0.245-0.762 0.83-1.303 1.508-0.123 0.148-1.151 1.411-2.077 3.112-0.291 0.536-0.823 1.568-1.311 2.939-0.356 0.993-1.446 4.18-1.118 8.39 0.414 5.309 3.379 12.938 11.645 16.89 6.21 2.971 11.772 1.918 13.271 1.606zM832.893 575.739c4.219 2.728 9.069 3.738 13.705 3.204 0.315-0.009 0.741-0.019 1.298-0.030 2.056-0.039 5.186-0.069 9.208-0.090 8.016-0.042 19.239-0.046 31.801-0.027 25.108 0.039 55.386 0.171 75.608 0.281 11.44 0.062 20.767-9.070 20.828-20.398s-9.159-20.562-20.599-20.624c-20.255-0.11-50.59-0.243-75.772-0.282-12.583-0.019-23.921-0.015-32.084 0.027-4.063 0.021-7.426 0.052-9.785 0.097-1.139 0.022-2.236 0.050-3.113 0.092-0.336 0.016-1.13 0.055-1.999 0.155-0.193 0.020-1.823 0.194-3.69 0.748-0.59 0.175-1.704 0.531-3.027 1.16-0.963 0.455-4.010 1.947-6.783 5.154-3.502 4.045-6.853 11.519-3.834 20.101 2.269 6.452 6.951 9.599 8.237 10.43zM651.411 724.628c-2.753 4.174-3.772 8.974-3.232 13.567 0.008 0.311 0.020 0.733 0.029 1.283 0.041 2.037 0.070 5.133 0.090 9.116 0.045 7.933 0.049 19.043 0.029 31.477-0.037 24.851-0.172 54.82-0.283 74.838-0.066 11.323 9.155 20.553 20.586 20.616 11.436 0.062 20.754-9.066 20.82-20.39 0.111-20.047 0.246-50.074 0.283-75 0.020-12.455 0.016-23.679-0.025-31.756-0.025-4.021-0.053-7.351-0.098-9.685-0.025-1.128-0.049-2.216-0.094-3.084-0.016-0.33-0.057-1.118-0.156-1.978-0.020-0.189-0.197-1.802-0.758-3.653-0.176-0.583-0.532-1.687-1.167-2.997-0.463-0.949-1.966-3.966-5.206-6.713-4.080-3.465-11.624-6.783-20.287-3.794-6.513 2.247-9.691 6.883-10.531 8.155zM788.472 705.217c1.032 4.88 3.74 8.987 7.401 11.857 0.229 0.214 0.541 0.505 0.938 0.886 1.487 1.413 3.719 3.58 6.578 6.382 5.698 5.58 13.636 13.432 22.503 22.237 17.727 17.599 39.039 38.883 53.26 53.115 8.045 8.050 21.16 8.122 29.29 0.16 8.135-7.962 8.208-20.943 0.164-28.994-14.242-14.253-35.598-35.578-53.375-53.23-8.884-8.82-16.904-16.753-22.704-22.435-2.888-2.829-5.288-5.161-6.988-6.78-0.823-0.782-1.618-1.532-2.269-2.116-0.25-0.222-0.84-0.752-1.524-1.289-0.152-0.119-1.425-1.137-3.146-2.054-0.541-0.289-1.581-0.818-2.97-1.3-1.004-0.349-4.223-1.428-8.475-1.103-5.362 0.409-13.070 3.345-17.064 11.524-3.002 6.149-1.937 11.651-1.622 13.138zM622.232 567.998l61.825-60.907 44.069 43.649c17.195 17.032 17.109 44.833-0.197 61.756-16.929 16.556-44.020 16.483-60.858-0.165l-44.839-44.332zM592.949 539.048l-516.199-510.355c-17.258-17.064-17.312-44.9-0.122-62.030 17.042-16.982 44.612-17.027 61.706-0.094l516.469 511.545-61.854 60.934zM33.221-76.825c-41.339 41.193-41.208 108.138 0.293 149.168l590.32 583.641c40.698 40.237 106.177 40.416 147.096 0.4 41.82-40.902 42.037-108.094 0.471-149.26l-589.789-584.17c-41.106-40.714-107.407-40.616-148.39 0.221z" /> +<glyph unicode="&#xe9a7;" glyph-name="user-monitor" data-tags="user-monitor" d="M410.58 192.454c1.762-0.301 3.572-0.454 5.42-0.454h320c17.67 0 32 14.33 32 32s-14.33 32-32 32h-32v64h288c17.67 0 32 14.327 32 32v448c0 17.673-14.33 32-32 32h-832c-17.673 0-32-14.327-32-32v-272.889c-39.28-35.156-64-86.247-64-143.111 0-65.42 32.719-123.198 82.685-157.862-76.489-44.173-132.617-133.312-146.192-239.942-2.771-21.766 6.101-50.195 27.559-50.195h445.465c27.46 0 41.891 18.042 37.797 50.195-10.712 84.147-47.926 157.402-100.734 206.259zM192 565.075v202.925h768v-384h-512c0 106.038-85.962 192-192 192-22.441 0-43.982-3.85-64-10.925zM437.075 320h10.925v-64h-32c-17.673 0-32-14.33-32-32 0-4.045 0.751-7.917 2.121-11.482-6.774 4.896-13.745 9.427-20.893 13.555 32.869 22.778 58.284 55.553 71.847 93.926zM640 256h-128v64h128v-64zM384 384c0-70.692-57.308-128-128-128s-128 57.308-128 128c0 70.692 57.308 128 128 128s128-57.308 128-128zM255.903 192c81.621 0 165.1-72.646 188.404-192h-376.807c23.304 119.354 106.783 192 188.403 192z" /> +<glyph unicode="&#xe9a8;" glyph-name="back" data-tags="back" d="M778.15 863.496c29.158-29.161 29.158-76.441 0-105.602l-373.897-373.896 373.897-373.899c29.158-29.158 29.158-76.435 0-105.6-29.165-29.158-76.442-29.158-105.6 0l-426.7 426.698c-29.161 29.162-29.161 76.441 0 105.603l426.7 426.697c29.158 29.161 76.435 29.161 105.6 0z" /> +<glyph unicode="&#xe9a9;" glyph-name="plus-f" data-tags="plus-f" d="M587.881 465.356h342.34c20.685 0 40.147-8.534 54.24-23.143 14.144-14.632 21.638-34.078 21.638-53.811 0-19.936-7.654-39.215-21.504-53.673-14.157-14.778-33.741-23.281-54.374-23.281h-342.34v-361.752c0-19.936-7.657-39.213-21.505-53.67-14.156-14.778-33.737-23.283-54.376-23.283-20.822 0-40.22 8.659-54.198 23.098-14.171 14.637-21.683 34.106-21.683 53.856v361.752h-342.337c-10.25 0-20.309 2.1-29.607 6.090-9.422 4.043-17.73 9.905-24.592 17.007-7.010 7.226-12.514 15.777-16.212 25.146-3.579 9.069-5.47 18.805-5.47 28.71 0 9.888 1.875 19.675 5.52 28.836 3.697 9.292 9.15 17.716 15.99 24.842 6.957 7.276 15.388 13.196 24.896 17.242 9.199 3.915 19.206 6.035 29.476 6.035h342.337v352.948c0 19.807 7.543 39.15 21.549 53.719 14.152 14.75 33.715 23.235 54.332 23.235 20.822 0 40.22-8.658 54.198-23.097 14.17-14.637 21.683-34.104 21.683-53.857v-352.948z" /> +<glyph unicode="&#xe9aa;" glyph-name="popout" data-tags="popout" d="M194.012 810.783c0 37.999 30.804 68.803 68.803 68.803h692.385c37.997 0 68.8-30.804 68.8-68.803v-664.069c0-37.997-30.803-68.806-68.8-68.806h-125.21v-120.691c0-37.997-30.81-68.8-68.806-68.8h-692.381c-37.999 0-68.803 30.803-68.803 68.8v664.072c0 37.999 30.804 68.803 68.803 68.803h125.209v120.692zM194.012 615.032h-96.554c-12.371 0-22.4-10.029-22.4-22.4v-606.757c0-12.371 10.029-22.4 22.4-22.4h635.073c12.371 0 22.4 10.029 22.4 22.4v92.032h-309.494c-20.726 0-37.529 16.806-37.529 37.53 0 20.73 16.803 37.53 37.529 37.53h481.103c12.371 0 22.4 10.029 22.4 22.4v606.762c0 12.371-10.029 22.4-22.4 22.4h-635.071c-12.371 0-22.4-10.029-22.4-22.4v-455.941c0-20.727-16.802-37.529-37.529-37.529s-37.529 16.802-37.529 37.529v288.845zM771.718 684.607c20.73-0.077 37.466-16.941 37.389-37.668l-1.254-201.736c-0.077-20.726-16.941-37.466-37.67-37.389-20.723 0.077-37.466 16.942-37.389 37.668l0.922 111.134-462.46-459.036c-14.71-14.598-38.472-14.509-53.074 0.198-14.601 14.714-14.513 38.477 0.198 53.075l462.459 459.032-112.174 0.914c-20.726 0.077-37.466 16.941-37.389 37.668s16.941 37.466 37.668 37.389l202.775-1.251z" /> +<glyph unicode="&#xe9ab;" glyph-name="vault-f" data-tags="vault-f" d="M117.679 809.772c-35.883 0-64.973-29.089-64.973-64.973v-660.56c0-30.63 21.195-56.308 49.719-63.169 5.479-1.319 9.839-5.915 9.839-11.553v-9.74c0-32.894 26.666-59.561 59.559-59.561h64.973c32.893 0 59.558 26.666 59.558 59.561v8.662c0 5.981 4.848 10.83 10.829 10.83h454.811c5.981 0 10.83-4.849 10.83-10.83v-8.662c0-32.894 26.66-59.561 59.555-59.561h64.976c32.894 0 59.555 26.666 59.555 59.561v9.74c0 5.638 4.361 10.234 9.842 11.553 28.521 6.861 49.718 32.539 49.718 63.169v660.56c0 35.883-29.088 64.973-64.976 64.973h-833.816zM924.425 8.439c0 5.981-4.849 10.83-10.83 10.83h-97.455c-5.981 0-10.83-4.849-10.83-10.83v-8.662c0-14.95 12.119-27.070 27.070-27.070h64.976c14.95 0 27.070 12.119 27.070 27.070v8.662zM144.751-0.223v8.662c0 5.981 4.848 10.83 10.829 10.83h97.459c5.981 0 10.829-4.849 10.829-10.83v-8.662c0-14.95-12.121-27.070-27.072-27.070h-64.973c-14.952 0-27.072 12.119-27.072 27.070zM150.157 668.996c0 23.922 19.393 43.315 43.315 43.315h649.732c20.185 0 37.141-13.807 41.948-32.492 29.533-0.386 53.344-22.053 53.344-48.724v-97.46c0-26.26-23.082-47.668-51.977-48.692v-140.851c28.895-1.023 51.977-22.432 51.977-48.692v-86.632c0-26.257-23.082-47.664-51.977-48.688 0-23.925-19.396-43.357-43.315-43.357h-649.732c-23.922 0-43.315 19.396-43.315 43.315v508.957zM193.472 679.825h649.732c5.975 0 10.824-4.848 10.824-10.828v-508.957c0-5.975-4.849-10.824-10.824-10.824h-649.732c-5.981 0-10.829 4.849-10.829 10.824v508.957c0 5.981 4.848 10.828 10.829 10.828zM886.519 308.516c0 1.609 1.403 2.879 2.958 2.465 7.469-1.988 12.92-8.211 12.92-15.579v-86.632c0-7.367-5.451-13.589-12.92-15.577-1.554-0.416-2.958 0.855-2.958 2.464v112.859zM889.477 518.057c7.469 1.988 12.92 8.211 12.92 15.579v97.46c0 7.368-5.451 13.59-12.92 15.578-1.554 0.414-2.958-0.855-2.958-2.465v-123.686c0-1.61 1.403-2.879 2.958-2.465zM458.774 614.852c0 8.971-7.272 16.243-16.243 16.243s-16.244-7.272-16.244-16.243l0.001-56.421c0-5.462-4.081-10.020-9.436-11.094-28.827-5.784-54.206-21.106-72.703-42.536-3.595-4.165-9.629-5.459-14.394-2.708l-49.386 28.514c-7.769 4.485-17.703 1.823-22.189-5.946s-1.823-17.703 5.946-22.188l49.871-28.794c4.686-2.706 6.607-8.441 4.936-13.588-4.105-12.646-6.324-26.143-6.324-40.157 0-14.984 2.536-29.376 7.202-42.769 1.812-5.2-0.083-11.079-4.851-13.832l-50.835-29.35c-7.769-4.485-10.431-14.42-5.946-22.188s14.42-10.432 22.189-5.946l51.378 29.663c4.683 2.704 10.607 1.503 14.227-2.515 18.325-20.346 42.989-34.871 70.879-40.47 5.355-1.072 9.435-5.632 9.435-11.089l0.001-59.832c0-8.975 7.272-16.245 16.243-16.245s16.243 7.27 16.243 16.245v59.82c0 5.463 4.083 10.023 9.439 11.095 27.9 5.588 52.573 20.111 70.907 40.458 3.62 4.016 9.543 5.216 14.225 2.513l51.345-29.643c7.77-4.486 17.703-1.824 22.185 5.945 4.488 7.769 1.825 17.703-5.945 22.189l-50.789 29.324c-4.769 2.753-6.664 8.634-4.851 13.834 4.671 13.4 7.211 27.8 7.211 42.793 0 14.023-2.221 27.528-6.331 40.181-1.672 5.148 0.248 10.885 4.935 13.591l49.825 28.768c7.77 4.486 10.433 14.42 5.945 22.189-4.482 7.769-14.414 10.431-22.185 5.945l-49.353-28.493c-4.764-2.75-10.797-1.457-14.393 2.706-18.505 21.43-43.894 36.751-72.731 42.526-5.357 1.073-9.44 5.631-9.44 11.095v56.411zM442.554 322.474c53.826 0 97.46 43.634 97.46 97.46s-43.634 97.459-97.46 97.459c-53.825 0-97.459-43.634-97.459-97.459s43.634-97.46 97.459-97.46z" /> <glyph unicode="&#xe9ee;" glyph-name="rocket" data-tags="rocket" d="M650.515 648.267c33.538 33.532 87.904 33.532 121.443 0 33.538-33.538 33.538-87.904 0-121.443s-87.904-33.538-121.443 0c-33.532 33.538-33.532 87.904 0 121.443zM750.801 627.113c-21.855 21.856-57.284 21.856-79.134 0-21.856-21.855-21.856-57.284 0-79.134 21.855-21.855 57.284-21.855 79.134 0s21.855 57.284 0 79.134zM493.141 680.645c113.184 90.608 223.416 148.836 310.181 180.552 43.273 15.818 81.527 25.336 111.933 28.691 15.138 1.668 29.339 1.929 41.709 0.19 11.507-1.615 25.903-5.552 36.583-16.232l3.981-3.981c10.68-10.679 14.617-25.076 16.232-36.582 1.739-12.377 1.478-26.572-0.19-41.71-3.356-30.406-12.874-68.659-28.691-111.932-31.716-86.771-89.944-196.998-180.552-310.187-29.487-36.837-59.902-73.855-91.847-111.076l30.789-145.412c4.388-20.715-0.21-42.314-12.654-59.447l-89.489-123.202c-35.299-48.598-110.586-37.857-130.894 18.67l-31.576 87.886-74.269-74.269c-5.844-5.844-15.313-5.844-21.151 0-5.844 5.845-5.844 15.313 0 21.152l80.747 80.747-47.795 47.794-162.229-162.229c-5.844-5.844-15.313-5.844-21.151 0s-5.844 15.313 0 21.151l162.229 162.229-54.059 54.060-306.392-306.392c-5.844-5.844-15.313-5.844-21.151 0-5.844 5.845-5.844 15.313 0 21.151l306.391 306.392-54.059 54.060-61.939-61.939c-5.845-5.844-15.313-5.844-21.151 0-5.845 5.844-5.845 15.313 0 21.151l61.938 61.939-47.794 47.794-112.086-112.086c-5.844-5.844-15.312-5.844-21.151 0-5.844 5.845-5.844 15.313 0 21.152l109.675 109.669-100.568 36.13c-56.526 20.307-67.261 95.6-18.663 130.894l123.202 89.489c17.132 12.444 38.73 17.041 59.446 12.654l145.412-30.789c37.221 31.945 74.238 62.36 111.076 91.847zM952.797 829.985c-0.744 0.223-2.062 0.551-4.14 0.84-5.79 0.812-14.652 0.934-26.837-0.411-24.234-2.675-57.643-10.683-97.952-25.414-80.389-29.379-184.973-84.318-293.329-171.062-38.013-30.432-76.092-61.749-114.295-94.665-2.919-4.249-6.879-7.644-11.433-9.895-54.88-47.714-110.027-98.85-165.607-155.478l258.391-258.39c56.628 55.58 107.765 110.728 155.48 165.609 2.25 4.551 5.643 8.51 9.89 11.427 32.912 38.205 64.235 76.284 94.667 114.298 86.744 108.362 141.683 212.941 171.063 293.329 14.73 40.309 22.744 73.723 25.414 97.952 1.345 12.179 1.222 21.047 0.411 26.837-0.29 2.078-0.619 3.397-0.84 4.14l-0.875 0.875zM224.258 561.056c-4.141 0.875-8.464-0.045-11.889-2.533l-123.202-89.489c-9.719-7.058-7.573-22.117 3.736-26.177l96.234-34.573c45.935 47.161 91.519 90.548 136.803 131.242l-101.677 21.529zM663.214 202.25c-40.694-45.278-84.081-90.868-131.243-136.803l34.574-96.235c4.061-11.304 19.119-13.455 26.177-3.736l89.489 123.202c2.488 3.425 3.408 7.748 2.533 11.889l-21.529 101.678z" /> <glyph unicode="&#xe9ef;" glyph-name="ellipsis-h" data-tags="ellipsis-h" d="M876.633 299.353c-46.75 0-84.647 37.897-84.647 84.647s37.897 84.647 84.647 84.647c46.75 0 84.647-37.897 84.647-84.647s-37.897-84.647-84.647-84.647zM512 299.353c-46.75 0-84.647 37.897-84.647 84.647s37.897 84.647 84.647 84.647c46.75 0 84.647-37.897 84.647-84.647s-37.897-84.647-84.647-84.647zM147.367 299.353c-46.75 0-84.647 37.897-84.647 84.647s37.897 84.647 84.647 84.647c46.75 0 84.647-37.897 84.647-84.647s-37.897-84.647-84.647-84.647z" /> <glyph unicode="&#xe9f0;" glyph-name="ellipsis-v" data-tags="ellipsis-v" d="M427.353 19.367c0 46.75 37.897 84.647 84.647 84.647s84.647-37.897 84.647-84.647c0-46.75-37.897-84.647-84.647-84.647s-84.647 37.897-84.647 84.647zM427.353 384c0 46.75 37.897 84.647 84.647 84.647s84.647-37.897 84.647-84.647c0-46.75-37.897-84.647-84.647-84.647s-84.647 37.897-84.647 84.647zM427.353 748.633c0 46.75 37.897 84.647 84.647 84.647s84.647-37.897 84.647-84.647c0-46.75-37.897-84.647-84.647-84.647s-84.647 37.897-84.647 84.647z" /> diff --git a/libs/angular/src/scss/bwicons/fonts/bwi-font.ttf b/libs/angular/src/scss/bwicons/fonts/bwi-font.ttf index f9b63283e0465ea0285ac022a2aa1540746d3d60..f70eea7af77f4f057b184abb7f6f85337734d3d6 100644 GIT binary patch delta 7209 zcmb_B3wTpin&-dI<R&-o+%##DHcgwRKuenR4U|$GtbnzG78Xlu3u37}S{@Y;Nx{cp z<)H+r>jPJ2be!V2qg}1zGKx6927KVEIOw_#pQ|(Y-1UV_lkR`+O<IO;cR%)#@AjVa zpYy-YfBsj}V{dVfe8$BI5JC*3kFX?i+5E{-^VPx@Tsee)S1(!F-hFz~XF5W_i*U=e z?Q6Snv>?0zN8Pn6HZGfg$TADzJ_5y4JFjV9n*U?=&j^%I`if3m;12N~gug{NzjNif z4KK|fd4|B`EJB!LD^@R=(Vle7eV0J#$H=5}W&4J1@)Bzy5SxnxxvSb&UX$NcmO^+1 z5_r2;uU&_VB5L_|x=&b+z4P$xa4<0!vf)Ug6TG#fJ%q45gdwD|nx2gw@PgKm?`Y4^ z5!eYVP7?Fj7wI_U;l5+kiA~%M&@*&E_gfWVs6*NU?$I8ON^_GKxs+T^R+5e6R<Z|$ zCMFx*AdbIzyER}fNL+r<>yB2GmxY3Pg43bc&zZ-{6E_*J;JZfp*uF%Q*%co-VKiF| z2D904-pB|X$4NXdaV&?AI^(#%%S^`?S?)o^)C;_XYltfGf*wc4oLtB}GRI($yf`y^ zB?EqN&OD;dQ>W~*Y_kPr*`7LL!#4#Vu$gd8Hrc32(Id2j5ABm*VubX(!*Ufh(6iQd zUdDa)Fx!V$G?%PSG#+#f&2;4fuUc!UE9)-Jgp8+0vu&?;G_$$MA7_xu+1%e~TTcZ+ zQ0@Or(Vw-IhbKJ=A{Fp8+cVTr;-W$~O@3ZuSzCG;g?B}-Q@NC#pu=00vPO|aichMV z$5gJL@;1ZK!z4Vl3lMQl?ei)kz0{$ax=)c+BCq~&RsOs6FHp|+Tyz_&!l8yQG?B{e zKWdY+bEZ&f6^+MrDu%RRCZ%XxsPd^;^h5gm(W}<d^@m%oql2yW4LY1^E7y+a>1fxK z{ekUCY+G`<dQ{XWadOGuP_~JRr3cfDK3UoY)cSzhHZNPQ(I4rQ!9>l|hmPqbRfH5I z{%xr@@#FICGPQpnS&UwK7a@tQ4Xs0;cTJ<xC#+bnQ6E@UsvYLt)kW$g!Swt=J<W$b zLwHB*C~C5A?M+DISx1kTXkGso1QzvPqG^2=_Xn^40hbPJkTsr9Y<hg$s$H9BXseHH zc~@1Qn!3O5RZVL1wzSl3H$ABy9K{XQ-h7Zley6veW;4Eaa>tsnOPsemee!4Tyei{G z{zMC?Udm3SR@~c$ik{lLQa#8#^geace0PxE*QR@{+DUian^f0#-M3iN=F9urwaL5( zC}yJf!FN=p9zvz&9XKVXwMo<rrZS4v9{iQcJn`jY)GVj}ejuaLt&datsh(9kbW+ux z-kblaD>ZxcKK*A+SjRJ;W)K?><uDpeV$HMKH8egqgHe@8A@RQFf5D<&wJ%<VvUa^V z=GckTFAtcs%g!d1<R>&C<$mc%{Opxy(<&@`^_SybrW~D*%LcU6s-tJLc<4Gt<Kf7$ zcQmYTQP}?1AJXhJ>&;)_-j{D)sqNL`-t4z-(VPweGK1Vi1?If%9QyZvTCO4OL!^#( zG(TW>p(M`<&6X!{<NFguta>I2gx<%*N&S}svpy&u2R8k4nx5`N|Ap7=@C*XAe@vr2 zhrpUsH-eggeuKJ%$SiVeBGNE(X!^%h8mq?<XUo5C)qr6HgimkOfEN(x|I={|h@g@k zr>~EzbwSPY3&?%s03oit&}3lCDk^Is7N}5iKs15N52cE_5>=cg2<C+fOF)bWqM*1O z3hs&y0nu==FhtSDDgfOw(I;40s6;DEaHk@MJP_RvE@T-mqpa{js>%w=Di*D%R13&B z!@Q6f$fE+B_JW9`bShjEC^6_6hs_@G!11Y3Nw2r_GOH5{jY_erz7^W)JQ2l^$Ll%0 zRnGt?FoNV-H@{$lD3}GExhh;(ZPMvYf|wuIPq^3zK~uvl$<bP0-)cq@zJ9)$p&!X^ z;GAx6rN^3QEUbd6ur6TAp60h_2{L2RInAlC?3VNr!?e^HjXGw(Ct|ZXz+kZ`MIF;^ z5|gDfyIbooYRQsC8<<?twgTW8z1zuI^>(XSG}*207M+yk2v=2w&2Gu)v)S<!dZ=ei zrm(lZwYA<8k^w;0i@eCOA~#{hG$m>@W?3X#WoSaRLvQmNb@~43%S!;z2w8ISW|0Nj z7ii^v@Qdd%v9{`~7|nzSzFw*M)#vC8$~OVcF=^qb{eGGjpj&=;O>+^tI`7A{haX0N zIdb+#)pb9@jU#_~O-nO-M)%M}M>FT}1ONl8h2TI92S=f2Sm5DGP2y*4&p?L=UupZp z%{n-dj{f1O0ph6L@DoO)!EuWTHmb@DoHYX`w}C|#7{+y*6)qjqZFre2!y1(vKH>l| zec0jGoj{AvaE%MrkEN4=r`*t!$t?pV9$2pedr_@H4?Ls-ClP4QhQ%uI2LztUhO3Z~ z;)Nbnq2YcXyasU^z+2g^q=j^nb=Y3sPkxCxL2Y_mVhIL_AQqkz2Gplr=)M>$+Ahv; z4a20cQuBCiS6dsWPVdri;`2gdv15;)0|eA?!jQ!6LiJ4}H;RE<1x^gIh=OknxFDQE z#O8*pIBuuEsU&|!_DVsl3WqDJ!g{N2wab(1tI=5{>XYFrL73vM&UF<#nnlDZtgbH9 zTXdHf#EUJaJ0+KqE+nU`EtK6JOONnPdh*T8*4dRhi!M~BaIC<BB(=?it?5m+t|~0q z^&5P7PRHL$ZW&pjJeIx3cX7D5F=sBzQu$S3IZJ9XTlD&^2DgcdLmXyf+`p(Srz&eR zFVKbQ*ul{*&YrBx%rfI|+h*1*Gy)UI<^VfM0BguGvkhLz>}zqNroR9wAVqkIm}#6= zyq<tlYhq&o^`a(1Ih|O`LJ%yE#|i@00DE|(&l3uHpe~onhdTPsg*seIE$i!pKHBRt zheMte@r1@ys|$G;gp+l3b=budwH~PoWRr<xGIrVoqKXgj!tz4XKos%{LuJ!x%EkMg zf(M|g#WhWWsH-dB4Di@|GaqR-7}R?tNg7;Bp~K1CkcU-uq?*>TszxJya2WR}!rDQ^ z_2>twIe;WpOh$dz)urf{j7%nBk(S8BM~YFsvWoZ=)DeAZBKlMzbwDes8$cDUWflCG z0n_F}VX9^DRR^T0ySr(Wz5$2wIdjT^zBAg}f7RWT?4vjgzU$_cC;KQ4gKw%1MqLUO zF~$it>Us$3A)tpCzA^M;5CrCGxN%YH$<&$Do3K08vj`T03nHnG8&doFnJu@czJNF2 z@zi$MkbF5+13Ob|nZ}*)myw%b8$GGkQDM~MC0tAHC2s%=GBOCMSK^8|Vi->96L%?& zba_&X7md5TvLJ?Y1g|MniPf_r>Ovd2q7fQ{+JiKBn;HSWBcUK-M^qZ5r}Z!D;xTrs zAXbxkwBViq!LcWj7mUR7H0pHS#GyP=hIO%Adr&;mDGsL>w5QY|l;S+kF<z%#C@)e` zif`>f+3K0_SoFaJ77<FZ!siJ=6X5yK3(G}UWkp$GD9;>6dkYR1R&*$YP+27)HCB2M zLWQi6hB~keXB136gCGc2!QeBA29CvL?ZW^s@H$Anf`u7?`zutxEE;%*#r5<DQAL22 zSp5KaR<KC+B1>F{_0~MWuD1#-uc2bO1`egNAiz@$81*_MWx%kkLEw3~4L7M)yg`&v zD=W4xsFh&MXr(dX@#`kbJX||TmgS&a>`RWwva!I(@U_V|b8?wRe{)U_<R*vWISX>& z_&FRU<R}2LJXd?lQ{xCwc|NirA+8b64ipWQPh-A<y6g0)JIiRA-^F4{XXwfG_98Q$ z%BVNTi?pL7Atk17j$!Z-z@gf7?4X+YRY~XD^tNe}<b0znPx7T0IUi39qtlXi=PdLy z4Sv6WVNUYt9Pkq&WWEiPNu&bX&Do@x#24e&F7(m!QzK1CPODbv(1`?0RnZFS@oYKf zOSP+W#nO4zh0ExAu3&krKuva9c@r^c0<{45V(?`$YA(%HBbc##!QxO&qk)N=Eby$u z!RpEj-)BjRbT?UZKQSO08e9_G1=7*JzELu#oaT=5IepVsSyS7st1uB^3c^H<oa}Py zE6?nSrSm<3c#|d1lQsRid{}=pg;_^9i#V*npxvzzAEs%I%bf3)En2jU<_REb{g*IN z*b&08(bE^kb|w~BNl{Y$(5om)tuu9{qQF6Ct-`k9E7hOsS85ff6At14gNi~K(#-ZH z8z&bbfcX@IiMGhtHU}`X(oyZW%dMDWsW}sgpIhUx0A^dQ)>C{dGZ&gI>aQJ=!D~qk z;*TCav$~KP)R{%m$O}4jDtvUjU=&5O4w}bt;JDFX@S4rp27{a$FnbLKqq+dgF+_9% zFJtR0>P(_2Q#KgO@%C|~)C1{`Q!Qj3GLy6sV#iiLT^lB<4M@5`Oo4LM1=LCwt(b-t zS8Z+5<yG?`%_FP@n8z?@(dt(TYAM8wqoK!LN5VelAAI4k@5Zn%Iai&u>t$KbY}U)E z-<uW145uiZyjd?RW{bkqn-x$j_)3);a_~nXc%$2*D3+t_L~l`%H`(b6Z=@6EZk+rM z=^oUZO!^i|icH%mkg8JI24=a;aaBThsvH^R7_e`pc(nB<di6TbpnH^%>7<FQ#s+aW z*$)Q5-v#Yjdx1BGHD19wiU!aObuNt8N?VO!0LnsGWQFlPUTp)gLs*0@0<FL0X*;{L zu0h0lL%RoRHPzsi=hf#J6_>`L9R`X*4MK&QkXQjqO%#8#JkF|ZPr3){%?^2;JRfw= z$O>X2ewh=4Su@;0fpdC8*(1-fA%W}QM2lgD(LC~<z_;@P&J7mcV`ciyS*()dcRKxk zr^lA;wR$Ep{k$OX$zDznIOZt;K@<f{W7+_ankNZjT;e&Uz{Kj<C@-T63Q>;1A2wLb zmt0K1QRd5V)0FSzcu5ox+nG<X%LEy*gcy1#{Y?-_qKM7PLX=_`C>(DIW2;$IU0uXZ z2xTX)a|L-2ILRFhx-l-WF4399#RcGlF6OT6(8!1CGJdTy=yVzioiq2_+%}WX(eCi# zkE&irGmbX5+XhL4$fa0LL^~xpF|`v<rW2Y49j57?<2Y@{DFZh!&@SrrVl^iKr&w5q z@v$5)$efqaaeAlJW>6$fhl?^AiSsiIYj$!V@KZ!d5~uJ;<TPU_`#3bB%=(Z>QVfz) zk2GFR7I+TV7}la74Og9dza~si9j>ANsb=)yMLCfED)>Bp&v`unoofHXT!~O^9(3i# zi(3ZT%u~N32JBJsbE=`MBQuV?I@&YvN*=`GL)xZ_p>~WErhk0;FEh`q|NoA0P1yNN zsqpjHE(9K@8y&q6eEbhXM$l;Lf(XX&Gj#vAF+MKz0yX|C1xCNq=to{1=kq^ODE_}P z^Mwjy_l_&W{^R(3oc_PK|8a!>jOf2#Ljw^a5eM;;Phd9l61$6A#g_=2*roGG#|*2C zQ{`S$ulZ@K;>dEwcPU-U8Q00IZSJSD^RplIp7b~P*X1<l&dq%|usztAKdYcB)I8zv z#EwbLlin)cRdUhfNs)%g+0syHbLpeerLp_U^2;`tH&yJY3{@Vgno!MEH&mal>6~Jk zvSaGrX{&1YOy5$stZrn+!kNGR$-}cwHFz3cxn$ENk6!ZeoMm%2T*|~R-PRaw{P?o( zn>wy&zT&;+Cl*8&j4XU_QNvZeEh`pZ-MXgr)YV-3$qr^o$&$@WK41E~rC(puea(H# zwqM(N?Gv4?%k!6??t1CEvK7ZxwygYq)my9I>t54S)^ldfp|xAqb*&p*zqfbY^_C3{ z8$Z~z<%VUO{hRl1*&d(QcXDg}wpH8ybaV49muB6Py7kblpZ~Psr(fUZzwPJ!Ew|r& z``bHgJ34pVv*Y(WZ{D?Hchl}Wc7Jq7{vB7``N5v0dv@&kHnCyvnSGIco%{CF&W8-K zCi-;_Kg>XB5!kbB<U@6XE}8L9Evop|kw*qf3gO~{8`4V>@!%IymH>w(7q82lo&)|9 l@7DwOAgbJ-UXn-|xp-h}7>WmCVYp%7jWA>lJQjxi{{cEen>qjh delta 4106 zcmahMYgkle`g_lJ&YZblxC}55a#2WBlzU){gO({ND2Wg#f+!*Z0wR*D%!sDRDxt@m zt#w`Uc~Z;Pee9HM)GFG>W|<W$S*%;+<I+ZMwYGL~b<V!uVMg8kv-6zyyqE92f44bD zzT(df@Gb&`5Cd@&N~YyMGBw#!u5qE{4TSQ_x{8L;$)nc@fe7T6tg2YsfY^$BD`M@c z+Q+LNY@WXfc{hRRsOps!E24Kb4G@Uw#_{xO6!32d7UVxhetdOZbL+E<LZ%~slMt@E zc1>kY#f0I{4-uHKADtA`RkSvc*Qu32+_z{DRbNrJGPWz#OCTPDauE${);43Js5)^n z8z+?KclZ4Qx}|v#4n0yeL}a6!{WwB0GFfeEfeimY86`1Fn|ua#0ZmZar2_gEbt{`? z2Y;PI!Yv1gPXV~)gW5lU$}iWqK`4ph2{%j}B#kT}i%28cM0Sv;36Uy_!Xa2%INdS! z76NV2wh2<>*>Fd!ker^D8W$U*2@bLc-bH6fZsQ`2u;!+lx}|xRP}j{XMvKL0uviTL zgW(#byp|WVh<Hl10y307Nh#IwybjSFYD~y9*$*|@Fr2*A$2pyPoi4<5tIrgo)9F>3 zbE@<;i@{*E8e$9<ua7iXwvWjS7S>2Df?Bw|$JEbhd~}($lr^Z=Rv}O)HY_q^76^Sj zp-#Cf^l6~wDD#Fjaw^djq4)<|B3&HXeINB2N25{a<yg?D#EFStawfgvMi(t4p5&9g zlC@}}JUF=>L?bDu-*Vk@Sz-ta+A{U@*sNqF<ukq``7zaJ>P3dmXP7{wN;kbl%jL`W zEyk45Dd(26r(fWB&?!44dv*~j)@PpvkylP*#2IqB^9yt(Mb11<0T;r+%r}|meGlxQ zrVdQXjY-9kJn|AIRUo&`*$q^$bjSg@pECmMd3ZXDot*cfR^>HCvjE!V++x*ha>)<E zKQW9&PApx^{2pBL7(?c=_Zj+hSsJHW*_T&<s3ZOK2X4L8R=H3dR1Q-@OQeq~A2r~z z#5CL_oZ#A-8Y+2fVx;C3t4(Ut$vtE?sUREGk*`;avTo%p&=A4{C2~R4YK|6SmM5!C zV0IHi-MFtJ$bE!N*z-(~^pDkDm_=L7VU|o>?K+NDpz?wGB%o1946K<5%v|y|d_sp& zFw`jHbW(-RkoK(If;RSMtkx}+tot`Ii(BSmTS*kX!3|4?*8hN_1Fa^GcBAF74aaeA zFfH8Ja@EG1v9e!m`v`N5Vw+3e=02+ilMC8?QQNk>DgsWYg43C`^(c@2MxGp{tT^W7 zqn%B+7iHd4SSXv0-rc4C1?P~KfEI{yh)$x9N^N_WVWOYzty33m9xhasDEMV82c2@u zvo@eXN{7_(t6Qp{j{QqPO?>p%6@UxFE$96PHQiFvbAw{3(nX)A4RYSW%c6!1WBO52 z`iowFC0R#*t2&oPkFmCl{LTZyWlT9zZa<FwLwy=0XURjDy^{y7Pj;Lr<kT2VzyA`b zfWgXozW|{d70cz^H^y;eHd6h`Pt-Fh_f!stof)QHbhPL6VTyTWy_Ju#I^JTdp2Lj1 z(#W4YC~tfFGvDr9e)a{n#$LiTRwf0!`-m{Y8>mC7dG|*iVsLL%ohyQF`Q*7l5KT%S z{TI!UpFRIPur^6EKRAruM?WY8^BHA?W>W{rAzKIul%5(0mX{B#2GJ*%2mbs7h&ALn zx{q5Xmkr{yH!0U>CpAm<i-Y1s<pp|?7RlExPNc{jM`pep^OrEVJu>ss6c7`XqpDiV zU(tS&YBNtx{7m4)hcJ9EjgzxJN42@i`}7pelIt&T0#m+nn!ZNUaktJU+i<tek`^q~ z$T?S%0gc|F$8oQ3_}gZ%<|zHNhbE2bu@cN&m^}WAQV=th8}v=8k+=Q*LlDm?pVDb` znp8Hr-o=q5MWOF=-;s^vJ3@rmnAilsS4>=dBBTYTre|aWJ|U41V%Lafz=zT5Gd6^8 zyba=2113qgk7+U;WK*&sCE4z?@w*T;%soD?By_3qS;XQ(Wtu6;DOv25oSuP>6Ip!b z^}l0|+2b4UAq$w9v-mhQ+eDV6=ua{@$d~r*m_D_nY=}>F#l^>IL~dy?b%d|X&8=`G z1qKEI<*0@at63BlEl@E;YvOs1<8-1JV9<vc4R*cGsu45+0l;$xhrmxxT(#0-(TG~J z$m=*NQV31I#};F?IP^LrfFO!?vpLFUi?vxJ4LUvOsoA2(%g>w{?R5#9*=QVRtIW+^ z;RvSznOcBA(I|uk7-ECPAiV+O1?zQYf#(4mCEE1*5JTYj01%^tLxXsoRZ9iX=NFp7 zjCc=F4GnYXw7L+BCE6AcW3@&Y^k%#+HA0|CCxFRRGm}D4U_eqh_kf`11(CXfd7BVs zGsXsK0}X}{gCSU_H}e7uCTcL4-H5?>&^m$wLUn=hX7iMBQTYk>K=et&!gN|~sM#M% zZ%{*NFcc3)^UQpEphFv^<N9x%vPBt<CQidqZc=$xSTxUT?Y^cizSC8m-YRGW8<rL- zim4$<kH#264cZ7BhyorVoWO;pM+8J;S9N*=a2inz9_y;rc8^i32hdTw4Ub`b*r9y& zl>e}7`Z|T(0f)Y+<HR84kMt_NUv^xJ#(5$ddW-9oXI}pq(2td^a{Kr1v6DOwr9<-K zA93e9@wDipPO1N=pTtDmLc_R)`ft29cEIlQ?!hCT;2}<4O#}l~4<q8qrtmJfd-OaM za$+!X(yutLr%D6g2yQ&}oINEXT;zoFNRys)J-9Hro)ZSNffGILCU_iZrgGNfH3OdN zo-zycz~&yi73Q-)0MFqc0B28)jk$KB>prfpr#}!xU#&d@LBKBIo@_g;<3x^ZqF!!` zr#BQDZ!^hZuuiO1o}qi`D$lVncwUSrZaTyr^^`bZsTfBJ=_j1qbJ78&(AVP#heq|B z?CFev_kfy|9X&0R09!c&CehO{=oy^?)mk{JoZ<>;j;ACMYNB4;ow0tI<^Jo$z!UZx z-t#mbEo1oRS*6W$B@t3wN5%%(fcnptARmK}AQ0kaz{dTU(RYE<N0%dJ;2(=dnt>j{ z=##sB*snv!_yo}T26!I^hB06VV&;GWGtzsUQz6{NSY|SKCnrTgz7XN=7!}Az`Q*ch z$sJI<Nc+QeBh7O0hhcsgsA(Yf+xf=Jz%Ov(AOpq+Auxq*1ixV^(rk=@^DY{BHDRVF z+$DD-4Xnlx7uKg-=Lz6E+&qh76N2Bx8KfE1AT%LNybBVLo`moa!d!%WADx3Z6X6^} z5(1`h8(8YB{*Qea<G4s7GX8*m<zSTgX+rm^G;>Pm>nhEh+-<`l&!83|9w82a*)tth z&<rf_D2lLiB#1<k5y<8GX(wMVxDquQu~r+QKW}I-W}8~gt(IO}Xpkeg%U)~05ppSX zYgli1T==nwOOd&eE#r!#@}iDJcf>9jKW9Q#TyesgNh>B7PrfjvD{=PJ#A&(HZYJHE zRGf4yxhmyAYFuhtT0wee#=RLQ@0*;d$;{2XnpHF1I=%D$eLri+-aEtXtaf@`IVH1R ze(=cbD{~^|yfb&>++%Y`9<H9ZVLm^9YhG&JNdAutRxB!BG_?4YqU0iP@lZ)_Y3q{u zrOV5j%dV7bDlRSOE2mYqRersqf5o+xO)C#nb*w61^-6X5>bTWcYu<h=z4m-vS>2EI z7uF0lG&iO<-e@|x*4<p&{8CFm%eD2Lt^FGomTb8B^D7(oZ;EhfTpw&M-rU!IbxYOO z3s3CZwsCvb_L}YePuB0)-x1R>w9~$`U}x9PkzJv?=5%tMqg^pwC0$+EQuf7qgqleJ z>o4IvyVD@hvnB=1CP^J*8u<0)=}3WxCWQJ#di?%;5YI#4b!Zex4*F=oPj#*5dsH>W Y`$T$@N|t#V(je56n+iuf`ZPH3AAw-ljsO4v diff --git a/libs/angular/src/scss/bwicons/fonts/bwi-font.woff b/libs/angular/src/scss/bwicons/fonts/bwi-font.woff index 1e57b1aab32302c3438d584d5f9056158fb1c5b9..52cecc3ead80bbe917f34b4e12d74f4d9583444f 100644 GIT binary patch delta 6946 zcmb6;3wTpymjAiW<R&+dCb?<TByEy5O@Wp)ee)=#b!!!@RkR>rOAFLe9=1FxC_@S= zBbA2`Gz>a*WLI?1Va4uH$7K{z83jdgeGR(mu&$2iAiFA}HtC%I-n1}$`|Zy5yZ3*b z|D5xm|NQ55(_R0@efuS++;(kU9RWfJlzvMLYX3Na`1*WMwvV$#*Uewr+J%c9xH6Z% zXU2W0yQOsng}sDxUFx;mvSdv=!d@d#Fp0i14q56t7Pc-RFrMO+(KqLZ%zx}aAi}C~ zo<m>m5bx?(x^gwb77)U`M&F||hMrw=>wLs(h=jGxp(A0R{_fJ&)m;RNcOhZ6`W|I1 z%UYK%MA$P(=%#N@V@a~>))g!9KyQ!Yu{dHmHfDr-qfdNsZa6VKj1c7V1^MEvI)mZ= zaB|8K!m7$p7u5y`Ag-B0hr5wUH(>}VucR9LyJ5Kda5wuAyMa`Z<^hGd8(0Ly`q`Ih zKhVy7$Ebs5aT`GQz**hzRfK{4(mF^V?&j#O8Kjt8L#`uB$r`ee>>wm|V5SSi*dr4& zw3fG7eb(IAb}`chk+RZ~V8Aap?27G@adBzv4dV^b&Y@nmcc@o(>h+SO*E?nQ;&|vi zquF9Gn9YXEdPd+lPU3lqV>x`(5y$;%WOxzCa*rUUUf?B!A*#d+dh8i<VixoGG=o8! z;>MxbEgA4na+{gQ)p7E)O_tNrWZ9NHLR&g}IL$;srZlQ-v?5Iv1E=MqjF1}dx6Gj` zlhRswRfWr(wZN+y8#tJLzoy0pSGT6$Bktjm#iQORgLHMi-)T!s4hSj(VaykdtnJi8 z^2oJhXKdcg{DItp4Im<zJ;!zr>@RfEO^=R$LEE6O=vq2I6<MtEEPao5-U?ZxNFYU| zvT0Q0trOp7IC_{^d(})te6H#R6_H-^z{cv&kyUJ2%>h*!f9;Eu^JiCVVpXAf*&Y%k z4dh7_>iU5b_1iT8+OGbSHmI9Ek?y^r;iOKbZ<<+4>37dkxo(*KeM&d$=B%Lb|DL;; zcFoOi(5`S^siutfwpLB_@7eCyiTT&52S@2*!38%NsBX}HNG2(dy*WNT*09(gduhQU zSu;})sUxjqt-4axDGuDUuomcD0iJD7`y!3MX^{*js?1(AY&WSOBsbRh3wvyG=Y1%~ ziO$EUG(AgJY1n6%6={d_cjc>tSVQ+8)zghxK7jYh4x?nJSKNs-?JMbVWBXS9gp<8J zS81pC5!cV(_C12mu9h{P$=eTH)cL`+Q#I(1>)us`r5EvT@2lFbdp4wYJ#pu6RYl-- z1ATWr!6Co0O=s8<OAKyVK6=Id+fpZ$cFY;ExX##IP;HnUOSQVQ1qCg<f2n$qeza6| zuxD3*-rv#PR_&yx9!#k4QxDy&Ni(6pRU7m_Ofh3e9(hL<>QNM`f6r+#B~5JOzT}8t zeNX&GWgeUG&r~gCPsK+>dg}nypK5n~1E*B!={<EGyiwD~k!QZpHrxN~=Oc)_4`neL zP3*bnv}@S?{8UC2B8kMOUwDi~x%ytd7WX>!@~GLz%3g^h{KP9Cr*?e$)n8vUvbN(h z5NtrjJbV0t<{SrKqt0P^{T&VKTjYJ_#G{&8cD(r*u1z?3qh>}`Xhu`XorKt8AHU@o zc>1kH8V$k+{BPf_O&-9>{&%!%XSd=`?eA%-dk+_C-p^;%Bhbn64=|)s^Q4m<9~NAk z9R2qcJzZJ%F|Qr_S)BC!DTVetPM$mcGobFu{tg8TqDO2*cdd>6>N;Uy`==3&-2ucq z_Mhv)Lh%Mi@bC!nLIyGhR(?K5!+R0&-u?4QZ4ySY_n)~fswNaQEzBejkv)Vs{lW3T zmXwuOL9wq)$pX;?PA?QGDkP#fOc3w~^9n%>3!<Pn?Fz1nb^*~4m=~mIqbY+fndlQJ z&r>4hg}72yj64wC3r=JiEupONL8|gH%Bnb0R<358iwyli(dVZdIBdCLd(k9V=qohn z8GD*7?1Gb%BA9t>yv*vvJfl+JtZ9apYFAh>_<232x9S<-1V)gYD`(`65e2iLGgpN2 zDor}QNf2|Q`Y~5}AYiJmlkCkkHO*!u;cI4?8Tu#L44fmwUGB2_jd>MN5z_fgnUlS? zbU|h;I)^zKk~1W|#4vNKjYb{Q?+T}-*}-73DEVzu(j+EbXU=G@xngd*ET)0U8EMG{ zp3!GGIIG@fHH#*jHDj($O1Fn9DnjNA$>>S5;VJY`!<bAVcTID1jVmYvfUFmJkz++} z%#z7U#Ar;nNNMH4F_m_Gn%Ahy@lIJ(2!KjRmlJo1EYN~Nz1D&Ae|dhy*`EEnn1;f$ z=ay=g_7xgK+c!SVRB2x6J3m?T&|}}fu9*mh_WzJ_fWv4nrk{RLjrkK?Fb%z~#hH(W zchE>jL+4-*fPvMVGQN?6<Ip{LJr6f(JATP_$M=iywYEOEM+fhv+<$PL0ir0~VA6;* z*dH^&8daEhodqzu#rIlZ5aG>MxMozg!Kc$kSfg-*CI^VA!w$L>pxJ0}qZ3w*#*;YX zg6@&v62ILAtJKL(6zjYT9#to&aIz;8ZdNCM#EIaBImoEZ4c)3hgYSCab%;_=ThFe? zI<|wXB)#O9<kwiksa29wEJOzp#Jo$~fZDVZ%@=(|Tg4H=&`t8nHH+6)HP}V!)GBo+ z-X9$G9b5F0iBEMWbV*#!Q(Nk}5p>)NaG;Zg6>QPr0&octn;oj)xGmns!knp@O9ina z6e_O>>8-k3ovv)p1f5l)HW{iAgozoI+0FuclZZHZm6dsVi|)GIXo1DFU2+;JATdR) zuxz&CR1e>z2H(^+UtO-V=z`S>#|kV+QcEqYPc5?56(Px{U+wWb?7xsQWMqZ=vFvWo zm7#)$tm!OE_pb=a=~AQFqSvoCWSHo7h{LRjdS{npRiv-w1qzsg1s*N(Y>DcT(MbRM zmf8ukjKKIZIlw{_z#6pIw!n)c>zbda;m=2MNj_d8MjD3|ugB-ms@`IsdQoGcln$&V zK?syai*tQeAA5MH*A)!9pgNoG57o5IhH8W*+k1Nf3uCsIIUIB)i7PlNT6NIH;5<=X zU5({GQ8Sb(KqeVW#$#ztAfk8xFU%jD45HxA3zkfwAs6p=A|8N>7S$vPpsY@x!^dN} z&U~WjV4vP4Nz%R*baptA9dxm(j8xKUTB_3*4&xd{Sg{XrUHW}g9Y7K)CZo31)k*qw zG7?Exq$M)(kOCC1q%1lSWkj1Ai#C-<4bY0>`cOn`Nf|$?z?8ahGgUL#R0C=3>S`RO zEnrtVZCWYNc0_vHZ@L;2y%dMRwt8A=qL<<@*iv!O>yjvlF-owG*F!)LK0Op;i>_Y` z0bu6A&t@loo4k;G6SgJ0XT#0lgmAL$j^qP<%({D%e}OmPK=K|~o%lm?0&GdHU>df- zPeXUY26|Ghq(ZO9OSpwRNZtSzWMmLjuf!R)7o$6=Pt>W{Q|U=fUexdWB>{BjFkVxz z9J6Ox#ECj`M#9twwFhbNHZ=ll!@&SzhgBM+r};19<k5F4pm@T_qdE5&2#h{qe;^$7 zYt*T{iCt;91oL94_TctNr`R2C(4Hc@P=sT@{bHVW;(p<>B5bt>_g2q@$D$7gu&_{s z8Qw1hOn~P@D=ZbA<z*##LBBbQ`WEa?%;=B@!IE-7YRvQ?1oK!Ub#-7F&M25X20;+4 zg27`F4IGPL?T-Oo;B}CE1rsv>_cy3sSv2qri}2JRqKW`ZG5Z1VtYDFB`Ie{-^R0P| zO>Y%gUPHxn4IJ*uf&d2@FzR(i%79^6gTV8!2^XnUyg`&vDl3*QD3xFwkxFC2<JTz5 z?5!Fn%W^<2@Fa$0*_dl&_^QO4S=mg3w<#+NvJ(T*teIJG@)C{`aufhrUMfB1sc{4- zzZ_YR5Z8!j2a1Nmr!ZeZ*>!r9on<u1?`JWkGxX$oTfP}jWz?Ia`P$KukP_3DV;Fn{ zu&X*9J*cLBb*Ia9x@qz_Imal=<2*@5&cPGI@Z`j<tXW>B-s|<w%1S(w1ztjgk&k6E z4u5*7BUh6q61^F}v(QE_4~;Y+IjmZuLn9I}R7J|D#j~XtFV(8fS)7WiP6VUrIRmA| zxoWV}(wm4*<EsLA5S?!%qGr=rHG~n%6DSByXfQBQlLel$+gV*{-Ulp6lCDN;_Gbn} zLxoF1My_<cw|AIKE2XicbXxD^W!B_9)@2xoFa%+sMou<4`L*lnu|+dnzG$Px?@FJt zI0sf8Ph!*&encErU{LSYum{7m#%0=f?Xzd&?@|~Hh?@T;3=}q;q1)(bgT6iD3#_Cl z$v)^&6s5|Myirl$2}hN}wqTR&OZF*Mio*d<U<dmYg)*d(?JOH5SKtKWDLNA^k+E#{ zVPvJfT5*?JF~(AL#?C*O7WHAU)lxl0w=&bA$)f%gA{pG4<UagG#A8+g$$dJrC>nV| zhc<<eju(ugXx2f~MI1P3G#K1wbEd%{C*x+f!C+JYun0XwC-5>>&Z5pFiZW$`uG|{E zh?IOdRdA|#%tdNR3x0iIrJqU-W7Ya2l^-TTscHdgB8!ww#)PX@HmU5YnUH1>)?5r@ z7_w;cD+e_bV!+YRozWv95A!dcP{{MMkS8%+9kl9YS<kH1%gJMAMKQx^I#1lCmld-` zVQS0@C>CszMTRW=#R%T$vM7q>I6KyzpYKj|ctUIFfZ2tEmyqrey~(7XOG%Mw3!Nk@ zRJOi4r}<cv4!e@2$SBKzWh=!~f6CLVw|N1rqmWD?jpSCW54VwiFhDwLMXN6G#xTVz zm_|_nnxZb9<F(RCBjAIQAm&)%;u^130klB4NlU+_DLp&2qCv!TLyHHhHC5r|m!;=o zx?Ku~78tk{N)XJ`HYv`<U1Mv$Srlc}swY(f^<)O!4xSHWOid499R33*2GXZy1O(3E z4rUHL&jtmqjT0?~sYdhAcLLwa3ph4dc$bywGpDmkme=9%dL6E`M32=qmg(aKflu^s zg1|8c0R&MLFp6m%K=OJ?5Tg>$DY+(A$3}P=O;Ct%4F0geVz}gr1?*+M1Q$&?4vv>Z z0kIu96uU%_5lbjW3#GppB1r_XSy_ls>|BN8Eg`Hl^D8U!*)hS)#A0WF2Z56^0)Y(l z3rtINBymvz_<)nSKQlP=kqXAII)hH9q0lk&%e0I%lgHj_cf%HN+ncaY%g9KBghAwz zEGMF#5}cUaf+y1nO@a=iboWUdw&IY1I~Zsc^?I?A6M$1JEW>zMju&Lk&FDD2LuxT7 z5~o9;j7s9X48xio90>eGQIf=oJQ6v~=*k`rl_;|wB$5<^<j^CHo0A2eLm0za6r|xQ zlOIe7(Nl*e(D0=O^ufJZkoppQ0Y7tGe&|rkALd4!_4%PQJ6bR|-eR8g9Wh{uihrl; zJKIM3p;w2y<Msd)2M4r86+<l;>72fN>Q%?_{|bOBJ{$n!|K2c!{~u6!BW0#B9v+UL z3qa;58<81K{w?JH*n&q!moqU)!{2G_hhF`EF&h=bXeI2<5g9QU{wpDW2csw+3;!fy zCtmUyT+JM1w{pw)LV*)IbuQ^O!!qL}xyRIFe#WZU(;ZutPUV90RQiUDXEJj#pLCz{ z)_YfGHDyoF-s`(3(2!G?TM=v;b9ijqxTbM$6>KfMV*I#pefY<sU{O=ilaU3*50&JU ztSxOU+gu(jKU6WMl8aW>SDu;BG0`+}^Q4`VmsRbUvaY(ldT8pb+Nb_uZ{6v7SN$tj z-G0@RSA9CIefsKam}@pPL>fN5_IzX84NW(^-}Lm%@XVoE@6WEE(=&I;&GVX<H=mxz zwVrBY<`>RiJO8T%FD*E?uxsH%?f2Z$e9O}v&5Lpto#{NfxMay|OXn^<zwE7B-|vbp z?=I=Su>8=9bt^kp?pw99XXR~{)%9yWynWpr?Q6Yj``6tQy}tL<`kD>PHvIXnrn}Sc zPHsH3@vEQL|NPu0@1}q1n|trBd*9xiwz*^T?#;)x+_iPdw#IGuZTsZDocnIr{^5=V zJ2vn5Hnw`_g$Ke9bUe^clPnovP4vqF|I}}r_}%$nj<@82Nmi3E!U4Ym9*^Ii2Uq6a zp_0(+ap7kV{xgTe$c`6fPK|+B$W!sp5LNC=fh1Bwu8hAPf`WK71b4*e<wJV>#}M@Y E6*9^cGynhq delta 4085 zcma)9dstOf7T;^{bMCqC2agLE2zewVD#|<5@T&P@RI(8@9|%5B2@n;POyP=#$taOW zy^YgH=zOU`Iq4~xs6o{7OH(VyM>&q6<CvOb4P`Z^oLT#DuhRTA-*<jzJ@$I-b@o~3 z-XFdZUiquwT#++s6ahjA^m&DtG&+ue_i`EpzTC%IRW+xipbQrm;K~e+oBeKf&M#QX zbswUv_rX5D__;#Vogfe$#WAvc{OSes3g!}sxrH|A9MPMWoL+!R0<n*xtU>rdv@Iwp zuRz@xLTDq$_DLawix<s7y9vFoW6-BLdUssjRZ>t<Mj(C&I!9@EpV><bO6H*si=z=7 zvGu84Ws8=U<Ay38*fHdoAjoF;T~*_a@6`LMiW-b^gWR~SNud5CL~yncrZq!b)NVik zZEZcYl#q-}-m(8%$4HD?$u6;Kl11_qCw&1}f?6#%u#cEWU858VcPKI*B~N+@z@yyO z{{=L8udx!GBuXH(gM^VZ@)Vgw7L(Ov1KBFSz3dSPmZOKbXskwbv4M_gM{oIMDLgDz zOioWrjf;)Z1qV3;d+-_Z3G*Z>E|Q}y;YpTzSIst?*<`bs{)a**V}f1~^+*K9^dbt3 zy(mvy9u)B+W6Yo}A;XspNTBV0J9WF|QhR8hPMgVOx0_71t_R$Dzlt}FpBQaR$|cKD zs`K$p_UU}<x_|<axv>>RAtOL+7YK7J`$M+^%ST;y*kY<NZ4s(}@o;1nMZe+E?@<<w zHeIox(-@j?gEAv(Zuff^Bs0lp-_b^;DKP{r9t`Ut6Uhp)jSzW9xl_41;I;0K*Cgfe zGR`GGt3^#a&q>(h7Lb_Q$nLXzB{6d{7L6gON2wirh6-R%H^{TIrl4VK)*+At^$^A% zs#Lqb#!zz6@O=#MAxDll!XsaKasy+vSXSwne*xB4tK?;0<_zv(++d!qa>D0&3(6Q| zIf)|^$U=F-xQX&xQ?5g`vTdpsv~t={;=j?rrmUR4l!v`N<5^Co<$cOYLVg<kRfuI6 zV#*(n%m&Foj<TQV3i<e)iPBkhHzRDC95eSBe%$2Bxy`<oC(bJZ9U%fRqBImPq%0Tf zGcLq7c<?>)Fd|15HDjXVMZ0;vCB@4qD?pQ*rAfe|kclko1Kdf@Ui>BNKt<dV1s@qx z*--h?(zWO_yL<$?%Da~Rg2K+_W3h20iru9ha_5SlP<6AyLRlkv-d(v5yN|`-tEuc? zHLUxo{#bbqD~#f=hH~0-OiQLTRQs|XU;C~E_`p<rU^VNS1&o)x;U?o-;!$EYEqQS8 zw!ehi<L%dNp6)+Xh4Kv015tOezU&$K_?B5%Xu{SK?O<-l!>BP0uf$TQQx0!;01HxU z<#T_yuf;j{>P*lIPj4syeDoe=`)g?Fk=u5hm8Prp>~&VAY~OiB(vc1<KT57{>hkZT zrui)`xSakc-j?LIy&zt}k|UK<`>=m#&!UtX<pNgk7J#-Zg$Ht}mZR;@H-L$ltf}>D z5F61jU)lCSFWT)Rx3+z$O_Xg1hf(ZI2kXK>mk#Y_SXa$QIhgC*NBmq<tf)&){@9E9 z;~#(JI}Uq~y}?h;n|N~a<h)OQD_#>!EKEM|$<G4B;OXCgVhS`W#*=43vZ(Fs2bQ5+ zJM}v7Hpz9L?Z)WopXY*fSiQ!wSeUZ;^a~G#$TKJuksWLsouxFL#b>=l?PPV#D$hQD zR_dd^!OpWOO4b*B7z+DP7_XH6%>@ru>Mr&NDM4-4%ntt@{rhS@<CK+OiBuYmIa^tr zQga#2#;TvPgKUt}dSx|8IqD(y9!poUuC4+5X!SVT$A;qjHj32XV3!A4UCQ-;&H`ze zdX%-WBxS<a=##15Wk;A!Ieq<ekWQ$V*g!T=Zo0XmijpK%WuMaT$tv<aA!2MyYy#jd zCN4e^(gIV{GqM1$kVpt|>Le@R#d+=OUcx($?0C(AYto(FmfQx#lq^U|cKUq$AtW6S zk5^R^hSd2yV(~n3%ar7lLHw4So`HcAd43-C*EzfW@s1x08(4X;_&BZFL|&xiFETjD zSN4OPKC`4Oh)=DGi;vSubY?IM3!j&rT@aQO7#IYMGM(Tmn&gTWnG~Y82m+<lAV~ox zV~E-0G#czWQ5O&Z0yTw+LchfM^K3Sqq_;|ffij6fX!;|L7`rXZXfOkalH{~nqa2P{ zhdt6{FoKa;ZASb^Su>-%szhovn|nFtWM|I}3ughDdVoO5EV=?rvB6T1(S-SejRvbI z2!M@}97bb^De%z%kfMV_g9L+J&qOfh<XT*29Oz8PTww;iA;e~jb_B%O?GYxU6~A&i zG0<WV!D1;I&LAi-ASs+aDH;V)VpYL{Lv%UJu|fJklPSbx3N{$6g2<CeI!xv?V=@8s zVL<_*hQN5MwSTXuoCIee#$+y+L9Y+B`coNAS}Gl;62NR7p5qJ*(+3&o(fbD-QD(D+ z>L{aq^9Q-21wrrhHFfgCt{RMXQ71ZZYoU^q8j|!(j49Nlk8pq_;-sJ=b)`oHL}OPC zMiWq-Bn5YO)$Vx2tTzG}nA3sN7B4<~Zyof{#J1ZheAIV-TSBEE^+R@x4N(d^qwzTr z9lK9kmAX4m0Q*8+r=0rnBp=vusJ)=<{COcrZX62j%q<7p{g0H0uTTfRLIJ;=?4F}n zx;FD~4bE!v?n*E*ZRQYfErU<M(=u6rTq*?<H~WTmdH3t!ThW8V&E2v~g7Z{7g*<7g zHbNB^*Fw$c0|#18S>QQfnd&ibwheH2dz);~0&7}k+hIKalJCGTxx3|ngNJrw=yuxP z5)cHEZ?7$p!N5PKEwxTqMkPvCvo5;Ut8>BP2STL_mPy6x@7WeM-+R{uuS@a7!!FQf z@2)VIDaDao_9gXrjo~mI+FJ_4VX-z2TP{SvNnjRrL(Ad5fUO(~ec2&6>rGFD1$t;! z57JyV%)2WQilUlc&R8+aHslU53HXzVkdDn;akO;Pd&ksD@4x|&QnjahkuTt{Uj)iA zhy;-k4+mfOQ_lB*+sEf4<?ydXBhO((n0<PWkNRy`x10b5--6(yz$qrIMalzka3`a; zegK45agn>6eOQyKqMVE9nWF&ZD4)IqDR~IB3weLKM&x-d{xm!ehgJqszn^bC5Bv%@ zE^=@=2*C{+5q`(%$n!N0cMl$Atzd2^_RxEf2foLUD%_uXM<9UD;odP+TM+&bhmq$f zLM%b_>45~~`yxgo#v*cj{4k`Mh!cn;1eWjsymY?*FMJg96p=&}{0aTW!Km}|gl*Ay z9+a^6HJ%6Q=|fTGC`QC1;t<@Q+wcty#SJ!l2PWfwbyBfD!g$J5X3nxySSxI;j?kd6 z;Cg4V^KQt+&~>iX@VM~35f>w~BbWD@8Z{wmPjqeUQ;&}6Jt%H!!r{Ji`%Ue4rhk3n zr~!!svj^TwdMs&b(%$64l!nx})XKD{)9W%G%V>MNU#2cIJM-3{qQUmTbwjp2QI@r3 zsK>p)-8F1l)rkEg_l&waCSuI-v8%@J9eeFJ3&yP+FN|L|A$7vFoS&bXJ89~q3zOfO zl02nr>V;|9(<^3_&YYcBo_950S8#E*FlXSL$~m{^9-Z4cZ^^ud!rJ-y^WRyJzc6m$ zt)h>gO)oxGl2`I`>6t|r%E}j~FTT5^ZK<ccxctrK0n0mA)Kwf^Ic??0d(U58_3G+~ zDqYoQYo@MgufDamaNU^~w$-d!KWKf?`lBzFZg{meruM={=f<Zu)^EJFDRk4AI$C$L zKBj(JeLZ%S|C!w5ZAgK>7FiRx3jZg0&!xcV-l0B~5&r{@#Bm3L0qrBnP9G1%PW0a4 isW93S?^78`Dw*YdDGfrsjx^}+J(3E~dnczs!+!yd1mw2> diff --git a/libs/angular/src/scss/bwicons/fonts/bwi-font.woff2 b/libs/angular/src/scss/bwicons/fonts/bwi-font.woff2 index 88036b7b3e8eda7d55afaceea4beb95b02821c88..4c8cfd6e047aeb1b7f7c6430b7252e34e995f1b6 100644 GIT binary patch literal 34628 zcmV(?K-a%_Pew8T0RR910Ea{X3jhEB0X3We0EYDd0RR9100000000000000000000 z00006U;tbZ2nvMSFoVf#0X7081BFZrf@lB)AO(aR2Ot|U>!YG>^JbVRf{g>?F#Oc$ z2+J4;20Yw)_MZ-@$XNT43>0lJYZ(I65;PX#St5ahltnSQYY&WM`0T{d)1PMcZ320Z zKkL8bz2*#T8~Kx*yvOr%dh*`?5#&n*?@xkO10+y9XrYFdiUBnR+KEBinNXb=GILlb zMrYbF^UjQ}6K8mSZoREx8*5_Z1|y`l{f#i-Z~Ge{y%8z75e6|}ASk0sNr_Vg1p^CE z1i`Zs75l=-^SnFp($hPYA+pV8>lN-`%N^WNWXl}~{VH%EvR5hX3SCf+D%dE^PE%2% zL9t<C><N}sP1Quy)Rz=X(lxEvc+(2^<+M-bw!Yu%7Ji^B5HGU89*4}kKXC@gN?*FJ zRaZ$J8o=NZP;#g<zpe5pId;O<DV9<4Gx4$%h}KR+hXtpx|Etra*X`@ZU~6JIVFQOj z_5p9V5$gZ%oq0Jq*Mc2O7&OMA_R^@`Hs2Q8wXEpzx#cshFI7L8T4d!LUUe>c*W~)* zSeL|+6-?^-`B7(m$(6hNyJPr>P&7v%wNfin>^K892L|i_4g}#zYR@!T*i36HonayH zuy7!KM^L9fcwq3Kj(O96q)SRo%(4b4u#I%YqZhjW-&A|s2MFMyes&J@aZ`$psocs< zIdkuO^Jd<=0}yY95I6uBhNM0PN*h2bVH4hfd(I39X+ixQo#m7zNSpXGC8u1vY1Zyi zTYS6Pb@h8Ujhn*ruWGt}+{re0l~fv)R;83SY^_b%oGrta@dN)uLiw@*5Ryrdmq${v zAH{1RrTBroRvGs`rS+`=piTjK4J2JiUX$o5rONI~EmP@Rwzhf8%an}XavrA4S;m}_ zzQt3<EyI>=%$8*>|1&_;yjCsX)(R3f{p+0Pw*SeT&r8I)_#RM*Awq;OYUzZ^n1AeJ zOta+m8-I2UlkLU<f`Y6&Z5Pp19docZx$kVL{J=tnQWH{a$3?OvJ9z+L!Po~R09}cm z1^|~WzZ-rJCbZl0o1yk#9wJwL`#W!+?SQR|V}U+A&=!P@yGek`S1_OwcJB+Kzy<^Y z5Ug(xyq%f&va}>Q-dBJh7x%)%*I}sVq8(e524F^qP#&JgwM%p}%&$8&?!L;xl)Qy8 z0<^y(fb^F!*uG7G1P+8{1Z1fT6dB;4Z3m4+ZBPOceiZ{MVg(h4Bst@O*@cTp<0o`Y zD{Lj3lDv={*klPqAxtL<kP15~RtamW_{iK*0jbj6t^my?5)0{i$bkwRLd&6Ze3=Sr z5F@o_3~ijXprAOy@FockFKVQyZ3a6b;8(jVZ<@GhT3O^?d)xg$K?&2F6aq}}!>CdU zEXiQHf%D6&+cxv<lG41|?m~aCyzuA{_V)gr;QXH`WruAMGU^$1^^=-foQK8(xl+i* z?M-Uw1-eCfd}Q!LD%I$gGGqrFd^kg?fa0K<xHNe6TKN|K`oS>1zT;d-`f<3hsvwV# zk1lY-_ax#31EhRPXNsL{XCQ~cVl#BT*_w-ABH5>0`-vzEc}kZ1MJ3<Q6w!lRUrIDp zH&^E}MWOEr%~SqMUR+dHXS$AC;{M1h_I<xp2p#)^VnU>8<tkB<90N*m+AojmSjdKa zt-HFCBN7kcuqN9JOGes`ezwA(C=il!-)9(F$<WXX1~v>1Ru&a{e718AI=Tuj>~oLn zu5eKx5ZGMKy9J;ypbT(aK}dU*j<2>sb`CTpNJJ)Wm9#crNK=zXDJBp^K?&CHrNKHX zmsNGV95w`R-g<i*SEf7CSAmr@+$Lr?Mh*y9p;VEoX^f>bTmS_u68xB=5kI?N|C#bi zL_@b)){neyI7TEIM|vcJ6v#KRk;xT9)inJsR>D<zSd243D>f3?%E{DoauCYONjO8f zC*0-I*0QK$>j;q^g3VsbA_N<Dl+~c%cm$&%5!RH2sT{)=LZBj~s47V>hyBaW4i;pq z2xmk(s1HS&W@y3S0l|2)f{@4dI1w5^8GT*kEwPJtG%qo#Wq=@U05Z4jjs0FSNEkV9 z7|QLUA^Rx;(+vk)z|*t4GR={$Pij3LS9z~UXO=Yv5{@G<oG+Qd(@8*T2&Hj$d|sI_ zKQ34*$g<r6_*=5qSxAVuCj76WumW6+cd|T^O6dKiVGM!#nl`tr8yb4#C?B*V&@L6{ z2c2lrqLw-z!qtWI7^jo<6OyIvL@jkaRUVn<inc?<0*04`n3U9Qg;my+2fE%kHaRk+ z)_hzLzCf-d5<9>`YDe9iRX(bNag9g_=t!j}#E3_r7l#3P5k4X@7Zf%?ao73K6ft1S za3TOsK+wFJ9t7j(V9g{(=$0y=uT-IqB0FZwU*!8zBU8(xy1K1}FnQ-28gq+p7bs-& zgK<1m#@2hsHd}Q9GdC%tW@zqHh$5BQ4*7ZtFju6D>>{3af+goa$-4brOZr-=nea&m zed%p}<#9?YZe7e#FTMNhn`kJIejAf5p8&&tx7XG(DjbH{y{ZJ1_`j?)RmHqwK$nDE z3k`CaXl94pD6EBOnL+T7gryhdu`HBB13SyoX7&Ir-{=eRxR0BWt0Qs@>yR;C5-f%% zMa#t;w!DA%?WkJ|>9FzcWLiNU{MyE;+D(W83SqJo6uP>~p=<Rb6^{Tt_*TRqguCKU z%1vo5$k?T<8-{gYj`py^-6Tg8<Gl(U@7}G-jA7}|Db*QWFK6sge&<w{2UeD&0kNI` zt%Yvy-&N+KG4SJL1hFmoKf1~PFR!E4SLW268)>19-r{(BJ)X+ocQ#KokG>+IZWmFV zu8idOdAu)VRQ@#g+N<<$jq%+hnK)>ktoxn+6Hc1>xj(vSKZZ0yvU`68#)T^+;<B|# zl%^2&bDDB(i_;$We2}-BV0MnkY1tnMB?CJjJB=~$LqkS~Ahn)p*@HV&UU4~ey|yoE z&^htsM{+4mH|e7JCZrC(@E?Qzhow;+IN-N}@q*tQl@I4NE+zqtb5=eYcQAkwxr~hd zQx61!aurHyco*whqMWABXCH>XB^y_DR<I6-4yZIioh~HIS(c4zA7jla6RHBk(n{xT z0o3p>3QD}t*ygHWBnTsRQ_Lg$(vz$z=g;VwwJ>0%y=1UV-ay1ef1fXEkG6!;pGBOr zuM%Q{<RbESbR`rIl6r@5Q1o<k48=eTbtEVVh?)+O__z%EehqSZR6I2n7c3kwl%D!p zUh^dxuhTrJq05$`s6h+(g&3l-uZwsbC+p`7PNFuKd(LeBB(%m*)X@mP4NTNAPash@ zjQ^iv<@GgfY+3Ch?E$EZoY&=iIjj_To&BD`Di*qWQ7s2#zQ`|f&SOxgICGf@F^=JU zo24}(6$2!;nkHG~8PbNOJrfFx!=bin=2L)+y(f=K-#4M#gVz|FC)r1JH`4WeDrG0l zUi7t_YA9%af~VzX8*CJ)QT*}OA=3e6>YU<(_L*~#@2tJ~xrT$&4d~9?CRti-tU|&W zNZ<R8I4#AHr~bxa4u%j)f35SMHZ6;`T#vY&aKN)TX*XHzIE-{MA|L05j8>iclt*+r zWsdy8b{yy;l{F!aeuVNOjfAlCr)UGbsZ6eX&Yi?O%cCj}-Z`mDQ7Ar%spfXD0=q** zsdb7gmJqqBx1-V`M@uv=II>s+zfTMTj5pMDn+{TNqhyVZVsl~io)~Ja`}{kVuG)=K zn#)Lr{LFc$Z4M8~lSiSZ#0-ZAksV{lOSO6-Zh~?~Z6`V;vK}+zX0b^5O3IC?*LF|f znx`xs%o$Gj8EV?(G9kK;8So~KC+hx9rM#}U6C#xDVYwU{yd6tZj7?-l*QTNf9+r{D zL!b#LAR8@;hjpmc=29pqa@!$!Lo<(GxG8J9r_7i&(wJz0g~(uXGG?gKjLEIB<vp07 za6*)IF)oaIcJz7hwtQ~eoUrV*C5D8CiM`#X9CY<G$Z-3pQ;ncg`o9?fkcQlUUH{%H zrbk-k>PqOj^nK^$<SaR-=S2Q>3f#=Yjr%LG=m)e&>8$hzAl1m41wHR)o;zLrzf6d3 z?nWo9+$~*!Ig&Kugsq{yPg<oBE_2~T2B#EN6b)oV!N-%zZi(D}q{+9F%rE`bMf)ZZ zhZDr6)1c=A)<<0*V&6Sdu=7KqkYa9EK9`wWwetghe_;5+fWexqkvD{Q7`Nb%z?cWm z@=h=fs2PGy7ca&&U9_dk03?Q-;YyenVsAypMg)<pULOl3A-0={=#)F@Mjomi#EzSx zFII~^RKy(9FbG3lX6#H`LBR-6{E!^O6ZhV(3f0GqyW1>YIvK*38n$g@5R2_Dhj;*a zp}xiMU3+-fRiKsw)mhbE&LcB~&x>ccAuBL0`Ui!uw1Qi-x#T!xFc^=b4*i@L@639w zaU_-jdy7ODY}#yVZ3|3J=gZ6tb0IU^g%56m7Tw2j&`J>21nqw+dI8T`Q5mkqyP6qo zfsF!OvZ|hu&lRPP|HrZsUzRa;@WQ@8Sk&4Up2+e8;1^}|J=r-%y0Dy=ML{kKo{ZQ$ z6_r>R#bE;9Fp%OY<s8wFgQ5-yqbmXea#tODsG7l+CZr32>uMv&2z7<;hB@84C5l#` z@C4!lFL)&++MFO~swf#w*kq7=Lt{vYJm7CpR{MAbdV)D2(q5ycTOkVntFg4(`J%w1 zv7$>eq2$D9l-qKsTSnLUZy9ob6Xp63SgauNsQpaPmU^g(-UwnccIP85K83dfB3+P- zNUY^=B<l69vXD%)2o>N^axIE*&fjlTMR8LIo=qH}sG6WHY=nn+%hJoY=C-ECQ7thp z=x@Eh0&WKI&4`aDcd6b}<U#qqj_2?SkuSWGyFSMPR5iDbBLpBa>U9=C#7{$bI>T_> zhvG?rE6tO7;&Qd91>MNl+$0-1*T>IjXb|+iz<AfN+|M$*>USB;`1S$Vj6=)hl9{|# zK-<P@7=Z=%HS)9h?SzN$L^=aJO7__T^p?WfWelYD<z-N&F9t5q_dYHLZUuIxB>H#c z$!<Rho2p^aD#pf*NKt9SqMYs_Q6~(eQUfrP@)F4u$mvx}%17NpPAp5Y?MJcG5Fg%Y z%$hG^cU=ix2fxaTFD~9gdaKtTn8fbCxNDK!fT^uD6qWbCPs=eQXV}vASyuB9k&vvV z=PR&SY25<Dz}+j&yKWXdm%iWj`yOr4t6_4sbz5GLMTldz{S%xmem8q>sViH%5e8IU zM!6|=S1UcNZc#e*MwRpM(ZL#P&^uD)Up}ewF>&EIQ=qYBopF*4@1*|*yO7c^N5g)y zx|Tunhs0ozODULS?bfXpF$$p7W}&O`He0El?q9$`Q=(;5M^_z?5Z8=tkh~(PxO&k$ z?dl-sVn<RP#Jh;)jH0iJ2&5bnBcT8eKP4is8Y2%}1pnbq-sPN-4F%36!J|+`CVl0s z)=gGL$B)@y6~aBvxntJmB1<zBslNlmT=$?gP_ocsy7CB_`eduDE`vH9R0M2N%VUsO zNDzmn9htbim{+QJ=nJh{S)rNdXq{Eys%Q~XEnrnzt8g9cRAc8Yu^`taPyx+B!vgC@ z2*g_>k@F32uQ3P<eOn<HFU!NR$P@_dJuNvO23f|8&C|=q^2uq+fQURwJg6?MvhqfN z4>AcTYD9=}$Cxccj|Y^p@1r#aYzby@hWgxn3Auu~BYn{2uDZC&pajgsca=v(h=nyf zqNJ2*b=7yNd=NE;%JURKu4!q{!dX*&XavfP78^ZI*qjO^yeSB4RD9xmt*u=D-W_4q zzo_Q&WTUN}SoGKy9=pIGN#!aV)3fX^ipCs1rZEfndqN3bszo5yx^+Cfm7tz6HstH* z=+wkVhv>1AGR#ZI%<FIw{b5??tH>bUzjr#iPYB^OZ_!UO3$`=gXD;Tg+sD{1hLBYD znV++(VVwG=+^mMqSz!wNh;l0Xvd-gHYqX+>Nxx}h>%LV4MEG6=s868H*7=cp+QF56 zNY0h@LCij1<2ThxJ_4-}Jmq!?Bsh24DSz9Qfj#WgeYUkTVQW)+?*I<f(Bk*=lSnbY z57g2gTld{U2<DvylJk=70#hk*6Xqi7KDFQjio<;ITp&YCQA|-sF<xr<(42z)Uq8j> zp9?C01$~N<Y4*B$*p%`zhT4`P&||I(u$1GofLZJLDu-n%U8>IBkWxu)ix3v(+o}q< zL72UfSdNhP_hG`hP4eqFeeH|S2t$I*9vKX4B6#VqkOdp$EuRHEnGKtsBil*@8^>*Y zw$M8SiG9rczJC#gf!ROCOGo<;{cnV*HF{o=#Uf<C#j6d8EQ&)R0|yN=C(YE<;SwIf z!684&%l>mo)GJvu!sQziZ~EG$iZ2q38-Y;JiL{j`69bM@7Jz@K@8%$B!^mcohH-Lo zh%a}5%zl-Rk9NnrY3hwJp(m*Qm6?^^*%jwbyLt#XeHxfEum201aiQfm;$W;QZY8Yf z&<ofeFX2m9v?Kj&%!}TCDjjp<sFy%fG^s8US0L{~Y9n|yTMbK$%jQ>Bm;Q~-lT)U^ z%63}{g|Uzc;l!@Pj`kQNhDl)l9t5(h_A+u7&MM|bCROq}Dy&S&7zPsJ|5LacE@3#2 zbqS=3*`=|TEB^O*Yd%L|&OX}RmJOc2|2^}kGjF~YupaBOcA-P)b8s1f=KRN9|9@na z-^Br)_gG1R5jfcP<g#zJ;sENU7a>P!Wh>U4pZlvv!|(ldAF%DfD}->%=dRwBF;sQW z{zR;*BF5VyqI2I@mtL7i+V=^0Wch6tsO6oiF}}d|z6YUJG!Uh?eWh9D%e06#tmqkm zRlqNZ*ob8E3bco2F;j*xCqI<QmWbC#19~Hcp>9O+J06Pu<f}dOI5u5_C3)of9U5?O zkAxt0nOzpZXJtA?4yZq$m5$O^<Eqe&8)7L17x#(6LQSDM!{DjJxJn}4z@uy{Gm3Hy z*hQFaW9us<3+c<@t^363-Zd<H`K;9xuQDfmbyl()O|pnT^;X2^h5m7;^$p5Uhf#ys z9@PuF-U2ssee_{8OI3@GRyXk<UAd5H;19`F=Y%2y1J8UcE<WH(l@CDBFIC=>9uCe{ zopOyLj##Vae3+uEk`e(GDvRz?4_aZ6PTe$PGIBR@a=ItBF?9v0x{2S_mNS2~b?}(x zETipa&IW!csVfC^LK-G;g5{MF9I#*Ag-oRI=RNxvrs=)a;9pzhBcNq>dxF$fp$YV( zafa+ju&X$a^0?6TfoaYtN$gJzqmKf}-|Vp$x;~<}3evE%bf_utP^}ggE(SAa2v^U= z90rM>55jtVXw?Os55T5*=Vdf^pVO-DD+(fg`L=S*Kln-S4o^tPI)R1<!<Z)_>g0Ec z`g%9jO$U$mA~je_goHx1%N2`}#-_+pbS;<xA;w|AE%uD>r32Ww!q%_@L+5261nNT+ zL^o)fDk=`)^oV~OrqiyR`OVxTe1lm>1k>SwxTbh!rL&ydaI5oC8*?|y$<IJJ6sNc8 zTt@lmdHs3p|JyfbAgtW|`Uf;$B#%Bd#EjFVe&4&;_RiAf<$T!X)AN18=rzA6iYjVJ z7v@dML6gCv2TnV266YD~r`KJA$w4${mn+bdbcxQ_+&*JyuzhVGy2wi6ufYXW49Dxq zcs3j%)7o_nHz`@FT<oaGg<JPt?G?t2N?ks#O`?K0&@WlWT*s*9y~l-Dq22`2{v%l< zB@6)GVWKTIb8KDL!zx<^Gint?eKpk|x_3)qxK-(D@9fUZa1-wDoe?A(a&D+8slMc# z(BNT*>3Xq9Z7_2Rt6XH`qM(B4xn8_A(e;jyTSo@P=d+tqNLs8-J;mB(7#m4_+wy_Q z9+I4<=C&Lo>%tmBtRZuKTB@>j41|x03qh4d2njN!kTJSGxYn3cR<IpUPU#U|hqN4x z;qwawHg@4uHyVASbrz5o<znvRo|ns(1<13W8o!S9H*uPCbMDIfSpHq7kFfG^<K_%V zsoyYs&}>B0FgY<?W(Z|(aYN1@sg$>7?>G5Lg<!a7CwyN@R5)+Ye^HlSQ0|JS{$7<q z_yZTCSk{LOOEnFi{~CJP4idV+c^)FlU<gnoNi0wlvM8Pw$WPgXcFqso^2NY1_kd;2 z(E#{#*sAFu%sK$DrE}$H-l?Aj2_2XTu#q1#B=uqg0d#U)FLVmin!*Fc5peds&Z|Q$ zMMT&BZ~ETb^#6+7iy^#KO(N6tmtm^;Zv<@26LZ&$>{{qoT;wKpynJv`&%!g?Zo+$> z?1M8-i?jF({%&&-b{vFp&k=eL9fIG5TPUaB=!n|f{*L-Wf)cE}MBO8)2&hCheD2k3 zcLlWlDF(yw!O;%=J!zS$`glEqJaj5UPG4zN{37K0Nxvkm$(SklY#=)ZSaF<1&85zL z6l0lU>scs^++}b-H9dVr1+>q;fYM~`X<B2H9jBgp$ju1B;0)mgwsupr$VssvZ`6xx zgu62UED+Wnd+{unqv-eQ37yRoCi7QK_puf(g3G_B#mqGc{}cEdP5z|D@1NseOH-|6 z>E4p)=CUZMAV#Q0`7L6$-ZB0*)NGD?bB1g)e97+l;N4lQ;?3r)Pl0G4mlfFWsuovP zT^$H1L8fTyxrFdNx-EKy$WiKrm9?8GTAl2M(L7&AwH`jVS>L+}FlgZUkLItO54$pC zB0+qA>o+E6*A?+d1OinFgw9uPQJ;`LvhL+w-CamrI56D5cvOUlgrO;=`uo!)$dFQn zC@KaP!n4I5-(*7Pww2D{*2mcLY?Upf!&oJPOu9hw4FxAOT9x_$%et@QbE5ZHhBHJ# z$qQ_8PIJyR?D6t<hujgSWM$9erw1vsF&fV?<{g6qTg=|4o&z4NgiX)^)nYp%Gyey@ z8uCN+T`!+?@z&fnuKisK^=0Z3d&(WSE$1cRjZWXPGT>)B-+bw>%$gZ48^$KhP=Z+N z`OYm=1(`U?DCmuCZ>{zk+~Ble&7yJmy^#?Ov#wVKD4z@kjKeskTk9L5mZ5UO5V1{I zTj)xaHNYFNGn3F|y!loI2gWGMh+(dXiM<EHSnOhu@{>bF;%sH+Fh)O=PS>c$J+;C# zwOi*^3Lur*TkarpS^+W%o6U^!Q#$h-jR!84bdDdJ36r&RF>eU990P1h{OqKsISuG2 zJN;V&Jta>2@9v(wuyTP*i;9a`B486ug166pJf{=77_IiR))z#m2c@{@=8nCRHK|c~ zmGWT|grjfps=<my4^~{VPl6&@zMEX>o~1^&)$9ErdHxXghyM(8sINik2;J(Cgd*)i z6g55&aXJTw*Sb5*0!&e#KZpQ30Q_RCr=%(N9>VigwM!lHWE(~`ZN|V)5pGBJIj1$Y z%p!EMBQQ=3{%Ctl`^raeM^PFAF=PXi9|e=BcIj4gDmuXhMCELx6l^qBFBR)0K2)i~ z%2D$l;Grm*1NF_-DSFu>rU2=d1uS^fmjMy}kj27)-c{OkOSh-l=1HTebIQW;&zrQT z+X!!>3(xT`(Z=R-b3`}#Ebuzy^+=_dFVZ-+F}F@Vr81ufqsR1_YLB|A07c~LRQ(V5 z_-IcEfF-8BS|NRBwh#s>+(9wb?UU47&#(~nvgn?=>p*u;Rw(;ZSCJA$J4On?wI6i~ z@7?71XJeS135QWk+Y~%whmtAocE$@ev3f5p$p<}SRM3{ZM(E-M?Wpo*vU7vYmxES4 zSM%_1BgVY+T=1^p7-i`x#{|D5?*3?cL9|5j^|=ZXug^T<25pauTc#Od7Urnv#*### z2qDFKA{6r~XbHZ=px!IeJu9+`&~cZ>U{h9k50if;B}B(K`0OSy7XB7NHjQcjYVS7b zr<?imt;Aw$jG0JZnD_TMnW<p7LnOenxa@%=A=wJ$=TL=0mN|5iM_JNBcs}^&;hac$ zy0LnsEHcH~yXb>Mo;amyJQQ-Rmo>}r-@u3TKzO4)!^pVwhOqcLj4(LlffhuFc%Ppo z0-b@+rjK*(5ygi&7*1@mE{_Ui!^>?jLUf0J>L0+^pBBDxcVTW>-~Wz5obNx_20kT4 zh>~qyqv%BPz)e_1CsE19QbdtduMgcvOiP;LG<J1;FKd^47q(iKUPD1VIE;GU-Kr$` z&l7NNx*`Ro+)vQ>P<M}}hp${T>^Wkt%?|H+l{<;rsnP$xf%LG%aEf~EV4rxX_ee;@ zh8MHMeTOLaW$N4_4z2a24jI()W`1Aj7;r(~9BcJU(SorffWSocn_Rcey!B{iw1C?^ zU{u#{mQJz;7hPxgJ>lN!16A%7uL@j$9rJicVbE@Er6r6CZ>8ZxNT1|{h_yk|oie8Y zSlifZp1c&%_-iY4oZ_>hX>DR<mFJOJFOq~oj*T#-tguj^BB(8Bz7iJ&@9dLppO5>R z=xX8#8&Mr;z{DU!vnsKs^gRX`E7OIE5v+7>Ullw41T4K92#%un1(KKgd^o|4qrH~X zd3VrXyh0&S+m1qe-HI*;1L!y9##BVtNZ;DCo%)+)@Hk14i<E^jDBVtnwxf04{wgVx zq;^dbibc9w{qPK8m=5mntB146((4aYIo~2Dn(oE6x8GcqbWWx9e_v)E%)CMj8Y*;G z8gF}zo=}ac2!ys_6gz8M0XWI(LbMm6cm=#F@*j0N2?gA}x>y0?_e!OGdckh)poci- zh9KwfT%_XA5*d#aoMf0GvBIk{O^aC+3!|a9oXTze(%!K{)YYaAX3f}H48z-gl}kr^ zBE{hf>v=){VJ_au|2q9fC1_uds?Yp>z5J6O_ebq|;QecfYtMdF^mInL%?3a8ZdFjS zIMUajOXqggqw089{r@N#xe!mJ5U$pom5|T>oUonWAFt+QBBLsc<L#cyHk%SL+y?x) zUf?L^V$SFEa1s?JRH^5g18JMS8FHuY>O|NfhamPMfGpbxZb;|vpd4lI=K}UL<_=`q z5>c0KPD~7!K@NrkD=CVd^C4)0?AJvUuT@zV*Gw^qa0|cvKYhSm{QU+kbPVyjMdkVH zg}49rH{!l4pe_sigS-ZW7iQZsDBs5`7#e0ri29!hYlta9()b$JB;z$04A*mOhJr{t z;!zE{qPzBMc2>VqfJ&X$W|SV;P4(`>4Uzv#lkhL;ss2%_$Zqrp+NtDbV%H=42qAah zUiXn5nl~`_Hq(8qohOiRkMKxO1TvGa#Q%z;!i~;0uDzIBlT{ZJwd~y}ZO|<JH`qL? zK0y57CON5ca$%-u&rhP-@q`H^polGu)#nhIa)H{~L)3yFxSa%gV|P!Wkx9%w=qvcY zAk;gJ@ILJz=2ra0TM^V8bkLqZCop0@C;$uaHKhraBCM@7#|ZHM<6w?Ea*o98D5y|P zBr(jrZ09DJV_P;s%A2@Ib<xZO!k@;}PvICQ+2z0>Q$=|9f6oMe?}u`R;rpblYNO{W zTIu85!WLqITsXCY7a{%kuRFvWNZ#HL6-eXvNu|Zg&Rumo8Lr@xxSAJ3F4~ll#aS!1 z{fx|507kqc|7e;zl%z+Yw`|p7{(n_@Sqf(DV~kZA_7JZ0;QNHm`g|vGQ#1Hm=$c8j zH<la;3y*knxwR_kQZi^4>fKDRTU`VbkaMdloHdiWsyS1j{<_oWFRN}e2&}<QbeUiW z=I^R*boZ8^+V&6oTjnmS#ckw@z_$i#(Pe@)n7^vJ(eO@?M~{XfVp4gtOSiW4GrP&M zzyTmTl%!oiTPprvR$#m82KAusMiuVodN^%<9RoLW|8F_vhw4&Ir*zDYYxlacY}}IN za{ONdIxZ<NAxO9<n1X;6NCv=tfCVD~DPJolZ(^Y=|F*xK`O;CRkF;@nQi^D*g1`ts ziU!6GAF~F+TZu=|-+8q42>Pd$n_L-`BOfNq0YBcp5Q*lJ*yDLaJYF`sr~frd%A~`3 zyt{o7*U;+?3nc09nju%@;pam&0!bA;(*S4$hsIEL+;6o@21*6s?z^uni}q7(7SqER z+7FsJvC*_Iz-74lV{fQMf8f-y7W5}FUcEGm2QJl)$uU)P>&5cM=hkCwIZFRdJwVuu zNQ<Q(+~=r*GlDz-8!jL7NO`}7kyoPT8Utlu=(qoSUgDkQ<2PQFzTEpBvX1l6u9r3O z-gq=D@|p;JNttJ$@5!{r!Z_GJe@qAsZT{vRC<IKr9!kpbn$}GFmMrJvnY`gyb{q;E zeP=+bX)zoUyIO&vat)<CRnv-h_aj0gf=SOu&=*l(=+=y1T;Y0<Q-r67LvqXuK0E#M z7+v23VCL>Jb<QjYFPsjz$89TsG_4HWa85WMfJ$S*P@yj;`#C+U^2^Bba+FI&nqn|9 z$nNYy7@dpgCfBY=uHM1q%9v3?AN~_Es`?0IS)FT$f)o|;@g9q45bo^I5$y@teonts z_@#%|=tKWe0=LDflZO24j|*f<2+3D7om+E4NMvIadG6}4Qzjs#u3BvyJw#?UF(iq% zktH1fa~n}5dylY-ssEgQrSU5WyZ^5vb0~<y5-_^SqgwZsrB~G52dayvF$1_5Sk^kM z+->`(_x`V0Pm??{jOb&&+tufIMcvaUs>J6%IlWXRa2TVHQRXNLK}wz_6p=|A=zDt@ zDL5my=hA`j^6?>PEyccOr`-GYwEc}zAtL<~&vaKx)tI#qWF7DSaZsOm{~^e-!m5>K z%K(-BXEvA6G{j1Z^GtuART$kYsH)YbbTv=>sf_jamJ%xf+?Vn6UYBWHK&Khs`Qs)! za$G{F>ghHoy53OUiF>AXv&Okhm-I`K^$bn>9VaId5%Kp3#u&<}@Bsr9k-rW_9Yj?S z8SlvEWQ@n`%t>#UqDklTZaVlpYYWaPt}(Is*04AV&CCNHJBUpXR0~UV44zcLOElKE z4-<i1X=?&7P^0=b#!%zCyT6ym8ejr?7XpRXXuhXXOxp7UyS8+`DR|qo1QAzoUMeyA zfK%iss6eQ0+=j;qlhJ6BR1dv>XX5QHbhjO!;EwPHc@4D1E4RB`-E>_q6;C1I<G9Ql z;S!|q#yawLTZoc|_p=TXFB2$hv^qnHPFfq`iOW(3VMxbq40Q^)m~Twa^?@gO+H5(c z+%;h*y#P4S1Ue*-K91E;noDw9iJy<3TP^D%2yqE^EFja~SHGs6fC4n*=M~7X0YlCC zU(o|zH&j9%$-yf<_4l@m1G6wdcTacJqt$<fFv@hXr$yuuhW|Kh-l7S!Z{AvO!nwH- z<yvKA!IQC;F&<Ypr#4rs!cKeGg??6R(-Q=ny$kD05;vSgv>EsPXKQ=nt+u>TAsb;6 z&xYj{@<#fY4mHk^;z_Vs$UP-<*?KrJoxNs9**O0pQ}rco0>k4m_<t9c#DtPanO#!% zz0Zl`D;`q7OZr|a8Sf3yJeR0CERyc+m!e#JOphx9+F}=XxfYsSTwKGNbo{*l-UwVK z9LSa(c&H#8EUBTy9S|)mc7JjiY+8KBlJ|4C2sa&-Wi5vwe-G~GZ){-S&Th|!=!ZbV zEX`;+N<yVd2{i6*O@?X3dEY8yj+0GC+P`8UFu&}v8|*YR>zqU~ErxbX)ndq?ItJ$n z#_x7ioco@~d~5UiQ?ntYcOa4uk9qfV)+lrn769>TDpJ|^;^}w#k}IcV`Y7;rwmr_h z7*-5_)RbI>v?gu7?PEU@!v8=WqDw5=3C2i+wzprrZf@&?%bd5NRPZlrN&8zib__7* zAiA?p6s3%{hGTb`y!z`&6fK|4iaxtd<hVht$=0Eak&08cyL!Wc!#4$nc?Jw6dAs1H z6!cbsG06zip}vmK-hYqwvB|B}_0VN{K8|lsn942k5+?@0*@a3AQONeVyopcSaM^@q z$A>;H1D`^{Vai>TOd+a=Wd&7u5sKx9iZp~Ng8!u>VzCR*cN8pF^Q8Bc#h1mmNk1=j zWA2;VAM-{n?06gbDhG*2wu7je10t$`sc>d+FN4?L`~HUCZ?A#S-q;hh10R4Ma7@6i zI}&iQCib6(COP7sy$7lkD@-u=yf@7j`3_!wqPLU*WUJJm-B7`LY6d+1vY4n~_ftBe zH4Ziq^9}=U{~(c7Kt+dXBNf1wy)x;2bN+J}2w&|kFKBe49Y@4H{%0t!qW9U1cjtNR zoT7V6!XSX)fVuv&D%d=R_Y|Rrzk;tjjVkj@cU{P}gm~&f<7%l#?6`{avx_%8^;<JN z57eH29af%S=j}h=v1yA8X_D*DNk0Y3C!J1WxO(s}UBTYDP6<x9PGpP-tWZVx?>#?h zZyG0jlNnUVSb^7I^7Iv`ca5`>pSpll(8xz?yM)7ZoMj8&936}>Sd2zXEWTWBYZ~$C zlt9z->0?$1KIgmGfS@1{CTakt9G-LA`?6!TZ-I1VougZf+FK0j{TVSEjg|D5CFLf* zyab+Kb|m=rPu0mq&{R&Y7MqDoBuF=p;}JJPW7D+vibNt^rS$<${tm0c6dT2>)wy;) zMPNyXz)d{21LsHIH7rdtyE8%IAU2Nl59km2k*>-z1}^n;iS|KQxSJkHr7?<EKl{~* zrHD#AXm!pLGk7UY+oaNX+``e*4W=hfG{xA3+k*_$4rByVEhMH4a&*ODN;zo;M!fuq zP&53fA--S4G#uB~y(R=Q%7607O=D_g^l)I^Sg^m~fzKYa_CW^W#ftz7(IY_80a6I4 zgmiNNYy7qVD9Shb#HaB%x&~qROevDfz}Y!iF{36_MgKzVe)yM`dCcZVe?sbc=a&!F z<$9DG7hH>Z=J%4!F1(j;AjKw4<6iDw5AXcfyS12K(|m+^Cu9dc4{m+D`(4*1d=w0- zb|t8&-#4L~H{pGi;bV>cKFnc&CU~212Nev0A-sg1;Irl@bd2YG0&Cs{_@|@xHshrU zjz7^NA~2?1wneB-9EI6n06b4snbU5j4n8-wIf%k^_35uQ#b&o3SwUbJ1}8pu)OCEt ze7wt*ApPNp@W9h2qG{+QYCn?qo4K-zlGI8@ff5?}Ide&~In&oTwC8|$*ps8gmeh^f zhCwbLM%rT4mIG%q4F<JjT+`DTdgG1cQJ4uKP7pC`+vop>Nyz;0`7SvfGD>$eZ3)Pl zzG(07c<&MCuMX(N$OZYL7cVUTquQ~cWoXRiDNH3=$IuTbMb~^csQiq$Img$T7P(7O zjh`+__~E^$>Q&F>-G2z<ZTYyaGi_uHhwUofaBrG?`HT9t*&(c|57xHb+^>>FY`1j} zr>M5uB?EO7Xx=AldZ+dd*MF0@@8n<lO8S*|msWm5`2T|e%T&@;6Gk_PzXNsd3as3* zGnMXTaQh_JO=JGL?2Gt45n4eS(9~!Ovjxia2R`5uJentAsd>^_@;$k`u;yYJ_~PQN ztzY`s#8KEXNdq^AaBgJEqQD%rp^_8Q0t8`=5In;O!-S$ygQT`euUt0q=VV6JCB}nB z^X}XH%EQlp_!De9@5b~E9#HE()mzvz5qo3|p@4fDChx#3fwDrU`p91x2^j*+eeyRF z)F5)>vsMGlDH-i8q$|;=)x9Y$>&i#BpT<s9jJl9vG49-a=BVo106Y?1`SF^W5@zcp z-@lW?vG=v1nvDDplCdh}IGweL5aTci2{T9QQ4U&%gvYTAiTtJ1!<-2%qWm0z-Ix$( z-#{v@@<p(5A(o`1z7EaG$lodQo=|h;@t=@|tUwitmp9R{Ho{}R3x^W}QVvNXgP8Er zYQYPD)`s{kkYb)(m?U3%kGt>%5!?OvDU6(_dGF%6eSCi8(&l%t?pwkMaP*nMw?LqZ zd|qs3ebHwymft$q`bFLgaU<wr9p|cQ9EX^7x7SICRi*+_9r}!V<+Wq^KdmGP&7c8b zB#d2`9`8-mO@f;ldyTJraC~coH?=@VeKCR>pI<_aJq`7JS+W3;n{5S-{Hqkk;I%RG zaU8T*h|O@HILRT}KWq6EghcHcrMg|K5}pMambgGZfxX&Ev(jeA7x)mAEGvOI=tZX~ z4!s+6hUeB-kHSVYt4b#Fx+t>(gA~W7J_5v%xmTq9Q4#X(DFrlXHux+ZHT%9#iiRdn zw}?Np8XVgCQ+Z^LF~tcTNV9|_QisxqtuqV9dYH%vfzklAz29mVAAWrCi?jNr^`C<J zfk<f4chX(a0LzQn)cTs{d_?zKdA+>`q@gxynx=b6MHK^L@@MK{7qKYV+<S@*3l_KY z2c*{%Ds)%}PBskY*-WF2?~T{b{!YUH51r(J<gera06zVBN<PJWT-15~@OtFjA1x^i zofBhCXzgpvakW}$X;d$ktTEyLt0^hY|KC}UoVu0S`MISkA>mkAw*S$QV)8dpJCBa| zXRjD_byY=x!wc-@gomp?ee~hCe+IRhw|OZk-`*S?9=^jSltkZs$1O<&8!ofYG#CbP zUyEHrVI$AypooKGY6{BAE9yN1bL%}TRt1-txDhFfSv-J}HqniOjwcSUMb6c)RECG+ z%cf6?LJNcAtiSN6j4CXEil$HEYQm)@?!L3@ki(~a3vO7^#ypP2V>RTDqSmx|{F5FD zpd!GQnJ&-&Xvcgye;q2j;ha0UH>wXUB1+?0mdaP-$w`dJMDkd!!JrohKjjO%ASfSm zqG=r0B{<+Wh~@c&YD7U&UifJ0h&a2AZbul*GLNwAE?>Vca^LXmwZWc!H^4M}7F*Y# z;|UX}EjihrEYy-ktu1tWZLQIyt>q{FbV|#`B5e{usO9A@j{Ye>0FU1Y$@2*|ES)oT zVG8_4IaGjGBQX@~M>=Sj2`mHyh)A7}=9`xHK)EPtcY&<Dkar%utgEecPfq@+f9&w{ zukPHtpWa&0N>zOO?#UV!=$HJG*?7>&8>n$>%c9@pSD`vPuPMC*F`l4Mw3<xorLqYe zfErwapGKepQN4IUnc^p->5{Liqt_4<5B6)B=%`#=fa7z_j-!McW!;@8AKZz`-=4fn zWhZ|y)r$&D&eM^jX;>6IgBOihW#Si0k}V5%>O*bxINcI9DP?YDzfW~W71_X7pF%4U zR)|tb%E3IYR-K8AY;@pffp&6oprTUyGX8MG@~n(S6QJZ+@NdiHtP;5?%E7W;J)&BS zk9D-+6T?D5mYr1MG`=gz)+2fkI(=9RMJdrK^q+(gL>@xG=Q@#Ngu7ECe#bvug#;5s zbDYQp{EwNlc%sGpTqGJsbx00XV{wu=-9NZ#X3L|gC<K*ZNFFZ5{{W(q0`7Ai$T7kl zNKE_<;LlVdA^2yGQ+^Q->sOi*`?XF`zWZJR+Q^YHNUq|y$BQZK&FqxBi~qQX*#9g( z@izN%5##-I(W_NR2w|8ujc~!v+6XbC0l=pLL51IR-HpW$?(eW^6zfCdEyU*N!b60@ zUBkm5Y~TTOkG*{yCbM{M7|Ubb9B;*B0nc}#2lyAn1DtfQ03D0Z=Xjdue9Fx4aI49r zVV@6}(@SaF-1H4~lKN0nb1r3PCE9L7J{BE)oaFp$NgQ*O$T0E?$$1Wc=Dgk^F&F<_ z#C!&uS6y>2(ZdZ3gTH@ktW%P2-wTdNVF>!f%ibAZEI7N+AnD4}n0jr}U3iHyD3z8# znmh&O6xF)-Zu_Q#+N+E$-Rp0qrz_Oz?OLtH_6})H&F9!k8A*O+UsFE?>`5}J&S~`U zc-LJ0u1E4{fMPrPAg+Pqbg0PSxCZPXwu`LQg?{_?8$mt)x)}X_fbY}~J;&29F>?_* zd9d3bi%M5+7z<9SHBeed8@Eve`R>Qr<WL^c(dz{KnMospc=#E-OW#JIf4)tiaPV0v zE#%Q}Cr|+I{q`i4@H4a14;5SVf**ej5@%qAXaQ#$!PQ_R5YLKsmS)p2yu~g=MRY@m z;_FiZ-ZUH^hU~3U!bvrSJ-yflj*dY^2E{e#_Xu=UPby63sV)>ip;h&^V3#`AsYX}3 zxjA`N+S)$64h1JHXuI?Pq;Tp~j`s(bJs}OkcIsxJw}*Rc##2y54Spc6NDJg9PHIr~ z9jWw#_$gKQQdeS%>jsHo(U>a(Mprsrz0sU<X)0PtDXfZ^Y_VFVOMNir;J+;Tmd_EN zTHRZu!OG;GRfp{ZcqmYzsraJ4A$wtgl{MvF{>`m)m1%)y`@~=9MM~QdDY&=)?%{Ft zq0C8m7yK7%=J%UbtFgIhr;xE*K{Acv)~je<f?$lyxS=c1x~tW}dHaW$dqrCUZ<hfO zzzZ$b)vkyAx46p21t7bNVT~{dnGz|BYOPeVLQuwf7|F-}4UA<{hb>v+OLHGrsYV2# zu^fE5Z5o=Am-o5_1mM(P<Ewv2D}?{}qcOaq%?@UJzOn|DciXH#t$UWGBdeB<0syQo zLjyLoy9`6S|AL3$b9HCoa>dWTT!g!X{Pk4@-^KG6hR>NZ+0r5iB`JvdO$3lXqE;Ua zu+QN^U=jb+%JGiZecate8D`^-Il*|sX7O5$A`GTDEt*dwX>{F-Z1)Sn3@wai48jT_ z`Pf&7P$W=~s{N52Ri@gBk4bqo-eAsw=^JTjrUmccUyesBFq3<MY_<)I{*!W~DY942 zo-<lc2^_~hFPjoq>0f1T#y`XKd;8HH=A~(w6FP(6c9{sG{8*6k`Kp7hGC7YD_Mr?w z(;;L+(oP)(*@Qxa`~Zmn3XPLdgx3_30|_5{W!{5cN+*|?IM2NE*mE^Aklh^8eTr^A z;^S<TOo_~UbVI3HdZBr74NlTu&EIfmN=2_$BO!zw7)3M*D4QSzxEdXvmEwn5&%;Fm zga^Med@SQZK}FjIL{S)6fgqhof@9!d5bNalaXAge4J6D02z^8-wrD5Rs=I`?i)wX0 zUy>wJN-X9HRm_z(I^q!m3`YpY2y_!7=2qW+iQ`-mm`kEyi-2_k(qSZu7J;VNv|3a7 z#XTpkB>DZeFU_8?VS@!R5#&E7ufNNX=u_9{zR-8jzsy~qqD$;nnr>^f=KDe&S`7Zl zssp1vKP5*$TT~2+YGLQ4Yt3>n8fHWt0v~D&d_G2i2^$IdDTvu+-GWQwxgkL(TF($v z4Wb}>CxcKn!HD}9vWs-g4xxR%*02R@ENJ7xD7omu==@phuV^uKcYd#lZgcWxBrWX= zKFgMcAm<^3kvO1+d2bz|Qf<rBU?q03rg4k8IiX8CrvqodLmMy^s3$&5>i_{A#vOm@ zlw+TbI0Xt{4@^5JQ*XrGDqHmZ!vfFOE|!=T6{_NTQ5M0KPVohr_-HrEZkR(PXJ<Bg zM^33Cd-;9?3^pPpQJ5kfj3IzxyM>}=+6n+cLqtFg+*+2Jo?dQmleJF;;D>!SEDPR9 zbGZ)|W`^}4plmAJe(Sk+iG<~J9z+0<iV6>bjjF<4kPV!1<42lbuB$iFU5JR;O;S#h z@##Gm)ao45PvPu`Z}L5_AJ>}|6{+I3p)4Mk%EagG=cA=y$%cld?D`G#`qNnIQh|v| zHM}{OKGR>gD-=8MYV_^Gz^>73_nt93V=EAO@_;mC4WI!KARqvaJ2VSYlHHbtRRjeL z&5C9lY;0?FV~hl;fk+U$ATmkPP7od#i4fbdJTygc_ckOmf59$_DakhYpFk@hoo7fJ zj7U!YzjRDWvy)IxOM%(Vie5o#Y-me-1|X6bm~M1TB}gN|2nZZu#_S-dNN_jwCNqEe zJ3c16#NdAdS7Eeohv7O0do}iUVPGfqkIW53o$w*j8u()m$L)J<VEMqxB6}7~BbC{x z!=-7lcX14Ad0>Yy_7G8H88NSwcH;5<vOS4YQoX$<4v)5ntm#i{vpB<Da^n`MX}rt0 zrFYoJ*DQ6)1YXl;(-FI$lXc<oL0Mt3WP7``8Zp`~jLFU5NL=c;!Pu<dYNKiP7QP}J z&Bo&uiWYmVg+Z<k>=edqH!^ESENW!C@WNr)wsj-Y2Bt$i=5&Ot>4!?PjK{km<8@NU zaF=mk^MsE}ZD6upi>>Bk5wl*l8;_xTomcx1+}UYf+<PlfP+qCFpnG_%bHGCSLk*|y z%8#_*LwmZpX$;!N=<8X|MHwn$=HTq6NGC%G5COxpigoKlOcH{ZFIgvfqlOzraWOEf zEYc}MdFoqfLXofnY}XqXsh>W0xrZ};siC5xoGK9>glb66Z(!QdX0%Rk@N;A;g?LbB zY;IQ>m%2E`Z_F%wap_V`#vcWibWMhGI0hw!DV?WEnUTw`bxBE0TCSJ8uFKeNicy$7 z(p*8gSk|xCi;bb49l!MYGhvvn<J3IxE1OnokWfq{%&}0S18G4z7=0<rSr8L%z{Q_J z>IY!$R8C<_NXY-^Hw2G+&X?-{PxIm&H*bxcdq&FOl1sT6z_lcUuV$?LmM?Re+^<-@ zx}698IWn5r+~4`rZ@*e<BODX`vh}_LWaMy+Q-7#?e}QH8f&~_ny^5gKgFN1kBi&JD z%^k15dts>wx9@F|K_g(Ia&Eip+fOTdtXz?<;N2$q!Flew=;%6S&<DDLiyQBrg*yjb z+e4tS4u0Z`Cm`sdZxZ=xNiG<bRXR2{sI@y<$-`tpX3OfnyI8c!CKxf{Z@#Bhd%0uQ z+1)p9<a)Yl-~U&e=3yV(=MEMO;X1joHV`0WVnzypoNhGC?CC<05NbvM`{ZT}pg4js zz?L4i3_$loXu>Rz0#M0*I*%a?2*R2x$85%~9&FE9e)YUee(rA0^0A>4Cv0a=oICkm zJ1>`==Y1!`)xEwsRgSv#Bgt{u&@}56D7K|0DV|@YZEcHpsMHK4#Kb_e1U1lHUXu{V zuhO)xcfGzW{<3T0cca@s{*@Je^Q`7Cyt=^PbL9?p{@uk7?w#xpYM_!`BMzjHb)gcM z`BiY%cM@uB4YJRjX@Wr&0}fN1pJD4zw2fu_9A$yYgII-|OJ#!{TW}plLm5Zw5NR{h zH~EZbb$2f=XDb&r!7J4McXT!;9qsU-dEnsLsE5@%*zq2&1WvT)ni?7?uy040UoqWc zH15Cwh}v&}*P~z`0TFG4ATS}&f9@XPJ-Q?1UGuOh4~OL{31PSe7$${q6AYuq!Vy3i z>qwtQkaEY+B+({OvNC3s3z<$&zfFifdGtGKubv`1dGC+(#!^br!nBzQ+8BJYzyAPr zDFjL?(hAW5p|FwLB*A(S)6H>>27L!wgboTAmviUJQizXWRj`@EaShL7wWB?A+NGk? z$hm36i?-uJ#Aq$HY~^Y+s*!qlmN*Q|t*40u{P8)o1T7ME2o3aXcvia^RO&5TV<ZS* zL^@v-BbpuS)uOL@GbdUTud!3Xr!<5tvvuj28*Ei_&~g=Z?+{k24tZ&8V3yicSp4Mj zO`0$IJtZMl?b3mg&&^cWZlSP=9*w!4p~FZnGE#JP%?GgfCZlZ#rxcBN|E)Vln1`NV zMn2!y*F;H?U)k3;Lq<(91!-p(KS`Nk17{%(I80ilAL}{<WTkGeJ=@6U`)3GGw;+jl z_eHGAoX2scdP4y1L^=%mQjT)Nh$;#$;+K^o7l?4hIgFlQIKs{=vjt1%_#qtDW#ZDY zEspTR1CVCaK&30zouwm<H+H@X&Xh(DNAsZoK%br)!eYC5_CX4eV0A{10F>g5LxVyd z8a^CFA0p(lvSN&fW$YZOp<u}@$<UIlon<^o{c9jl0e*}eV>nk9PTi6K{!R&gVP@OQ z{1v*$n44&YC`2CH(Ra+%Rh^l6y|*vt>}y-b%aRVb`SNP|?$Htoh&s1t52_lF0bNM| zGeFG0vR)>ax{%Y?d#^I3PDnV@cW<_%JHR9<{ian|LH^5ay`^W))Q;Y0wF@9fJ2Rcr z9Yq3K^8hx$vQDb`a^ON;z4y>-2JK)u3(N|0n0$1wWYWb&86NXFMm69t%hsdlcJ}R@ zZiEz6wev<Rw;e&dSkeW<ipaRhQN~%HDH$E4OJGu%!W>(Vd}|%_uCH6j)m~>fSvG(I zD8<|H_y~mL6Ob{2vj#J8?$pjnX8{hI<FFqVzyne{J7ogeMj`DCTZhaA2XR5hQq3_F zF;=~Z7EDvD1~%BQ8n9(qgfWc4f?<2Q8RiPhP=*;|J((9=@6lp;N;JnIK7Amx4|5{( zBHG9>4l>SGpr^4W0FOX?`T&v+hVdeqGc;)U^Rev6^tUSl|CoEIRu=?-Hl*+4G*u5e zWNPL}6Q|EfRqbGiC}3a!l;UQg*eLQi992!YhNE=GM)1ckX^?5WhSb|K<m+paqL7HC zgADxy=qU7Qr?^x-7}l-ynKDIkjsrSF;6r|;`jY#m!e%To874`;_jrdy8uYA#WVNt? z)19P|lB_@yfHMDD`#2m?@8DYp=m1P;0nq8KY~$P?z85t=ekhJ~@yXfQYF&W%y#=kt z+BZKDS8-z2DeUl3nQvq7t(h0dSnK_Tp@2wXbVHO&I1WcdbUV~+2v{mK_p^`~>d@xR zJm;=%Y@sX0JVzhvSeqQoETSJ~DPo?E&oUDL7EuRap~iq!`SzzH;cX6R7lM*871oXH zvs&3WY-cA}o@#vxt$^oL-7b<e5x}Y($VqmpiFu&WB_VeHB8<;>E+PV8H_ms~!A&hr zN$R5P@9<zxJhuUP8hiKT33u^H6Ur5n)z}ywu2indvT%d)xK)Q!3`(_zGsj|fnp;4b zR`k?c|CP0T^e|8K`gAFCadlkO{d*RDg(2?k3sW>m$6xj6?|0jERDaYyb5v_N+y#-` zo)(dsrx!oCmA@?lD`s3yJTT`LHaioRc=p=-xo?79?=HF(9-jkC&fT+SsPmIvno9t4 z`-0v6TXZcr;f1<*p7S$?Is|PqFn;c-3Az|Yh1msDE_rM06hp)kUIb|%7(4m&1d>b? zEq4O&`I<3OWxu|Tbdo$$s*fX`Am{cq7LiYqdN!5@l8^ZB9Nx)#)}rfCe7F)MPe#gy z@pt)3Yv-@m%Y(yOW%)nT4aISC_ozNn`~>c4nY0Ijsx-=QoFwB&;D+k-;gQ3+nX-qU zBK2%ckt+N2b)F+GLZPWLnRZL16F9;T>HcfkS7M~~dir_}5P|tCU;lhRIiktOkJ<<t zBtT9Y@{vB(Gt?aPq#?rfnrmpzRn#j){6k~o-FNhqbNn@OB0gNBXsZa{r#I9Ycf5yO zFrdfjG@p}g;y0G6Gg}oHGQGR1>*hP%s()HA^qkLUsufE98oQgnq)QjbPb+b91Gb@C zXamFgsLASv@li38=Z0}uw~<drqpVR9Su8;Ch~OR(FFlyg8;8-_fPk;;F{Mn{F~=!> zHMoZ-iUk?4=A8q;;$AEI|4tbvf==ffg8gE20`O0ZDVlrW$c5XEsfE#d8iri!dqb^L zEw@4P&*pEhOuSxK7pC-TouMAg%zPCWx9ZyYdGqL%P0_u-gVN`h2`knFTD&`X^Bqj( zgZVKdUH|adUw3!M7%DN2cTP<{&Exb19PJ0abO*N5DklciX=>F*hZz`@2fX!BDP#MY zl@D%#ipPLQuni(w9Awe*7O7E6+LH0EC10z=*>BnKxSi7-b4IE~dg!yUdoKs~?>A4b zO)|kY*;<JK(ZzFVs7P>Zd8g6E^a$-M)-6^{zY8j74cmNCE3$smFWW5j6h*5xP0$bN ztO+CFGlD6(2?R&`|LqDa>A_&A&0M>B;y+I`ouW!vf)!AtfjE*jRP|40<6Hng9}Y!r zpca4A#}!jlD4K;xG&Jl>pJ~kUt?3nouU^?+=hmT29s6ZQ3`V-Ds-pTe?Pb5XI&<Qq zUmTK3kq)Nba1c&=^~aa@i#AD?)Yv5duU}8sKqM&!C1Hr{eVQY+1Wv9iynpZct6ek# zTVC=IO$3rJ%q?AYbVNr6PtK5>J889_vn#4OxH^+*94`n%Qn*KiQOUM4HRq+G;bO<_ zLxRp-LW5M$%=d+cs5{Sl2O1^O4qt5g0ZkS!4dpo6MK=q=F*yBi-tUn+e{G4aa<)Kc z8H__%=t#l^K_dsJA@=UvxG&w$N7(J{qH1Zo6fU-}r{hpjYivI6<KvLVNn5PB@72*A zNJz3}W9<miz;xax!U(Ls6B_z@TfH>EXC(<8E~>@0F3P)m@2Lg5U#q{u<EXeFe_5NF zCpIwPrTIS$+Jq~I2M2dCED`}8iT~IiW^4;e>)o)6gLXlfLo6m3n1V#dm-!JRGSR3* zCkiFSp=9pY(0B+<BoSsrgXlZLF|MinDeiMur)eim0CP4`U~_=fd8w7M9h2>6AV+#{ z&$xvtYy#Z}?qE`xBW7K@ZGt`ugw)bqT7*tP$P`0_Xn>Y8OcT@vQI-F6)xe#FG?X+w zNYV&kZ*<38(WRrsuxBmtT1T#WMk1~aMbcR+fD8yy8NH8#1<69QrqAx?cr`V3AuY`g zw-^&86%J9kiO-%Zicu4_$~GrLSHLXsJ;TH>A|7d<+MeEvNalxzA|aCSBA}PbXN)9d z!0KF!<+v(XeV)*8CDN{r(L1Q`7#MRZ<zLBHCW#Wo(=0KYqi7UBrlq7FP>`@4!mY%K zk1v1cQ@g-3Ipx;a?5P*`kG;NM)7sWX^E5&Jf7@@BC>py@Ir)$()q^pbk5vYGweE*l zrPN4%u(TnEJoOhf+xeJDUA`3+htkotvqCJ@1SkuvG>l`hy`mKtdB!GMp+aRQ&o>!n z?Df_3bu8nfSWEciM5cx#{md>Y%?K5sNgpTlfwMM=P&OejNlBMp^KXBpk^~463V12J z5EOJ>Q3`C}XDiUeO0xYFj}MbV!VMc{vh-2};aQ|~Iwryh`b5d%`3<m?WE1M`H|(N| z7{Wu`O0sK5&~^h`N>MSe<3u4U5oU!Ez%z0Xd|_!PXgjA^m39JxcCz|1s$sokZ)&88 z@8f8@Iy8A1C%1lQpP-;snAq*Llsd6-pOY4FA2yUQ%<$UyzJ(~$vjqI9P>vmpgn?J^ za~#NS0zNF37vR9_K|hC%j$Stp8A&Cak<%?Odh%TzvS&?cAStM(uk5N1Q~cTEKV2H9 z7(3bDj7GH7sXmtKQ+Gy<BodhOpNfp){^LhscA(_*HGS(Z9Nl&M`Z{GpJ^i~WnIS<* zir7_(Oh>A26C9xtgDkO)Uf+;;DbzY{Zs7+_;NXmx<t8C+KrT&15xC1SP?i5qVg@<@ z2At5rtZFZUNSZyDZfF1?s0BVDh50C$kiq!a-UtNA0ikp~1f6>iK%suPq*6YG7Y!rO zd=Xycv<HBouA3-s^Ls|YS()P$pCMcYBt!?oFY>&)gigU8{|9;BA{hdTZ9dNaukF#- zkb=7V0q{lW<j-!rU<&kCHy(M=ne@Er$9I^U0}SlzaH+z~Z7(7`7i?siw6XXs3Gy3u zGc5ji)4%24_v<&&h-FJZzQ3@kq=ea8&3m**sBd`~5^B^rJpJdNCh_7)Vc^6Q$mAGu za{6O@=#o<L*wFANGu?<tZoGp&fn*HpPP=j@;K<FCe{`i$oQ8%@OY+60;L+ec$M>>A zYB*7eZ}#_9+&MRS?4oDw#*GyE%KRf60(|QceEIYqc5tC<V$>V`{S~**O<p(?xp8fo z33YXT-kScgXkVsqSyIv)P5rHBukO0=s`M5Fmi(4E2<jQjw`q!%D-gTjTCRI35<(I} z@TPlF<vj4PwpV-_51_28w~5m?SS`zx&5UH6)Q&2yh!qUnJiu}Ao#94fKja%FL}VJb z3`)ZC;J~(>{G3>~pxog4A+UpCyc1u~KXzYFOu8}ua`a;s(^Nn#39*J3!gL~Rgr-3u zL?WA^`d#L&oRkNZ%y_F`dvwE(1|iz~D&7J^9<CuNUr0b4Q;d@IelI+O#-}Q6|HVL( z|9*f|^Dz<Ex4eu4nM#Y(`P2aRGOpnX<PUGaK&j>^rYR}v!wV_AUJCFd5LAU?2?|Z0 zZDRWV`_$OCo;ft7(z7(-m{@FbjN^-f@ucV!eD08v;{Ruvi!Yl}?uQUOVA+`na5F6v zIaz^-6q*y$2N>!_GsJ-}a@|@AMtk)HCQ0?<BoI8m6M9xeumqK*K#3{@?0jE`me{FV z-P;XJcix_<ehHF^w?qtdgfH|s{D?Tvf>^=lI<W9)Fl8{ULLvFVv4IIfXschiJ;i-R zLE@R3ng=%FAP(y^ct5DAeVSRY$vtJeNY!SF-#aiiV-2JiKlqk_4xc+%e$oPx6cUOq z0A|W><lt<w&rkCW$?q=+?(GXS&fTG~b|KN+Y|^;8-(aG83dBwcn>LqD%nY0LVe0gZ zpQS7mtO!<UpIX+>=mX5&_7B?f2;*rHPb(^nT(?bXzy-@bp|o4XRVZjR`j~N7bCz?^ zAxxc~^>L&HHgOgTiU~NNuY8xqc>pD;Up8|MfqPUSVd#&|{aiC_D1QV}wGg6=7sWtd zpK{wlk8WXA7RC@1ha|(VrC{<V07KyWh`RZ7C&Mb~sGlVbzsODQjO|&Y_YI25-vHp~ zu)cB;EDj<0I8l+=?gj~+Z=oKLiNGiI4z7IykTB8a*FjNLI*&#WPRlnH>$^!otNTC$ z`-U(AA&AvC;WV8ge4i05iim~s*8pd-{wbw5PKfIT-jR?p!psXH^zZ$M6i$@Cd`F64 zzYmqPG5Q(3$RsK871O(Hm}jcAVkdrM;Rshd)!}B-$uxb`cFvBnwXr*kxblRCCwcSK z8p3tXrQd}*em4Y#_dcVHdRfi8XH?7QW@V~_o08a%Y<wZz^81z<E4doKoas<Bd8~qB z=TEStJXmr6@@>|sAn1BSlzeiM?3CHQdKsXQO8qj3^_~uQ%qGkStmf9OL&?TTw+p<% zyD>L2U8;Xe(5!mwpWyjHCc&HHKRNdEgp$yqvwn-XPvY}77tq1h?QkA2yR@$7Vp2_& z&5Rt?y;pENrLu~Te5$&u-xRxksHUBKV~u0WZ*eiTlV(XZ>r99>Yt}2TIQvwGhty1m zcw*<)E+#lT5Geu5#a-QZB3D=2fSRhjxn|-c7bjlr?%HmKyQrpSk^*2(8NUp2jWFl) zPbPT0pB@I1@wJ2b7{PN|2DRbX6my(IU^rODtA}C#B5bGenIj;8$n>uwKMT?$6eQpr z;^2<*cZ@DQ*2x*t5aH!FqX>>91Q>RHGwUv8u)WR2y?`d(mb$F0FX-4Yq*#68h-jm& z3KbtdK*N=NWSp)hn7SZMWgL?QAX<20+>=M(=IF6w3>dh_qJbw7$@gr?(PLaW!+4(D zMlgPoU1tA50;P?egh%=OpfCV<6ooG0&HYM(MT_6$=ewiLFeNf1zx?6X*i!_&|1g53 z%ie>Ylc2fVaa^Zf??z44^T(x9U>_6U`DjjVyNyWjzz?(B*gBj@0UvPQxCrKzrB&X0 z;t7iTdjqyF#K)yS6cI7yt#p5-q<In}BTFyL(?~zd1Q5V&1Z$@p^YNl0^E53dGkDUZ zX8)0U-@JPE?BRn41qYNn7B##?PfvR?Q@?DyQ#~|<)OM5m9Q_P08nLm+plKJ!`WU?j zWdbbYB;O(%4ubKb46_ED)dbki{usYWUU#de)}h_VFw|+(bnhP*cJqX+@8sx*H|9Wd z{fayC;x(5A%TlQ9xh(Lg^Id1Pu9PiG$~!d|Xj`XP42WZ$(Z9GPjTFn7H$4xsUj<*$ z9*df5&ujD!b~DeWsU&@OsYz4Xo;t`0#F+?5JpLjKv6i^Oa}nVrMSnN+KLF^6LuM@x zHb;=4YTzfDf*f`<sqcmXlnwqxH#4l4K@^MevNscsRMh#$q`bYowfOS*f3H8Zgg<c5 z(}T%QzR^nFA8uZ|YtmWtaNf4{UKy{R-@fI$rq&%N{XLkh<+(Ns^>6?DKlHGvZ97aV z6z6+<^UWa95Dwcf5>+S=C%Y<)FR#22CeOuTQ!HTS=)0mvv9Sf_Dq1aWtH`AtohqgT zAD%AyEr2|zDM?&i4j95rnqDcmqTjp&T^<z~H=B6uiGWmdHLVhKMZkY#muIN5oySH! zb93^EDq0P;vDJHPR5@Qxd^5>S?S}ourl)yXy))84kaoDBc5w-b<#~JXy`MsYnGJ%V z-Dq$lh|O%jBr+6}?E_&9oAFvcLy54|;arxnAppwuwtLU(GMSRCu#JRFcscR*+<{+7 z0_h^T7&Ka0&izPgh+A{BH;D+c0kbvl1NslM_F0;<SocDr#7YB&jid(cV6)2a%-*C4 zlBU?&mLoI?mBXJ#?>sh2%k4h{2tZybsG0CN&LFvZ^!N%oOTZQkr;(lje(h+*aPT(W zWMBueW71~<?3;PHa|X;V>$1{D(0e!;;o*+(Juy2_1AnDV%jU>&aN9giqdL4a9E<mb ztLxS8W2r;k7>;=f_o*jWIu+CD7-d5l9#KD3HM&3sSk9z%#Rrv{K_Eyu4E@|syll6> zWV4gGuSoWj+Zbfr({W+VW<Os|NUGiO6)T=c4gP1;yunf(S?UWZEe#HS@#j%cIW2ll ztNB?&5jWBHTwgD#%VgTWaG|)w$MMn6G1Y|`zK*~C2lMu#mpY}lI4<G(em>c)+@z|K z3Cj`GZbAWqG!SqQ(e%U?rECa}s1u7$=XEo|ycJYM?*NN~%39toKst<Pa!MqW7>Iof zGO(Yx3>}StCuL!_W0p1xY`fryZm6DDgB93?)o~{GA{2z;35p$kY?Jx5fPMCcqtrnf z9+oyJOpq=tg7Z)nY)Y704QmBilPO!4HhC`rU#cDYmS8~u;GM8;{0Jn^!_9ZSwX51y z;wfggC`IK6Eq2jOdO>2C2vaQ(8eUcZT<n^i-JSR&s`*udI@SaRifLXLZ6p~pMN0&? z8XgR?D<vtKvy5O5^@C(SR%LP=!FQhw%b6kE?BIvL#veX3x;{OF&_M1wDz0NOoQnHD zG183^?;lI=hfG22ol&@{f9@U{zW6f|NDGXKumCgw?Hd6FiA!}`x9<N)Tv!z1x&%00 zg0P?Z<&pM|Z7&HfVo)<6l9KjRATTW=d}IICohK}a3u{BIRs+!{{0#K@^Si&8-~2c; z^IfPwpMCTHe@&lMs8i)3B-2UqI-%`aR7z5W<C}f)|2&eic2iMqGz{T@<9m1e-LL<v zSWLIdBapXK#p|hrYn2*}I15)dmwx$b8o5{{%xI6WXR22Fwy=g_1naNE1PInlA(APL z+|t5g!4hye!{n(T2X0PnNryF9dL}?XhnP@M&MQo%(J&woG|`+`i0UyelC4L<@Dtd2 z))<2bH9$4$UlGyJ_NEeC=Pz%nXu4521)-Qu-hu1n2;v^uEDi>_rG=-%&~9T($js!n zw9vWC9S1O0hC(zr8`4BGG(rwJ=g~9?{5qkR(})@t#?EE}Nz8c#V_~@f3g*>fJ2@^D zWiQ41_2(gUkU)YY!2z97s3zScoD!id7+_*a&k3)IeR#WAe0J6$d8h-E9=fc7wKq~q z@!1;Ld|BFDI$(qt=c1jwF>s}fro5VE9qcAvp$j?~#?i~Yb5`lq)fsJi(aUG$*6sqy zJFI5acOuYKEH4cGqa51F(Y~bK1;J=NK^UNWXPHAiHk%I5hkjWtkffBSDo0hWN()#Y zNogpDa;XT}TkVki(yYYc)b=Fj@Nhk#qX0qQp-R)cI3ojIr+1Eaq`XF(KoS%hc~JSd z*i1#4(a?zjC}{d(nR5Am??1SIG7ed-Pi2*m#=GRk@zTmQPU>wLKcCes-Pk%X=5%We z#lmP2kPZRS02&7LK^j6}!S^44L_$P@Ejt^#RZMC8g=qgZYB`mlK%JHang!=BU;beO z$g^OJ1o(75k`F+*@%=;lu@YGxq5&{If$CN0SV<iJADken5hSNt$(WZ-gO5O*r)n(; zEUnnrO9{SwZ=62}lBS33ZeU<3bh3<3yFn3y`Mx!d{}MlFjI6`SHuy!wT@0W)S_z`u zGw{6!_L3i(+QVC0olTK%Xd9SL<UTjL?U_0>dYqd`a&8V7)>p6PJvtTE4&sMl+@MU@ zOz)dvI$<)CQZ(B{+i{jfSiz}U#mn;}v<CYimOmfST3;B=A=)b`c;w{g;qM&W2!19A z6n777F$Zy%nSr?P+Uf@C3NzMc$1k%XhniJ_7r7Y|%>KGIT!P)<n*=C47RH|A*b}Kb zi~mk^Pryp80r`eT+rwxW!3UZk2;v=+G&CwvfdKs~!qMv-JV@oE9L0_J31pjts9!R# z0#D?b*GWBR+QDu=yJw0Ksj!Ve;MNF%=b6aPzy5u-=3waZ^}dW8>foW@%%3pq%XJoA zu!YoS;+vCF*H|7oL4;luQ25a(IA6Rk^mv{ZbPzn%WkNa8teNw%?cP_Xt<ADR94X5@ zB1k*5CGTuX(WjcyKL0D03?hEH$tb^`4WQ}+UW2xIIoG2)BaWs<-c1d8QG<qmw=1Qn zFa)01X21>)ZYLkY4q`hwlMXonx?ppF<AxvF#TjXEK0Rdr(x{{{!Smzcb^GW;J_%Ud zEy!lp8PXPIQK&Agsf0cL3s&)88n0|K75u<*D{xv1RGomc46o9$$49=|F7=*QoVP5( zx!H$Q>jW~&(|-sPSym8#SSPT<pvsw>zS$Cq&o5qOPusG6<cAXbt;Syl3RFV!Y|UB6 zS4(pR;G87<GYJ?y_rAoc$cb9>;X`%Ih7Fr-b4&sH+RJb;q?t?RWzhkXi4J7VGnWuh zz4`0AX-t#h!z*grMU{fl>XHn{x$#8`i+P1Z&MHYrP?R{3Qi-`p5g(P-*PHR{ulr9O zCKuLbTSh$&CZJA324GNp|I$ErQgJDEGPgX8LcT_g|E_@<fvCO5t*qQqqXS);apksf zjnkF%DYa(1<gvdJ=f{~GV;((q_;1#IIimRH@TuL5J(SA%iI_b`W<jYXz401(?Umcx z`@yw8kBq#(Yf=HH>6$A8GdvqmvFOWjA1!lf%ix2{?=FsvkPlRMHn0V636y)Qs)xux z`@>(-R;K-6yj6m?Hbod^zXh_!x4yr3OC^QNzl?>4A9(T_hQ$v*Ek2dQ(v~lojZbHo z*zmuLi%a<)U^j+zZ#5^w!{@afzAmUeRde_K`^73#BcrSHQ*@K&=KcLFp~WPO3?m>C zbxqD{NqUMVlQ%9P*Y%g}7OB9CN-SrW=yH~+V~}?5pCaI{$wK}&4;N=<M%~Fzy`)%N z)K^)c-570J<MSi5eOyplEJVJk>;$h97qqzl-TSBt0zTaZK+dos#;vZd5sy#Z!nkri zy&<<%2RlAt;K9H@wkK_ubI#iO2lc*Bo!Ixs{sGbZ_`mu?;xTk1SzQ&y1>+Sk0|c2A z2oNJTvu329@&Ex4D-M~Col0P}I@~~s*<eiw<PiDlQ(+($I#vlV0x0Z6jXP7wVtbWl z1c$Xa9p{NuW_CE#m|5-_Bk}W|JVx&5+QgNT6E_efxvW`VdK)7^BjW8$PbEV&J~-%i zjsbWieRJv>hJ5`*7kx7f5mcJa7O3keQMRhSa}U$rO(A{&Q$OH}r6bYp?8b?17CUB& zqOR)%IV_uP?iP7$r90~%WA%&G0jr6!$~?3kY5bL?AfrBG<x<9pGBVjYaMxKzgqdAi z>*Fn?>IFtWm{JI(-nv0`DgGc3!R$jFXa&|Mgon6vJ8Pqu<$FS>dd#GOQ8~kktEV~S z-9qVO8l}ON7bEsVz_f=cpOCZO6n9<2nQg5BksL<@R(=IXi=>Oaz_AMYME4i{@WXPE z{?1_sW5xT@(e(~n1B14R`TI1P4-1ALXiOQHNGF+24+<5w7^)~wA)?7DRj_n4S0$w2 z)Ur2VNfx=juFhl>O~C$Ua&ONbs;nX>w%3pU!WpSt(x|wILqx30-Eqjq%b)NMIo2=N za@+xX3D|xa1H^ii!kmpk0XqVeYm_c}`KA}v2KNSCGE~w-PHOcRRpfcsb>mSRZa35R zNZcuJ9x(ymc`>3m=<%DhZ1LX4M`n0eYU(*SVpn=ZkK;B=t0twPC<UY|ATkjYOev9( z(Wo=xitfQ{kq$dX3mYf`AqGe?6k&<>^9>{GMtps?$_4C~6*mcWR<{&Oij9p9CF2tk zo-ZpCe)Ec@5PjM~)LiQ3l<fIwRoE7$tHc+aD)Lg1vA&$3&a$;GkHq5kUB1a9g%X^d zr-~DF%GL&CPrCFHf&}3tbDYSw0;$V)$E!||CfqZ+Skeyj@aD`{Oi=pz;SJe15X(dZ zZs1-mL<(8C&!O7zruF2qK*RRKlQS*go|ntU*Q5}`bU*o`W%3-BHUd`?HOf{805gw0 zvkDtqT9Sl)W+s`K9Wz_0UlOSbxNXnQ+A3p^bdvQU&tQEdt3}i1I8P2BTtAWqO@h)Q zo8t!3$M_v~3>@QH#Ldb+^jW4^Alh}}fWpqF_fn)5-Hd8U%FBR!xTyt=0Qn)vCJ;2t zfi#_&Fe91({G{pOHfdOMbAhq>3(+D-4{Iz02BM&;BM6jx70nv#BY~fgXdR|fC~}N^ zw@tVmXOORhVM2p|EP_DjsREOE+^wOQgjp2FuW_=Y4%R~fje^IT(#f0EXHcj{Fw)&c z+R>~vlfotA?2SR;2MTq8Vw3siJ2_+;cXX2Wk599$>?Ted3BF(0J3Y-#s&P$9db7X3 z?e^7)i?2qOmv1zoug>3arvVJDqih&G&dZ&jLrIlyn4imgZnT~vKeT6hV2ZJ}nt{q# zy_WIp^uY0MpTQ_@mq^&G;8j>27N?<`ZM;Do)@giUK&YiPDhvdJ$KGSqzhayx^56X8 z1K|I0ks&fTvHxtj4S<x>&+>@Ib}e0_6e0V>{#F{(T78WsJ<#<G%ch9n4I9~U;jE^s z!4&q)hT@l|A6~Q8oN0h#2jL`()Q#DcqR)qp`~mBbX(c5Ap7QAYH2HslDaWyV%r3qz zrNL`>t%oFf2~V7`i2$Wz`_^xe@519ZK;k?cD>Ms2#*mBq5Ht<SDmN`WnjR3Elf{5H z0uV%8fZ?-ErlSPU5xK6YF(Lo>Mo=;szJ7!JsDtJFGi+}p-MUwx$I|VND8E_BcoQBU z{CZtuFZsk!hz?&o`T5-3XS#9;M<>1g=q0m_{;GLtvP6+wq_5gaXz%T=h*}a%-M&6| zi;QPop|&jf$`WcT5`MYS@#3vz(XWkSn&&WN^4Vei3dxs`)&^p{P+4Wds;xomI6ppH z;P>FRJY8BMue6Esy)VNr5nad-J$fKFjfpj&ks|R*{v!OBv>0T9&Ts_t`u`wFRtu2_ z+1P6LZy^Gnb*(P%6c_WQj<of~r(E8nrA8P?l4_un8YF4R$>{x@b-t*M;b-@B-*RdF zp-h8NVqMwce7dfP$*Jv~cAi5EAoDDs)$C~_9GtBv7nge24k&oZk)Q}(OlUE%La#k7 z+|zy9CkDcr+VAPY%AweI$!|*Q8JS;#F}<H;8o8I#Ki9SESj9Ie(GUMd#2dnu*0`s$ z^via1MRo|?{(p1KM85V(XCITqk((a-q!xVrqW!C;8Zv@Zv%Y=mAW3a9>DVboY>7!8 zRehbHK3)CwGo!ST31<EilflusM`2+iL<slCkWbpZ=VktC^N7SVrHzRb6II7XJaSvJ z$YLu-w;zfAuJYItw(NA}h)CAYQc9(mZF1~{BPXQNdIxbluC8!K-zNf;wdMZ*QCu+n zB(JL;_&zB)v8d4O`I-IH?JD9*`ujIl#Y=x6#yz9cjd52Gn|&W<Irb|xY2=fHTvgtZ zcJIQS7k387HdoVCWNA<IuTR|i^=x5#$WInAI6DD)hiN1d9+FCn?fZ22%(d4Io6<$z zWo5t6$|Xb9%xdLO<VGm)-&d7Z8%pR^bZqRC8pgL#KNVE{UZ8f14Z;qQyn>~W3Q=dQ ztOqdeB018n2TLA>v0lKme;30`M}>n+j}eN~Na(|tR#9Og4@^(<lUC6cEp}Na-d;u9 zHCjSG@&X`dUo`uBgtFoI9!8?i<-49l(-e5D_e*FYd$&XAE^}Sx{LdPeZAS{v>RQuY z&M#UJd*$Kxv}-^&m*Tc0)Y|n=m`v`SW+knnDO!xi!?>`9Sx1IMJcE@V@K_NRk+41| z+w*8o5&3zH=;;=Q3!y{;mV}f|3jm1FD<{v~r+<z;w_BvX9B!XvI@aS8@bcjy7(I1# zz0YqS@=WCw^n7oG)7Y2QQ0@YKuu0SXqO3MAf|8Wm!`M4&y7`fw2(w2=-8u`r+tVf{ z9rCox&}H8$!vUDM-Uh0Sk#i%o3RQ|UnvJr+1~x~oP7Lib@;Y>mJ<DFB#%wE_qCXg= z#a+gB8rX6ewawoU<v6uqoVB`W$TE1<5@}MceJmpKjEe3cDGfrB*iCV(a*?~g!6bmk zZ?*1b<96o`U@LwA2oeJ5pb>c_)IIcFxn_rxlV0+yJ*Z3DriuPpSBRF(qyCwndrG~| z`26}wG6lTJSf-I1#uZ{U52R=13&Wwbc0n@-o&jdx$FA?~>KZfS9nd%Y!gGJ=`;PF8 zjIjUte_}>S{A!`U^nZKiuhM=CfeSm)xqBfY`Q?gMu6*3gL{*l#B$F!D9l}>d(*{!$ z#ZzlBPpZ?;5gsO7Y*OtLD{4&f7*EH|72oP_^Lr&o4L&r))6F^7wBASoOXI@7p;IYp zc|k$u^`^$KW5;SK^0SW}3u|n;P_QCTrjDk_Uo60yxv0~Jk6a4HnrIq&`4t*xu(a1z zeEMZMJ@%3!Q^LU)SBWzy@*57Pu-HCh34Pseo|eFdK2VGBlS0K8ZhN^e&&o{3?;?uu zoaoy9G-1<qqns=P_z(`i1?dD7gdRpS1mp*3!pQ74KBIl!vakLOK_Zg;;yJ)(v{fJL zzg%kh#qQ1nrdWl)IH6fLHhRnNzmTIXxa$z~c8vKRSmp{Q&pkbGKzW1>%No@z_`WO1 z>o;IS-hSpm)j&)+uiGXV6T}sQfL5i=cN+1zZ%<K!nsehm*L2H<JDf{g{(W)VJK4K- zt!`*I>gp02A9qB1K;OyXXsmgk*VQVGzj$%>I_+9$oztTUEyjvXq{i4^^;PRqxOMlO zvr0T=w(UX1M#T?>I*i0-bas9pt!eXiX2kYjnr^?QG``_pMrK&hC*C!oVB`AOH~B&q zs#7;**N7!ivKF%=ZR$+ATHy~gBm&d{BHDJ<H=Lk%^#1+&`A-v7M~{V+0T62Xqbmkg z4!yK@9KY*QI96d!%+-(~zb!G9ZpGbm4{~8nj~965rDMS2NNohq%lG%&dpS>SvV|Kw zpbx`SQ%f2Zdt)>=$*CrGbF*_&vWbqZffB){@s2{w<k7|hcQ*C8&nhmU;K?&yiiJ0y z{OgWa{;pn^!ZPc?cLU*q=$)-XqxcZs4zw^Hc_>zG!8mA2g~@n|wj-O-b_LWR&e{P# z9TBWiTp~hE%{~(c#AmCApR}Z~`L@n%%B<@OC|nC9)k{1E4fEo-<Yblj<=!d}{HBlc zv$I$4tN`(jCd<CTYH6kvt@<pR8r%DJRNIM|#-^{m5yXJ=C+A@ZI)8Q^?sAdw8q1o> zVl8*A`y#hl>7$Y&S7SydIYRLB2}z>!@!KRDi?9$EL5C~|c>=;T;esIr+63PP5fcso z;H|(|NuOm?^&l%#(Wja$`x=d$_ztuYhTB||(I_Z5_WRfMs)@*mdOlr=kYq}wpi1Uw z%1oK#N*ywi*_*lTEk4^JK7t3zuo%k6gXh(nR!PHK`i`Bq!3n{2jk`zUd7sU{P2YEM zZN#Z0Wnx$_svi_`o9K<NAUE8R6b<W1P&|ZWUjk^zf`f+&?hN#LR*7YCj@aKnX6&|3 znNtV!(WKm1z-~5+S+ryXS*hrpgkqa~!Y60{EO-V+Gq0sbIwh~@=`hR4Zzcc$6ztMo zZEw%<y?9>c<d!AOhOZ6#B35k@_SUc}a#0)dW!8EO#5up8BHwwmr6oRVfoyx&m0bC3 zQ)5spWma{~<l(DhL)pI9?#i8VWr>1d`GA>!b)`wzM-veduys=rC4B(l!Z_46=yK># zpV8i?q5e~+UNo`OTPqwYHN%P7Sur;qrx_-*kp9Tv!zYjQTQpUEYFK{e%<Xh`+9D7e zsN@IZVze=U2cuSZK{{B`e)*`SBGjcrPiFSTKC?A)+(!K6f`gM$Yl!<y$l&X353_AX z%P2o3IwJ;v0(JrU22j$JMvXGk>|_`jdl}Z<)C6M?Tx>zTXqw6^_WAh%;{paW8jTmv zQ<VQsF@oQZG30Ba&)Fr&IeEGj`u%db!)cb1-=mvSjLcv!$PgpbX0*(u(@Tq|O-6j} zg0u8pwDC2|7T&1Eh-h93Dy%LC*No)@0Jo2DLm>wO4g%3ku?2SkIAWlk`o=bjG!ldj zMx@3VNRo$i^kBQKfYFMbp#<?<7q67$VU$b~F_$C_3?Z5-X9yD%D1GP@9k{h84%kLP zMuIUQBrU?m;kBbc7DNFE&_Dn;2t1pOU@+ox;;|%Gdh+la4RSxL^a@)2rohh5D69>n zo=;CtL!%n-X{AYoieinr57JOt{Ysu+lag@GXX!KuA7eoCfs(<x$5K;L&!neiqV)vi zBUtvMOc7YdWIIz3UR|GlR#@}yh2=sd8q#dw)ohTq63l_+x&e)-Cpjf5+#FetHi48C zU>2-^6v*Ki&{Apkpx`HpF2Am2Me~YVw<e`!(B-43<X&%sSLG|GD!m50J1C^=jYznw z(N!zEdPr;Re9_vj)mFw-t*?uBtkgEUD~4=b&$@2k&CO;cB?Xaff$xAJ+BQErZ+Tu2 zDP?6JJ{kpEJT+ycj0qaH3N>49GRFO_<mZ39M?P#zF~M*)%VJs1W*d>V=BHEH0JWcC z%A)>ag9d|gB@l&=kwg84Vhx6bA(RSyflv(n8QB<v^87v&_9KV-vokapY#PvdO{u1A zU1fBm)Ym_G4GIqORMnH>oEpxfmHtxFxtbao9<1Ti)R4>|bN4!r0aSjd8JFaNb%wWp z*6=G@mzftt?fcbYE}8S#UU$4RQj76wy_){jJg*AYraJg<nROM>4yAlOoHW@RW^FOw zYEH2>$G5IfI{f)|E9hj?!6VMGuU+!1PS|5Nd8IblyYi2ZuwdZ^595`|UfYED?2PPO zUa=GuH$mWy`tu6N1!3`tq@*k5x5C3dgSykXLXhX6x{#Mut8amup~*N=iP0lnA}U;( zer4EnXti<4LRRr(dvjOjuQbg)U@Xa8wq{caZ1l-{SzfJeUO80p`=FuIq!&g35v5Zg zAW34D9CENL2!JbB9N7k-a4hS}{Pnz5LWmAJ%YrRGqgFJyIN}{d3H!evS38{;b1&#g zM$On1C5L=H#@K5ly0~s_J_bsDaLZuh+aG_nmixSeUmK3hT6j35BfgKk3#-*|x_7xE zPxIP`cRntbO7q+J15;i~4_(}&DV}2s<o@+?i%kGt<#r}Mooz|c>JgvLU+pB$c61~@ zsAJfdvS@xr_zywMebR*O%%4#;G^1X!I9mAJ9yTCC#wqOsfBy&XTg!6*ZhA&P#e-|A z=CqP`Nn4iXp;j$+UOf%>$p%nHs_9*V?W+y;ZYpi7w{lHD+^(Qq(G#5T{x#8Z(BQa{ z{_xc0`@5W@?qp|sFF=2F3NBrmw5lll677%EE=YAE(^KZ|UTIxACu>gXnpBW?Mrt$! zn6}1NsOhBHK~J4@p>!(ju~I3i{uv#{sv@?nI0+>`66aphqV7#Ggzq;rA+of1x<s-- zwL`Zy;iZOYRObSO2jVw$`PWhJfv9-ddE1H?e630~djYK=ndDew$u5g!=;XMICG|c- zmm(M&Lz>`4PX<2isZ)@Gs@K=O&y62P@ho9u-mn}CEUBM#S*{F^J6y80k!}y8LcR+Y zN~?9Mt}b9`NZvh}Nb?{_gn2r#hG`5%#|<+6YL`;0x^kt=3d+x-?zwm0`E|W}nmpLr zs?OA9Zsyw)U)UFJmy@%&Sfk^XtY~&qTA8IG|HbDjq6>{TH)|!M4U)_k_8NCIMg_)F zXO~$S#SbpmG%ep63O)_2*J$|?hqt_GtvC)-YE7nbJa$0j?*M2htQ`P^8%$ve?EfD# z7l#HMYr*sKP8@!~9<pLefeB3bN!K`*HmJ0%t)yX+AQQ5>Z$8nj`sZ^M`OSwi)k-hz zTD#k-&3574y7!CTxy5#qeQrwYQuf|b;Nqxg<Ic$$iKf3a(`$2i0m<cx4O*IIHe?iM zY>SC$TM-KS|I{vxO*4{oHm%h_J8+(12(sd`z+}&cJQ%zI$OA|eE7MNgAyj=X*v>D5 zP@PfeKXmrM;<DNOU8Lt$@1S~_Xbgsn9gO~#SHQY)5WE#NxzFwA*Mu2$bmn9W^#2|_ zD1_Cyu4ruG(Qck%>*h%=C9e#ow&&E)SUG>}>+0Wu0m8lm#*Nsos?XXgr*EO(UI<<t z^qXNT#%>UnS!J|)<unKR0R3E2ctQXQCP2_M7#S7@?PG96i9O_kNfMfnZX^!Hi^gC( z14)>m<@@LuB8eP;bdCX#WJcy@1Wk@uj=uGKYlK&E?B<H8h4tcEi?TC+fAip`zRI+& zR`XVxs<vE?y!FT5ps|eo^gsh@KgXYBf96~n=j_~C=ZtNChr0r#t(#tR#mrutECcct zi7@B)q(|v6XiKf4tr<ZnpqZ5lv|(?$j^+z??@y2TIkxN#E4D9Ix6zsU?x`)0yy+{4 zdX>Z|;4i-tsG#ikw&KF>ZcChRTLlJKO7@ArlOCE1r6(9`Pf4U(=CXDgk+O_lEWSTh z%<m4|_|&PZ_9klmzr@HF-%&2;YwV>!r_aX&21L+KVm%0&UAK$pCl%DP=O&S(1bjq} z<lXU8?oMgW=Z|s4FnJK+hJnJ3VtVUZP1wIM6-9NvIE5S|;7^C@C8Ws-9K%tx(rd%% zVhUDRJbXt|KIZO+l7!r>)>?Zn=Uq`YC-~ahNCzr`x1Oq6(jW-AEvi4q@(Gfh*z_m0 z(Qj+2nMNT7GBe7%dwCR=dV@D!p!KdAW4&?fyK_u7LJ-_DFiQBzviEILY<_YZBq+5d z6c-3BOw!x)FBv4h%zU$Dko}Ya3dn(!rw>a^LdA9BtLoNGHij}yaMcy{xY}K7wa9XF zb09jj_2oiz!Vl^*@)vUVnCI_DB}bQ>N2ul!^F>ux25ADhoV9(T4-ZOAs<JFsG6qj` zMHO18>dPoih}vD7pGzLk&&fSFO7c!K*|9WI?yi0GsFi^8-&0Ur#i`<pk<}1sY${u6 zPiZt1Zz#>dRebq!C)a`0NCRC}CpZqQ22p}tc=p1=A1t&%_wmJ7IBTTm?@{OcM7_Nk z<fmjm@{Z;?UV~zqX^At~$;kr)BZR$#476HbTDHJzH$?ib^0i66L_-*o#Ro;DSrM-x z%a^3lUQ_uQI=Gt(xxj^HiLN9FA?t>f-}Qts$PKgK<(o?y$zpX#fko!bjYBfnL|>Lg zdsXJf4)7MDqhLoFqALguAGn-waoN`@Wn$aD5X`-dUPd1iG<?shQrtEji};Pzp!~-) zt21N|qn|O`i*3S0at*o%SGHBU;Ejza5^7w8tHKs}03EyG_fdm-Yy%C8@D2DPkI)eB zi)@ZOY<50MP>xFDO=^21NG=NRL9axgBNeUs(o1HCt8y$%l20;i9~7K&-KY}7)}W1O z6DDy^$n3J&AW8Prq}$vD3cFqkqlG+rasu@m>fZHisD!S{Y|l|hoPirymWkVmE5~;7 zpg7FvRd?9Yh%iYpPTAEqEY`%2!!Z#x_E0RgO=<>39Py}jnoX~OhEPKXX>yp<Q92+} zg8nCMsh_@oXs0V~P_2V@)K?yxKtG8>>1h&pH~Q&s3E4lI^Kk|(xQ>4W?s}{hweZ<? z$M6s1kC84RV!sVXx_RuLaUg`B@C7EWTR=h34+iA8@!d`hi&x`G&KxtXVF={*Wj<F1 zXN%>bf`Iams8U;tqg<Y>@br(J5#L5My$>9J7oSU_Ef3^DWYoDYc4w1J&ztV(nE#rY z?4-ky$iUEB0wcQi!})|!LR~_>E%;zU0MK_ZHJd|eV?03+B4aR37tJ<r?~sp_^-VB& zAkAB#*V?@1F_GIjQj2mL$?i-W`54u>U!jNvv~m>d|2Yf4oB9eF>)|<D<U*H-whL>_ zn$uaX6JIA97dbUxltC-p>`TIJ4Ob0~FUX-4<-Cwl`KI#h;xca~;JXz$Bv+5275b-4 zPF8AHIMtU63xz?^P2-W<<VX%n8;(07IkDfU<?e4?v38MWihPoIsW~cvh}SuwGeo>o zUp%Fn=>~H7K86qww2@<Lvd(Y~-RGBIDkwlpZJ)KLP08hYz9J5{6`QrkHR*dGc`Hk! z#?`4Y+J~Z|-V_${q)xMQ=yEAi##h85B7-${VsJS@a8SVk1bJ;F2c^;y2R;M0FLy@0 zGA1d4D9U!0RiYx5V+RSMuL28YGi2vDd8G_Ex51UNSsAl}kVvdEJ~N*bwLHoRFmd}F zV(G6RS%tmrP9J$hE@FnQ%qX!xk12z^zHs06{Lt8{*1%9Gv(_hb?$GcZu}sJn%LVf2 zNf8tBmEULNwsfLRWV4XoWB>I#RRm{^_E9`T-jU%Bc>8v%9j)A|>P}nR9o1Fb)*V*C z_a(SvbX7xZ@xupiXvFsq9u~JYtQuvS_AgAy$)PS2afrTh<a((5G+9Z=H%83KS`o{t z<3nBZlKHQG9c1o4vh#9in4ROH4_~1fIZa31rceJZr`12x$z=7q^CVYK{e%nFE*y;& z0smLahVl4-tor|eib&(>{gL%_eX*kGv;%jvx$DTvrCOL4`v>9hAxdRI@T-Qs!sb<8 z>wXcuyJel%8<%IF;h)_)Gc8%S_Ou<YXOnPG<LiQiAj->^e^+M`CR_BrejBM~F6emz z25?#Q&FO1nqN;aP8OvI?4mQA&{CvsOO;oHY_oe(doA@^Mh5|!D{Tk7(%dT7kDnD@w zqjP_rtUc9}l+(V>;+8|-f#2e1&8H)`v7D@mS7^6wrS(Od+vDAQ2G8=mh_{fC5f143 zyOF2IT-zRh<jO;~425r6b%vgN<tc3{JFsef6RoP>z}5RI;P_7}vwi+;fM-sdOX$Rx zJw0jGZnwnJ!$)e@o-h5T5r&63PsaB3{dEI9D{7=ujfsg3Q`3n%{e#$&X^k5mHl7KP zl5OUdUv3j{eWCB)7wGNPQJFFN-Ov97>j`*XVu|)Qeo3MdPD!1TPQj_fZ`827R2~k2 zjFg3PPQFj#N#R!Axt;Aib6p4A0F&@iie4|h4nf-)dmo#`*^mcE;GvVmwb%=t+uond z`Q0;hfVU4&^F&qJ(iTB%dg=AsVPPuP`>%zDZ|`sLkZ<1$3t~yPfHWet#Wb7{L|!1B z*~KX-uf-Hb<%+?Q#+CgU^-m6^lG<v_YE_$6dcD8USJJFbHuJ?cbnU}7t4s8-<JT`M zH|AnxSiaVxY5U|Kmd=RnxjN6$=wQje_zESYG2jF?D%RRG>HMt8?jm}{q1L5}^jLf^ zpsl(0qu^q)TjKlYF1fL#T-kMMAEm1zdn7k#@1E|B#S_UT6S>`c_6Fsy9V)Fe8Kb_s zhF8kI?soCMRpQc%&xN8ck1HGDMfX5+rz1e{pPSK>v%4#&Cu17}1%Ri6zzxdubmhd= zT7%xaM{m~KXWBQpcjZ@3n46~DS17RC1t|dZlxICbzgvp7`PI)<$&xyY?>)6Vj}YNV z&ax%G;D#q8pF90}xkP43tr36Ogi+lybChMuxi-1Y&26-#E(x7gg-Joo_qJmlT<JLF zZ1P=a)K5`~Lqo*-X>-D6OqY0TW+E-azA*dGc-$lxOf-B{|KatA>W|2$+uJXlh@Gt{ zFMs)Rzx>~SXUoegDqd|{H4@Ql-l;a~FRz`sbK}+8(p}mI_M)C28(V^)*%M>s)&Oql z;x~wm<w$QRu*Is5meux7$OW~k05^l=)N=IyB@!o9Kz;rc8X^`+iXboSj1F>3o13#F zwk7Mq!Q8+o(pSG^BiZ*~M3d5UPk}9)CAJo=8dxSFP1*X*s}m9s_ot5hquEc1QwX`S zjrJ~|`<>bzcFRRG8W6}5;^Z5#xHvw>Yj`kst1vBwkBh@L0{^`|fDiy;mn0S@7Ol^Z zvMY^VurSKDlv$teR+Lm2TnzvqqIZrc`mq_=9u*1z0K{LycF9kPa(=05)-HQPVcNJV zA(^dQx+o`x80?cr`+~LOcfWPjYUlm%pw|L<Q|q!NZ2_^(Pl^j&qXGbk*t|<UebPjf z681a0VAziS>))O^?o8DW-y`^nE9J|Rax_|1&Z<@AVG!ZY@SR|i<)+_~)<`j7$c8W* z1WmTbRXi+_oC<J>w?mNE*2)xd`LOhA05f_iGaOvYDTW~OmS?R}<!CiYIT@?2#3J~Q z-_KMjy;H8^bWlw4HVsk=D>qu$?d295;Tf%B3^m>Q^rqHz<*F56ef5e?U#WF{=V=VM zQP=P|Ew+T6obdosc>06;-2B+I8(<pRv?6@M*KcNMy1s#tsIyq?vr*vXCWW<5$8Q?o zO?`g4u_j?{qhQ0eou^5NK<MdlkvU=vIZM^+-z1I@pj;To@uFv{!8fni`{GIg-)B!b zj}zs7IV3JR)iTDJ)Mce0t|f_Zj%%Px4`>KQuo$1*HiDoIoVFoo_O%rk3CeyM>rq2F z-zbp;%u9q}2?8)wVSNfV7!2P2gg}5R*Sg(sOW4w`i3Sinx)2<KUMD_uU6?!3f4<PS zT^f~KP~aIQ?XebK5@WBt(Dm>5?c~A&k1ON<(!n|^<)M+%?KvMNVg)7b`4M74ctl1- z>;=|ImJ*p0*W+QHgB_1m>Xh^pwdzJkRku<7xb-aq!e*ZU8h`7Qrk$a7H0XAKN!;Ye zM2B2gn2`>C=clE9{0Hc}Apto4)-i7azV5<7YKVCQLKkptmrg}h(>`tl<+Y;ih{<?b z$x)YOp&4;g#dTiB`R!;>-flhayIdI`f4HGrV{#z1=6R}Lsdq~(E^8A%HZH4G&M0TT z@nfv=tCH8T>SSm4TV^kq{px;k*x>={%OO?Q50pD@GH>)ZLS5Ey#A*A%mN3aqxOQio z%DL4ID_xhZ5tjjc+dRty2O3Rpm@04fH!%Z3@34=^q|yRJGB3;Jy8BXM`TDLxv1*>h zDHKHB4)^-G><eXK!MdGWmX>D!Jq7;FG(GifN7W6nEhais_fz}OGprwHKc;^8_;vJF zJJ71VxKrz3<?X?@FX?&nKvqP4UU*`O-SOea4XMq$2&!8bm!#$YUjH2u8{EzZ6(DUX zkOI6cRRM-_`OD+M&v~Rlt=q@Bacc7*NpY5C$kvn&hV8eiXFg>spR0w5ME$%<95njX z-921;g&<*m=7zSwfaUMm<=7>FC6vl|s4R7PY<cd^q@jiMUuGSD`a|+UTHbZI(m5sB z{M8G`x`9-uubzvgw()clq8A(kIUMmM%}IYi|0tZBtPvDP*7e0_Q*`}LaS`=8NT_RB z|2hkIY*KX5JH7>-XlXK}q<mr3@{(;^?abcvDI3VmHmSe5x=|<RjZQ|t6nIKQ-Kxyd zzdvj*@79n3=>``0&CYzr&u?cop4V*u^Pt}QC*{d&<jeCa*5>~~OrJ?G0l+|&1_1W; zu;dH)GaJq^=7M4x?d3#=1G_{J>=MIVT2>PTqT|GA18k`D9{|1JlWUSu+1!16e-o#_ zpVPMyok1V{WXYlRcfy~y0zM#eHt<BMU(Y@T;)iJ1_nh=_e~=at2T%{XBY>1wE5;L6 z>j{1OZ9ZErRwuQZwhDHHZfwo@ui5G4rWQsO2p+kOY32YT_;vG3F^}f=N7Kv7K=f_% zpJ~U$T&_P~7BjoE3l?=gDG}u7q0N+W`<x3P@yFj;+5>VrPw--y(S;GgJY^HW=I6L| zC<jZ)iY7JV2=OCCi765?nU?4{OG0<52GW3rkmI8=v}g;M+R4B(AP@!;5W*Wo%qTbu zqKM>%L|>XTS@uYzQ~?@kOqc}#;MbiewDa?0(e$z=dT<^TGY6rys=>-b#6l17St7i# z@zL3N&E<LfX)9y?dmsArN&ZRfe~&y{?#6uy&3PR2?j5N3KO|={2D}6N&Z7`~q&K(| zPwQ{-@)ADF)Ycxl*|;S73hfn^uWqfmt>K5F>tNuqT1x-*^(W^A-!hh1v0G&b-+nE7 z05&Z7-yfZ+^6$?-IY2O!66IIc^WvjdND=3Wu{L~SzoNGU!AE0gX9va(P(yA*!%139 zEA=F4*F%gRTg!=Gccf1%qADDf$s*gd(3sWE=;`(<FNm!HmhWHykOK*k{ZVJEOC$GM z>!a4J(AZo^Gn%%ql)P;}!)nV?`|a8OD<gS+uo`P~QOiTX3w`-yf^Dx}8?zlVaMSWY zmp=9X4*+#ld_-BqF0)AlR$!&^oZ6=1)MB{?eM79l#J*o;Q|Cy<0=@GYv!1Fvo3Vkz z7NGSMOXoz8O?J;|3qC~@hYA+32pFggy@t_8Z>Dx6h2CZb6raLv@Q<wW4MxI@X1XZ) zWvm%jj{b{n#WrJ6CFrw-D1ePE_;S3(ivrXMz30pcSV4F?4DilW!d|=y5v;kpwLIl@ zu#xslVr`;7!VrdJYNXCQVF;OtTFWK*Di$cQ56c8Nor%RA=9@fjXs-VfUt5AuN01{F zD@XWmbZ9QAtRkN))L>x^NsZm)>^$o1V&Fc~1zfnXF`yAF`vNv$!1QtIjF5?BGyPKN zu0t)~gt_X4ze#7-kFyO1FTJU+fhcxM(?2>rjzy*xzc|B5_^{k&%zwj{`0}oJ@{cJM z`9We?l}Q%zc)kt~LzG;H+;M0)Mrd@%9A2i;Sat&;o0;T|rMzvY87y-yS(oAP1gX-x zvg*oDfA-l-ZyLS66GcEWx|eSG$q$~<`EhM4D=%%iLY1;Zs?dHkbVZ2DhjwyXeqQqL z-ri+F_y0I_Xo60R5l8^<k}WT@vMQ54l)~d3f=!d+Iqhw!oX0KH!L60$bWFG#f!FJ< zJjr7UB7F-4(wKo8F9EkvB!Q3{T0sU3mk4?$*7O%Zo+u!|n^GRZH+gsa2YlAC#Q<hN z?0BG`q!Y@$H{mIkb9@l94o0&UZUT}{oiuKS#S&!{z6t*eTl5m94_jdC$DJOuVlPy{ zL-8!Kt}vh=Pn$*1#{F$4{2g&tJ1S4l&Q_qad@`utYsc&P0DXZat}!Bwu+v!j3RQUA zUr{<&CsF03EKw#q$$RonyN4#k4LgpU>aO3m6P_eeWnEU39)2wDA+OQcj#?WXeg=<l z0FC=k<jMYtL$%IZ+nD_o!l1avuFwB-wZl)~X%qY+;yll@mFn*5wjYQOt1ng0S6{3? zU46cKiRlj<6e(KucJ*J1jWVeZr=b1?$T%{Z*Y(_b>j-xu_J&PjXGgke+ZDIWR;oLz zpG@-cOX@ujVg}>FoFLG+k7=P4-t{i`;P4Olir+A&g%{#PPzDJKU^nw`(w5ItsB?cM zIt7f+9Sh7&M-XpbBL?snha_OeU(j*|NXW2%>_rKthfxh&5Q<$zO7+zu;MtXSeu5^J zkhT2vzbJZpPb{%l{(1esSO5EfX+EI73LbIK0LJERBp8hGi1nHQAIE<dE7S!98{D!$ zprRgdtooTR;PsfkAi)Wvfs(KOwP1%>IQ>ozNPua;fWqE&Qy>;CAZT#U0znMM1stn> zyC6Wr=?fA(@KQ;s;({Gg;KBh1gkV-hPSH=08f!NEe6C+GsDK<O0zITbA!LFEHh>#g zP`4!+;PGp*Tfa(hlGl*;|Aw*wY9KEk6xHc<HXf^iLP&!m8Dxg-4k+UgWK;y97T9i_ z$92<=z8v?T%XI^!gTAKY4r{L_el_M@L$V&Ux|rx7$J_-!`^sAd!O?+8;x}<N1E@d) QSx^k?L^+Vf_Z~2?2KUbo8UO$Q literal 33420 zcmV(~K+nH-Pew8T0RR910D_DF3jhEB0VzNL0D?RK0RR9100000000000000000000 z00006U;tbZ2nvM46oasO0X7081BD<9f>HnkAO(aR2Ot}h-bDv;*f;?8AlyO}!Nvik z0K7305o{a)ko-fl|BnfD$dIy|0=rXlY$<KWY{WA&?PPcT+r-^*s_1AN>t<@@kx9^- z3>KLcFYS1Fl`zS{89`=!N<O`RbL0paaxA_E1X32rz7=Q0sf(O}Z6kk@llSLIH{W~z z-(^7H-(4o4-dz&Ha3mxV;!FrbHw47G0R_hd9Npq*Yqu7)+U4$A+Og@F+PUl+cF;9* zug*SC6kpzxEW-h{)B!<tY)BT_`+?$BqiJVa!zYC90C}G0-rYZ1)CVn+1r&#YjxD%> z1I#2(!%lB_er>*MuHgO!k^f&1d3XPgQs7?_eRpIXp(s%%(IzS26hX1jsVEAfQ;eDz zy*Vr9oal5@w;9lXf6w-vy?qUnAe5k)#Dh?>!c_An369n8w|zS|SNRn@6f&ie+%idY zf2Nuq>P%@pbxhdAGK35q2HOSRx_w8o(%)y@=}b8r5ZFGF^r-4Gg81@X{{x0WomR45 z*Jn6l_-O!=R%w+nj#IEX4%K#miVzU2U;Y2Xbmg}tuaHm9k~`o+6!Pvej1{xB)6ikj zY3xt$X8)XW<vnPx+LwsZ>lUG94PW*AWPX$1nTY}IwuvJZ#9G#c1&c2I*IUw5s#>u& z>mUs*Pa5LT6wUvcs<pci$VXjBxhchqsdQEC{y%>QI{{&K36l$erk0{ifszAAB|2di z^1WRUl1b-$*)dX%v@gf1@?DqO!lmyj_t!Kx|4S|DAD3jjRk|vtbmiG84WJFHD5#(d zV1a-BpRwl8e0w!xSu2g~oFwb^$+GXBWILO&wpH!*j?y}7qmg%`wz=`>isWs!w%V!A zraHBt(pgwz#{v_0EPDV2hoEcd5_X3ISpW-zVOiD`GJ&Y+vVf}<gfQyw{`cC~nChcu z4Qa}?N)16An`B?H(WG<z$)<npV1v#1qSY<M5+tTC?ICN{($&3k_jmVKuUbnLzlsJ3 zkY^?{lfcgbtme2K3D6@m09-!$82k`jHhXY>*xZ<a<RRY-SdvmMU%VdpJJ_ac2Av|1 zypch{gZ@mA%ZhMF|2)2VSFL%$8~Nsk<&dWbbKe)l&PoFoWB{cbCvaA$9ZW177O#5V zLzC`;(8l019GTZM*7tJ~7INnhusIdVD#WL>&LlFW$_aV=W(Ea`f{iUDMT|4YMhPO1 zt{^cfi@8=IMTP9ps$A4OGo4(>1sr6{#i}LyDBQ6?Zjfw;6A39k<cVu4RA@&bwfy7t z7M3tiN#WMkh$ZJ>cE~k$NbD#Y?wuD4wT#!!&oHm4@xGMEr?_(f3SY36DHszPrE`rq zzx;H~%(C+fGAr70{GOtm-5s*I`R@Z`|9C?*+ZI9to>5sfqAGcrU`WakT+FPkU@Mj# zvoO>8dp^YDHEsa~w&c*`D2Pjn16snuU}fvXSK-$WhVk_sV_e{medp5bOg1zy&5Y0z z*AOHm?qfWj=cGE_X_U-#17|A@8SpKki@0k);f1k?*>Ed2|NCJpyq)0-LAtWKvJ5KA z_%p4U@_)hdaw{uM*KzaRADMZ+?-y_$gFeR)Az@k>au`Ji3C1+%=Z0dCi|Q=3t*nFr zqy=D56-_z${f)an<4F(C=7KTbr;s$D-rg6K*rcbYBsa%n6D^aZxi$OPKJr-1<t~sU zX=N_+79lbvm=dVT0I2W6+Cn2qO(V5B3W=z#poZ?7sjc-ehKQttbPcxOMzytDBuXn; z5n1M}&a(7Yldn$~2+gm)jkI6{Z6l^Yp<G2(DUGWzn`F~a`d1eY`Kj4k&*XO`9Jp1# ze)J{7F+yPt#6lq;L%Io#WF{Mm>hQae1WGd@PB9@3(9ofkmK;k<10W?$cX~7SnahN& zreMd80f5>OG&?K{A!t#1ShWnb5JX`fROP#}QY_$A)#Y^sdJ(gi7wh~OjA>0^w2J`I ztWS}c=?%?`T9SZ)6U{PqI}O-P1?2L<fWtU@1-rVls#@!<7OPd8Eb5%$t^sk&=NLAW z#Nau^Cpl<gm}B2=m@vCsV@-e+J2K$t39l_z;z^eW-$PNGG6m*TSgaKgj_zpo3eb{b zZW-<cA9xh1m~o>}DJ3UH8=SnNq{bI;e9inxro8%{Lc9{%V7C(GRo&U8gAy@-%udeF z2^zFxn|0-7)oLDV42fp$)~wD0iei!&3?#^>pa3o{UsF)_At?b3$xMZmPYDddkS0eE z1D5onpaO+$C%!e&Aj5Dh0AX;@r4~VT(>OavlxR#AFflbz4yEm5Ctp<OQbwVGM-6Rg zz-``1QzLGtLj?*`{Xq=9Qw0NW#<sBrgGrJ!WwXIR<OpY}fyCLfvmxvoWm`Fju&0Jp zE(qNso=PYzm()a@A*|a*L#CU>JWM%h)x|PS)8%Jk%2i<VHY6RL38p3z$+!@ag_m&5 zZYn;-`7c?TsN$$%fJy?cmxgkMR6mGvWmpZ+6hX$Ts4Pk5u`I}%evm0M^K658KK9Q1 ztp?knD}<6|ScO?_yKo}dET&!5DRrdJrQj(j%ly0cj<?lcD`6UDqd{&#Ds`{$p;hzU zY!tPqLU3n|LqlAIo8!XJOHFSr;Mo3TPBW~6V@Zd-iB1&c<qAFT>1}1HaguL}>SKv0 zYivqiI+5j$g|acAv@gHVFgW{-Lm?e1Q8t1tT=M@zrU1O5-lNXUhn)*)q1|h+ytR%# zyu_Eb5Rs)fCeT(9(*Mh0_O*cdg%r!L@u+f$xo3>8N3wAuS(($TyM&)M_|65FgDXfO z#D~we>8!qoc+$1ZZG>VHPA4nM^=*g$aY5K3FEpC#6yb{58wkY%TMzC28qh-mqQ~&x zTrLU^9;Xt?HQ%lJ_M-&76Hmg(dXj$9mE0@w9X9OeYQM``O9%GoEr4C{XJ;k;{EhKR z2<x4NGvjeIpaiY~<Nw?Pj!=4!92+*pno<kL>BpIZ=*ShF?G`Cmg?tARTBBYk0rQqA zJDp=}T3te4U|pt@xfcN&)(GfuZ*y~Y%asIf#A%WwVbfKOs^yQ!#|LqMy>{av5qSk3 zqxn8nbT6-SX(5X+m9Ok1A-TNxZCncYLrLAEuv7H(bPUBplR+X3LZ+onC>}asyx%CM z8^tqZq7ZzgEM;K8rdG5d<9RiAs+h86Evi5OJJEu$E=vNNVX~<2HrRt2omXtN*&`W^ zzM`!V{usy`WL85h_IUum#MWL?%r(R96m=3nRis*%@};0$U_<sg0?SzF`bD(8kckG{ z$C!`^CdF3Q5#r+*HjvE25w01awhgtCZO$cb*x9+kusF5U*1${#7~cotDAcM6i5a}Y zq&z|&G~Gz3zb57brOnr8b-b(zRs0u%yUn)R$WepnQ+bCRU8Rg&mV7xt^A6>DY$v~0 zU{JaNgEI?C>TilpAnpuh?4IS<>spbs4{^SVN(AWql}?^_QIrF*yTHRGjtPlh11F1& zwOCIhO3J>-VBIOtm(ZkBY{|pmvDZa1YecDS5oFvnd7tP33OoWEE0Zce@^guJg~c5f zUU1qIJXhR`v1ZL;g?2?Toz^(2SX|`m?q@`aDlXBi?#<!=>}#|Lz-}1mF&re}gvkm! z!SS^4l^CeeeQKxN4YP52YuWJ-pK)h<qvNh}#F6wADLC8_I+k9LnvKGI3fdWs9ZiT( zw|LAf7Byd4tvRjwHWN4(XiJZBnkT-4TA!nxpy?w5ev0fw(?745)=n~^K+*0O%f7{1 zY;Y=}jdC%ybwNZBQ%Ip9-~<?`rkE7=n^22n*C&kpzE9lH&11t{S<6EhF^kbyOS*=* z!pcI-(4`sDEs^jp<dIu(${e57*G8xEW3aw9Z+eWd@CE`yOe>k?y{1ohV>Z(8$EaJK zpjZ393<VH|+?J_-qOut$qjG37()+Prcf9=ctnYGhib&thQ@ebzf3{86XA^Wt>&%Rn zXs(g3Rdjv2;#JVq`%Q_v)%|{KxzyYPLnLU55?e)8g>^y;v~zk^1lLKbDH_07XK4IW z4wk^ZOKa?KDfqz^mxBWe3_p@hFDJSiwVq~gR(d^Bbed0SA(bm?{{1+ZsKt-T)WD^m zj8?;qG(Z{>9%CGVLmXonQp?AK+^1GhlP>-k*VStCkO4L^EEp~adBOKqVQeCjbQ-l8 zu2g8<O<oDgy?i4N-O6Aq%#fS<#Tg1l#Uw03Uz!;^IT6ZN0yIC=TX5iMI~8h-#~zDI z&~`cqO%?h!Jd#DSOHOwnE-GRDE;~c)P(gJ9sP?j%yl}%~;iQ+22<FY|tEhJh!BfF) z+Z=NOGFXh$V24yDFTs(yTH!Qa8}P^rDk^Tax3&w6*Ibr~462Zcws&DaRfH;zZ@mQa zCe+uDMK9s$6qOcQfLav=0&E~)zh(7`e5dGi_$JSWnkjSwJUzh?7PhvBM<|~LU6jSg zYsN($!ZKPG1-vMDB4X2&da+=NwFJarAc0g$y5h4Ag4<CLV4*CKzsfK|m7H&B0&59; zUusE)RyDpK=hWy{aky5PfIt^`#xt5BIRP&A;c8yo6oGidXt+`A4ttH#I>syT66VE- zdzF*7LI?m%itr>_1y&4ILYfifLMpPumNnfoI?8{$;MGRumLtl2LCQhZreGTE*){Nb z1|!z($p`SvZAS=pK|P?h?B77(>#{NuOIn0d?vPV0%FDsuT~%S6Btm2p2WYA$7!MmW zM2xU>=i%ze^>})k&+6uVXWP^+1H>7z#rim%Qw<ij&Wyc+R}|^OdO7Y%tcPm4Bg^Mp zE;91Q77!s+jYy|IBX=E~C4|yc*Akkm3LbPLBYQsCAh_PTPb(V3`yAtmRdKo!9I78@ zIisUXbbm1!k*w!3Ev39|V|!izi~9=k#KhqeOW_q846q{kN-1RDQaHX7=2ClVB5dIo z5*L_z4^yCan{G{$&u_!AFXJQ}s|tCm7&<q_@=6OfOL^DKI$;`>YJio>10+=-^Rt$K z4ETz2O5Ta>Khjo0xpr?fOI>JtR54yhe3sfzF0LS><mJ;7+w-R{O|oN<`&xZP>BJ|s z88RIVTgE=iYwp|dtgL0`%P?6<Lx8D<;od7Aw_f#ry#0rT?U&alw&&CSwj(u87ErcL z@AEv{+)3$lbJ()GAJUXAyVP{E8|9u>|CN$yHLI3~D!aR;K=MB|&f?GzgVKWKZvd?s z?i{1~@SCYaRHr2^i0NvaY<A<M`4KT*X2Lw3gWviMD}3$XV{I*TBix*J8ngQgm~MKo z>~utELZaL-mLc6GsB-&4Prv$Yj44!P1H`Ksb(W@oW+aesNDPEL$p2J2sdt?b^D^}3 zw98p38*-e_L?bO{CM)_`yd$jwv(IV5HweDso!d@*T3PkDCaru%I&XZax1eM)VlsOV zNIjZS-o)OJ9ti|CiC0SySg0`%tv*9y;$q&Z%DzUuacf0AeoVdN75F-O0bxzBs(M@F zdelzi^Uug4*CbE@<H5+3>qZD9xJkm*4L?6g1hDiig^a(f2*=v21Ce`ABPV{6Wt`AG zy<jYzeoH0^$;(88v6sh4>3x6?GB=>9F++@7hn-h2;sLE(@t4H{=Osk(p0%ed7rCUn zQ|hAg?bwTB1UdjD+Lm9U6blFT(3MJf?2V7ed>$1CInQ9%!kHq*+a&9K48W~=(dn_R z^Hdoe-lUCFEc?jW&9+kG6I<lgUsS3*;`G%WWmsD{8UlkLm8$HVp5=cjp=9u}>Z}32 z`8C4%Z<;}*_uE){%RxP3Qp(p-Q5}ewqj*+v5oVnu@*EsN_qkr>3n9?kC&q*4gb+@1 z5C6zfkj#9-T})WKf>*CtM3OmYcFgX_;WYV>nWoNJX$oG;ln<|8)^WVv8Z9AX(rf0L zanLOSBCMhW^-R3ezK#U59bD-JW<72UM)@jAdfq7K5zvajQ}9cmM%8JB?Bh@dDyY)0 zl=g;%#HQZl4gpo4_50Q=k}MSiP3t&2|1*sM?4<?b<9yvIr{Z!RHiXjjRR=y$QJPOr z1#-kHER_#Yh-@t_%{IvY%~NbCRS@Z1&~vQnPQM#7O)o8bMY&}N_?Vp(mhwW!bBwus z)%-e<*s2qEssAPQO@XlRvR$cwn}pdn5=#N%eibGhyi$G?YtFs+oG{e5+5?ke$s{km z6|hVny&1DWB(u}QSALtxW9@hhmmPHW4CBMZgT6!LmM)L6OFH>${>LEIVP4Q?ve2A2 zz1Wb*q(}=9IMOt8)KpKcE#VPd9F@xQx?2JZS<p4g3i*aaH+}6+so`MUfDA=#pgZfu z_Tb}?Cg9gi?oKl#3>-&j8poxBn#Y0Jsj5{EuiL$DTFoJTBq-f0Gc)}wz}>Umm;s!g z2R7KJH?hB%cXTWASygt+LD|;YnA@WcUcO=i>GNWk_x`O+BsUK$0&UUMhRD2vids@T zgJ-X+VVUEy>6JC5f3S7>B?>HWL0h447Q9xtwui8bWfq7fl9*pgAU!+Sj+_Vwx>TBZ zl5w3phY~C6T0@X&a8#Hcc2RG{x(KOqn)>}ps<`FTZSy>GJNMJ<woLH+ZL#A|J3gss zh0}L>PN&e#)aT$b0?qlK5550IqYO(+UEb*>dB$LGTc&5?$*Al>J^RvQC@gIYHRC&1 zJY9YB*L}dYL$4D2t><D2pNX2P!F64}ZSn+fi-^{JPfz$Q7I(g9$|KHiPJx}g*EA$h z*zS26nZ*IZy|0L7hiw|WNX>}e#IrUH>O8T7I(ZP9!!%eZLz$CHBBexvqckA-QaPv_ zQT(=Nh3_obqmIr^B;6+u4Bz1Z4)zf#Vu#pP1w2pkCUS(`=Xui8`eLRmwB!1!RAqYi zk#b6{vWL`}=N#j572JaV%QiA2L9T!$1lhHwzChBK!COBg%;3bT@M_scQJi2#c%omD z3r(^JzjSwr=I!pspXP=r1_K5SHutDe&|a6?<-Nk;V3tVpw%ITSpxSmK>w!Nd+x;3v z20gsvV!rPXi7N3x&@C0tlR7*&Tep;TC{$v#nz3O@ZI_e~R45F(OB||&VKQ~n3CYN% zQtdpT)G@IOsXCt^QkOHA?JjsoJIkoMnc1pKf(C*^HPSGFS>{+qkjH*#1QC%!4}0!( zj5BIA)2}Sr1`xP+%QBo%Xq0|5{)#*j%qk|N+{$-rS$8%m%lI3by@Uer7nXV3s<lcl z6{KM&@>x}2zFIZMUJQ?VeYkoq<S=mjd^M<zVWtaaJ^`EN(#vkWeq0%|Us4e0%XhV7 z{=q`MJ3JvF+XA9j#vy@(s9hVU`s;E!LkEuz0yS8Od_vOge8pm<IXTJ{eFyyjiE$j@ z7CU1*Ne?zIOI2vWs0}g@1^R<>#VfSRH5G$!y3FPY+pSmLd@}b5KOk!hKOYVh*A&;T zv==kQ-<rRyJ@>eovsZx<$ghlaETdd{UKd;bUpQE%QTycCAJEpie)+B;W}K$IGZ#YJ ziyMp8SD#C4aq3W)^qQ|5qKfLA4f8sEx^ZtY0H-}!iS>-Pr`uaJ$3Zk_7Aw$^^jZCV z<5t)!nzW?sqW)zivB&Tq5+cXzthi{7fSH|J4HwB}s!Z%C*ty?!r6!ALN2RV+t4g?l z1N~fP%(@t{_TJ+%s!`7e*!II&BQ690Z!N_X+jOR}-SVqu6ild95cO4Mcj$@R3RBz4 z-RNJzrtWXT`@1xN$%c}!H%RrmU*pv?9d>Ol7PSLruEVkz*|;dEP;^}P9avYbE}L8C z2gRo{n_fs>sZT4x+PNPaNqyP;it!ebtfAJn90TiQ3?bH#xjv1mW|V>OQE|kpvIrp} zQwk}g>)N%#jIx65cyN`J@G7L_a0s8BAjq)`tDDi_1I>{Dwkj913LIInY*_$5>b99z zv3|F#xIFH@`WY7Qch4efU+f<&Q(^vlk&V@cRKryHaG4>Ly^Vch6(iDfGhO2+6~f`7 zmC%q975?tgzoY+7QH+FBmn#eKr?$%5!~Jzug}ex!-+?~#5YYzC6No5-A)rD@Vu7NN zNzr<N{FHg<X7W|HTrn`r17Mjr8kbhcjG8uryafQZwD09xZ<H57LK}_>u#uIi*>14` z05UE&@SSvWqi|1g9<2SK@ycvXq2Sv8r}HnR{~PHvhH#<UM5gEOhN<Si4*?q!#N3Wv z_9Wl8*~m?7Ir(6tZiF&!HsO07?Smt(7iaz({;uee^DVfwuRQO8J#$mIgJO{t9+En# zm0i_Bf)cDeOFI;yBA{Z~(%f%Z=m{9}lLo`#&e7JXmNbP`f4o`*0XmhzsV{fRx(E(` z(kM}FGEM?M8{o<SBaX4Cnbf(56AjbNZWf9ncNy$YNq1jR4)wDqt+!Y^9j&oTmQ&Z5 z;no1bU<}~~Hg;3Yz)7(nZ>k^G&|6~wSU}gWdvP6?qo`Z;h|cB_<Jo`A;B_r*1($!O zYjZ0{_(%9;i&yKsFpuw8H_F*O-Q;dHi-J-<MJ>Xq5u2-H<1@77F{FcKvdxgl?78+f zUfu{dn{}E3k(68(V84rQSXsDTAS4Bua?s5sgim#C(IqBMsT;Kib~&GRD#L_iF*~At z^_j_f<tAZL!Sf%@UOFE(Wr#+C@cynL<|of9VzCGWsuBpDH+>ZSiMz<si@iENS6Z+j zb97;#a~=spQyJ@n!z4&FsR~h4^eo7+#TMTrLg%)WN#WMh*{j?eN+GXfRWW4JUY0Z! ztkCEU)&-bSeI1)AeupX8PZYRLXyR_gWxHpOU!5Lu2lOQ~u~+v!xy;5WJjRe%j{;lF z^c(j9_vT_A1fOnVD`R%{7o7<BLT~KVvMwI3j*{;05utve9<i_7g*vi90`B*h3^#-H z=FX`HSD3G5xM&Gg+Cd3Iz0HqbMp=-EqpXDbs}-ektHBLk55_F^4s&F{>GCY{u>j@P zP{7cS6RNSkQ&BQouh2(qA=U=EFk}tz49v{LcMWg8t%5xhC1u1oHig8lfG{I=F-Tf4 zsECeMB3{Di7YTKZG_Hx)`leR<dP4)GQaj5YbgXF)v4YKJlk!tK!&{nHoIR<(__3KV znbbpU8xXV%P?gxxm73_ZN<}7<I<TxJ#Yy+go!axS&+*Bk;$jI2*hnQ<{p`ghnUKI> zbtPLXLJ0Ms6nFRh%!`ak35tt^51SwuZG%+}RxCQObjl&w=j2s4KGS6{i>ke;wu9vL z<8*iUXH}1u6u_3ytsZkI(#|Qb@`}*mJnCQT?~sL%qyFq50_*^Ai}5yfnr_nG@J!TC zwa<}lXw|kv4GZ$OBX6G58XIO1dc^@4CwhPC(~$PTFCPq|xdOzH4NTq$B4KUQ&E~_% z3N9chXCtLxqB#w>Sc~{jZrLeCt$%=rBHtf~4_Bw?Cy$sqO}H#z!K2&>h_I_na|6O_ zxk*<)xKV7@8cm&37EWceadWx_a2H(!mbXhAo68Lm-RQEwW1rK*wPL=A<JbkcZPrnu z%I;_Mm<guZqi$$G3S4b#JjB(*TPFYwG4&RO<kIXQ3{u#KVv4;7sdesQ!Eei?17g1g z-C0$E2vC_KB?32$6oBi$Y8>7>&*9Hv&+JV&jAG&@@r*rAW~kQ~_jdV~XA81H&lu;m zC1(-YIAKQ=t|p87JhnO4i#s<S{x(X;OV4?4>JL%6=bY&MDzWzm(+gA+m9NiLka+E= zL)>69qUPptMi_}XD7aCVFeV?8)+<Ic&w?Pqml)JrDdDp$$Dkd1X$UrD6%R1^b5npC z#*t^|12e+kBFLs8-DUfABYwJ>o-QPYTVwP^`qaN);e#?347ZT@cos>I2?@#6DBpw? z3Rz^$2OLUQ3&HW=&$VTd^7__tNm*oywR15EhnzX5G!9d}&9Y`0{u?Gq9TMJ1Wf&Oe z&JgBr#{kVW4t0VE5wA!|NT4_H+4OPFE|ETsLw{tGb$Yr$HYl~h(BK{Z(iiD0wa5ee zWPQtUKe~x=k)37=l#NLtQL@czq(;Q|+=SieBx>1M@F<d)b)kEZnQ_yt#*S)DChe-^ ziOsfzuPBHIYr*mEq!QylXM*{`oZ$3QJHg&Z#apy`@ytoF$BC&ni^TR0^AdHaPXFJ9 z{HnmPntE+x5j&_)5ED;qC!PpDxsPHZQRg;_wD&GnA%kkB=06jvmf9ffUsHOEyh~?3 z2Lcnt_o;64#roy)bODcb&Q8~mwHdGFMYb7!XFA<E*I|BRLg0EQ7QvW8y58Dai+UAS zOZBXfKB;mZYlEmep^pR58Jch1dCJ|Wyxky^7@rx%?66a2Spxd?B8nJf*oY&QBqtzL z1Um~gT#1wXOZ$ULn~xiY=xSn(jVLM|U}B@uu?oJ%J?~HqmC5?(2v$6IMOAy@2qeB6 z2##X(0iu_B0+{9IVXoy=-W{};AQwo~wy4nEY=~3d0Qya-F%{7^k_UEdg^8_2u$-i5 zMan|hly192+|t_9d^;!7thD<i6pM7s#(ocC*fqGra)7h(!gJ?3jO~#Vt#0{(ceJ%C zX}8SAeYqt5d=?Fh$(9P-rPjyZ#JEtcT=+s;9fZzWR;Qd~u^`&hC|m)Lg8ie0N1}jt zFaE6ndReHnPj0iDJK!M>xdF&|-$gABGnScY4aXT~NUX4GOc(jm4~40rxxB^&{rO1M zBI=kM7G~{8n~4PXsj5YXw=TtD8tW-Rc(@pS;$in}v+cFd#hqK{)&q-dJh;&5dER#< z>E7NN9ybZ;HXD4+hf$$2gCl((#Mb4CBh_(P_5X*Wkqgmi3VPM%tcZN}al}e`f4rQL zi4GQ79B=6_PH#!TaDn3Q$2~`y(TvU1#V9I_s1olJ2hst4BWq6`*NU8v_6&9+jSSle zQlvdMP>ymx%SP;L#9f%}NJu@p88I>Z1adGum`PCR?eRhD72h6Eyf?;@I4s8~a_jJ| z|LFtv;)Bh&;K&ly1bO~k{pIj)>3x?$>=gKiH3JAQWD6o}o#7Sq4YLD8{bz>b2q{6* z1RICN<1DJ-=;k~<7d@dB@V^`AV`t~o&8%Lf0Hyj&oe^qaH#LVZ_C@~BPCWR}O7xLs zWH$a1&0Mm(G;WbS4ApOiK^s}*iH1QipTU(unn1#Q#v%Qws3_(uGg2bZ{r)KF-shDZ zt1f1`$-7b3r&)UUc`WXnV}7wdIq5KRVx}C-ej3A!W1=TM1?*s`ntde7g=%ZHQ1O1? zauOJyvG)j#Od{?>U*Z21+TLh{_hk_ww}m%Wi{QpV1MSg60s}Fl0a$?VDXp&*VRc0v zBZ~izgE_vEaU?bmg3D4Qu_Ydr&D;cX#*|GE^CI?f36#qS!mnZM+i(n#?0jJ0u_CPb zUok=Nd_pb~d{WY^v1+QK6+g}lY!VC9aseprNNMh<ZdgO`c0Qp%3O^}XjaGJU!tQin z8M@*dmiO6c>jVtWYO(F>5|aQJ2}=F~iu#-+|D|rr)-C4W&|a2?nf(}rIt^P0X+8Kk zqO(5Vi`<kI{7qfWsKyyfP7?8tn7Z7yEXf#?j<5QKaK8EkYha#RUcifHw&t2U63ExR zF~4=;i(z^VKPNvCen$EI3twE?LuLDPu)odxLQ!JxgO6cy4c{g|5xzzF?F(Pj``+{5 zA!1Z{vGsdjRYJSTE5HFDJ1UL4fVxy16(wv}?9&h8B&zVU?1g>yy8^Y#XNR&{7kWZo zjOlpZq<cCLb>~opiN(JF<tG)G5JcQ7n1aBmtFZ^%GsGIBfRqpC=MG4;uKuvlZ~9== zJ&UY=bWZSSs)EoMK#B&$4xh6I!rS5l5YAp+zXrXt+JQiXt-yz=Fu;%1FF>M661z3` z@xfNRcaAij6X}@kaCd7V>7v&=T`H)5*9@s5r@TL}PCBZhYbFI6!J)B|eXVpmrA%uA zaQB>PiE?nG&SHF+gI0pTj5NWtFTiCu^m6~a)@O%igQ{M8(%K`%QT*Id9h>}LsG-fp za?ZEjm4TQp{%A}iY(}KP(o5}=Xxur1JOrm-o%BdJFT%(xQEQEXvTEgrkyjPwJe_gq zckoNI7a?|W0owI~*8OvyuJWSGqhwU(4(Q|Mtg$eT?9VR>pkeIayaR=Rk=K1uIe}_z z>K&>uwwQ?k%PZqh;OIN6guKWnEBts<V5po$A&b?t!`;0Ik&s~2^9D3T)O!bO!k<=H zH^?Z$?UNNg<^{{Pe^pR>TL5JK&SU4y=G+6L1D@b{TL4KyG+b~_I3IvYWA32Bgq-Y~ z#v7TxAj?yN9E-HB8cqzdv^*`0)<tw6Fe{RY$Cz9hGL7iNKOv#2hd`QAT@i^CrSN!H z0<-{k_IQJi3)#MDqLldaRcj>Bf0V#wG3ultp7`SenW7>3YE%Dkm2;(RguJMp9X85@ z3iD?z4oVM!nOzv-*xAUC4nTh!VI_Mva1c}frbeakSA@N?+2bh`3d0gGy2)vJXIjE< zn9jM*wrXrbnF2N3LDW8J`lqKyTDCOKBg23`rn?<_3wCKR`;;N*scYwHH6xraVuV5B zC<;Mb&k~C8s10<z1B?``ksptvor!XF&$v5$B0{B}h{dCcnZo3~lP^A!_m_a7#vI-4 zOo6DaW+g^ghx^Vv+zv5Bpd`-~bff*!m~CPdm9knmqZJJ{M*#?Zo~Ro7=pfh<RHuZz z*N2prtEMi&!VtyC``l!;i=1LdzMf4<j>Gkl`;SkJ{W08FcC9T4US2_Yg)M8=z-VKK zb7O{|g4m>D+!VOe*B+s@0}>g6B^ni=5D0v^kHny>tI+gGw!~vN6nl=kYiJvFG3#VY zE!(#6q2h|Ln{Cy_^k}9YlGz4qqpDI^wnN}J1wrDnJh-0-nw6FziY`h}e~i7-hxmJC zI*s?&aM&gp@_3lyF(R2d8HQ%7`*)K%AEiJgXJGzUW2|L~lBb~p0g}LNSdNY-w@K2N zK@+@*DYCY=Z5`v7@DOPLG{Q><dt9Bj-HP(Fp-9S1B$@*TDZ#N$d(YDdllynG_7X1= z%4^Jq;7R>zz35*~OK2fT^|Q?)enUbgnlmt_;7)FL+I}q>JI=3xOC2hRCeCSp#&YPb zrI_u-j>nEIMz<mis1Ao5K;6OjO3jNtc__(yB$y>nbY3TZMFEn#Qzm3W4j$EU|8b!> zFmWto?{FJETKzkOU80M9EkX}d|6>9M^Ct9v1Z#sa?8YZb^>Vlnih;|R-m8-j-?KU< z>@<ocNTpO6@gV##xb?dj#jf!JSNx~cw<)VG?<Bw`oCxWQI37xRx(*a(7|#NFA!C?0 z6JAfH$5)j}JNA4iW7Ud~%_d|*!2L)lCNYvSUZs|Nn<wKV?vwB;aUeB~S_7zOouhG) zHJD$Ha`ZZ#;h0Y&?BY0jL$dRI16Yxc{}{ptk<f&bvSx=K32-o7YbbFAc+i5)U%7%f zDZZ%deG^AvzooORr#=cHpbzMog}vJG^4XYw9}1b(F<Pb<=~T|D#@AaDY+AJOg)*_6 z>;R{|j5&db*;7H-X>Haq6^h;Hdu6Id14il5-A^!lx1r*!;slOsZJv8u91@meR7&Qi z(EFwVa?{2cfDfCBL{6Xr>V9|0v{SM%pfaEZFEXDJi{Vj6$#lrfdY=egH%dIv8>$yN zqj@XF5W0{;4~qAjTjtne6kT6x`RjY?)GiZShQKpa-rGltLdG57yGvqT_2#6EY0bHY z9<xo!VZ&~dk;U~!B1{4ASWQPFVbG<R8Wl$ZgV$3~YXwrA5u}NJ6ra8Q3A+EA97x>q zo%vV6@~+8Ua$7i*rX+xDZ{=waRtaNFv5_DSnB3uLm;idT25{U^xNDLe!rL&VpbGav zvG`Gu(5n7+ZcRKaTnZD8g2iec^|mtkZt{~+KQDA_Zkt;l%R{}e=xxbYElNBxO-|7r z5K#xL3;I3o0rd04g(17!a!Vs`XijwwYykQ>J4CCg{{)+AWLr!y*@>r)oa+Q_Va&Rx zJn&?gKLl5bNp9f~w^=S7S5z>bn(2`LO=BM{{bC-H2Ef{?h{qUk^at7;15~t5l$4M% zdu7zy=KSZl5<c2Zo&xiVk48cu<-bFH7Tq@^+{X7~@#>#$a*K$B2WIb8T`+&~PBpNM z-@(;G&8o57vYqO!#D_Czt+}oNwrs_T<>}YlIAa#4T)(FtMD4pO>)-v(h#g@zjdH&j z^;6`1)Y-U*vqxvq7VOTe9JK%d#ZpdSgbD@^IK7*EG^uPf!C9M(dI%II&!2&Kw|5ru zO9v2hB-!L|`^k`wvj}~e%H9iu!D!6b;LByErs2O134wn$BFzjzrz$Qrz^@365;X~Z zIo#)#_lsk+?*i$_TaI=ycHT6o_ohVbUQ|w}sVg`0h6voV=t$7+m+EC<zpb21jkaSM zOOS3B(HqCYrn+fO7Bwt&h`9u?>OQuWDYlYVtM}WU1Zh<t5DAJGC0G2kCsu{-WDiY{ zdxXl7{)~RMN;)LYh%)nEsl0+l^^<%!IOkCO=G)&rDMeJ`N{e~fnZZT2+dP%Qatr_6 z-Eekdp{p3%_<fj>ID|pL=naV@!l+*wOqI^|Kmx6@j$#v#YKRL}j)K$V`o9t9M!0uA z*==w1GIG^b?=G=wE(Q?x_SGW5)rqu3$tZy40iy`10uK(*pGqGA+p5@OqG@efUV|VW zCv+BPXzdLB*rZNVF*o_~iOV%}i~W-qk%eOY3q7&B5XuGTVs4#dd3NBdSW-!wBu%5a zv=?H3`*qXI9(guueFCv#>KB^r@t4!Ki$3BFig+c6pxrki5s49e=l)}rysUX#p$U_l z@fE@w1T{S9koQ?lg@}yjd;w$L2l&&|EAM%DY|_!Q;4te@)I5Gen`2R7d>J6w>#8P^ zH`8*f8=Lc_+;rpapVV~Im)@{4NHYvhIP>+TbRoVHQs&s>Czn<UFZ<TmHi{K-=#e(1 z=8};!Y9JnU)@UV&!qZyK9}x%gh-u<XpPO`+F(UO1(xn}9(49|>;lPWd(lcf}c38>G zKI@nmB%{x*u#tD_TQYUsBw%*l;-d4rUMZ_%iciK&3dw8ujNrF)fbV)alCR2NSg9k_ zvW%-~#JMxaWdcX`7BJzMe44nK4|Gpd69LvmIpwx%G<N3iYDe{~I{2F~9;wm9aEfF^ z&zp{D9%bWy|2BqPnQoisyCb*y?OYWY$4<$Tc6Nhong#(6zPVy`>+Ep7KVMn?pa0nZ zrnkIa<-rKmkaa`QH}axtltC}l`Wb8#jkc`PTM@kdg4}P~$zP{^E*=n}7DRp8&{V-( zpxDh!0q%fDTS;BgJgAxYE}g6om_i1cuN)f%&ENVs8q`etn~veb1G|e(AKrXaOfU)F z6!=-AgdF>|wW?1E!$YtrEea)lL5^N@=L|n_6jSCL$4t@Y=cj)x`Zsf0{@=fs|F^yb z*W@X$xXuQ<G%eBC{pONlw(jn;ZX{l!PqXX*m564rNW-9M{M?tuOjjU}Pknl|cn$Sv zFdt62Y`F~$VuT-8q{f@SAsW0}ToNYnh6}>uLD&p-1e0pUeoS~Ur|kHiV)e<t3|_ES z@90;)w7RX(%`d^;wrceHImUV~ubRKE7psq5X?o~;Xk%Gkeq>QW(8x$7<8wyk$VgDZ zqOERj-a@!H9kwqh@V$TU<3Ill8uMN)%*goZa{ch|b;NDD!|WR*+8r!iE69~dHj$nZ z>m`y!0zo~I*;!s)_@X6CTf9O-TD+Dn314JdN3b(qF90yMmGYrq>7G6F)F)f!YOSs1 zxid$lkrCmEc8@$0?IOa_$eE+0RBPJUy6O}2)q9TnmEN~a8}NKe9^)NGUX$m)=tTh4 z0%QhyappG|et+g78h770WXE<xKb}GqG#3}E!pO>v1d7H+8s<WY*cyIcg!Q86BvK8r z(lmQ8A^EUO6-Jp&a4Kpe{&41qte^-|5_CzAZRNtpFJ6>t?|W4#Ft6(SU<Nxu49zL$ zV*|LAg$4gwS=ml2TnPz+^Q35So?zg;lg|WFQHq+xn)pTY;$D;lljS3*DvXjMRhiKq zl2bmSLc__k3B{_>MCjt+8G(+V0ufoJ5htMwE71@lw>hD7Wd7Te=bX~gJUoYgJ21NE z@ssN}Z)LAqy2_0E#hVA|T(F<|cW&-(Cm*nQ&y9<FSyGQHEBjgNjhV;=PUlh-Jx`$= zBLS$xtsoy&qv4cxllgI)7gSH%mzubAlcCf;rBV*7WZ`6avP?BXN#$j}dGL|v?)v?~ zn@pakrpu;GM%4#mx8`A$Yn)6uW;BO<i9Ai7-cucEZ>H$AB#*~%xlY&VrggEcBK>YW zk1?4jpr~p1N4;)3HnvSDDuUq56rzy^L7ZT7tAD<K!2k&OH2hCjCbJN^R5gttjbegD z*E5K!h&<HehEf|@hy~N_A*7uUZXnJbB(AD1UatK`8CR<!DDo7wdYm%rZu1BEv|1fb zu}oI0Pm|v`f0wi3v3q0jgqA}pG?`Rt^M?K<dBoz^XW|&jM^RNgDE|R0#{%S^qEe4j z-T?Q)-+}zJN*zIdj8Uu3kin7+b4IvINHyGaBR2A(VitA&et$NDw}O{3JNpl>f%nhs zgJ6r<Iix>lQ=U?*BPd(a=A*PD;|gSmDu5w?Q{|7Gd4KjJ?*&n$8X00)TwG*ab2B9Z zO?nHED?bZ}oB4temqMnwg(#4E`20$VSNhFv(zf|e;g@h;!fXcJXRYGA%3Lai7F9~g ztA4`jFkdk1nT2g_>zEjG_i!17OkE%8f6thu(|=@G$k+OT#&j&(@+AgVEcw|ZT8wYV z2><mJ4N`62{2CsU!N$by$9?=izUr(Y=19(oLUg@Cv6)Pj4=NN@sBj{el*4PCu3q!= zg$e6v^0m!Ze0`lh-wvbEs<Ttk*!U$OQAu~~?Qb7of-N4@+Tb=%&zCK&UWPljEm*Sw zKSavm^Fy46OmiV=Kd~Iv=^=h``=vNrz+OuHs^8x&Zs-doz@<H9^LB3`g7qOKkr=WO zBOlvtBrr#glQrTZ7V-Kg1pE&^GO)}Z{%H&c@UEYZnz5ST?<cwwj)#+a@)1-vN*cuz zW^^klm9!P*C-`6(iyk4%t1&}N531JU^qkWl-4fn{3KB{tgPn-VK4K1^Q_OitH5ZEe z#O3UQOt9Fm_7DL^>9a10d-X+5sZMDtR-~;;QPEd)BH~0DMXX05v17*yeQuxg!U@DS z_Ja_g82*JxFYzpP{YOT+PgY3E{ybcJX(lv8o~A&J>}&Cr&4VTuLnP-YLT8mWyVa6; zStj14Iup*~@m9<D(8$Z<cYoq?PWf)}Z7P4IZnZVNGhmM(Sb+1-6ec`sX)U;Vl^f9S z5&!H;w$40U*g5tApU6{eQGlB}Z*1O&AI{y6ZN@%u-@UtBA4Z(uvs*oSg-!~p=6O0+ zEQ_UA=zVqFA$H}3LiL|_<8M~13Awfi06$)2H9xHw3tH)>>=uJaH;hyeaA|N+Zf;AW zmTQJmzFW7Q<hQV0?RM)Rn^9S}Q>PmdgGL4T_nNu+h>4-kR{{Z?@pn@4-K;X~ufN)& zm#&5}r1FU!Nbj{TK2ZKBzntM+*#-b$Z5$q$7Ti@gwB-ZVfSqhUfmd)p{BZ{BmWa-) zFa0G+lwoZ@_{Nnh#gPyjX}>83O2+m2-NAxj0q~zD*f)1y=ksc0T}6&Xw_~BHTyT+W zo{;My@&<XO#)4YwVV=k1;cOA1R?L`8NQUv$5Do&%E0m$w&U$m>z~|J^MjtS_XX28N zPjvpRTmEE+3{|WbY?@7V<9|y&p^NQP@$9#?Ftcv<eiSslzRLgY&-LW}oWK|FUFV#d zhScadc@3K;*X5^x!Jh>T)eBXRP}bulAQVJKNaEH5Ktjj`nt((Aqtsgrl&AMeCJKFe zX~vVYlH(a-<T&U0{UC2lN1L{}{NFg^USH=5Te+~{-X&#L@eh-irIHox)0s;ia5Lp8 zjXHu-kLQS*z=#k@{A}Wuol^SXuxH^61;h`2W9tyOfe6)^r`4KD=NSanMiHh?VHc^J zFPM@uiL%P#20-jX%G`KxuU>b9l6g|E9}vlhS>&<t*j`=yFHQ8ICz5zdvfv6(G$h7a zbUWpaV=SP}w!`a!6oCpMk%Rj?oPFk@@h7((KJTH|J4{U-w&%eN(?aFnqZhBRsY?IF zBVVB|=<AV-(-76BSB5T)%loyg9B&1)%)4)E?+Z_ddnzhHMF{3xyxby~q!B|@5DXML zZ6jcKk%Dlhii|lE7n6Ma=4NphUaX5$#gLONmoAJ5UAjUdWvV0_#IEqOBb|f+SzMee zEjh7u;gsEXwUv6xe$9(p?c}4|GTIY<MlOq?;&fJp*sq)!A1C4UUY46jwApw^?ix!= z5vqK+9Gv(Ce;cL(6{LkET7h37KIbp#b>wT2tiq^QL$XdP^@~ZTf(l;VDfK#Yrph8R zLYLTwlP5LV%uH6h2=C&|MJ%S6)o`m%?6fYnPZZe8<}of2#Z@H@OWKIiW{I?u;|G8s z5D`$oO>vpo*-Hcp#+vD1@)q9(3&Iz(G!+Pgm~LbUFn*}9=F*F9l1f@Qp*R5&1tkwa z7Tr<-#^{~alDBq$TEiAY8iIu}m!j4{a7G++La#40{|zg+^RmS2=Amkf$Vgq{I-I;g zqchFK4v6p)XA)BbhFUFdNv|Vls5b~?Yxl!j!uo2Tyn$5;1wQ_ke^&3-0*}fLn5MVc z?%Jh@NCzwc0t5uW0ih97(5n^sL}!ja(#bU{Fj}h69j*{c0ErWGB58_J&0;m+S(4RB z6kvH$+t-@L`J0&N7#$>O+Jon!;@6cFo=I%bPl^r&O9|n88gh(ua;q=}Ewar0;lwF^ z!uW_|KqwVLPZB5zcfcr)PHJ2G(l~$ro#YraU83JZN{}i$F|1iA@JVm^XZ5813DGvY z(H2Bjzym$}B&bpV{{wR?l=)ntU5aqKGq70MZC$5Vg>*_1hE0U=t(HxSE;6}Kxh>_O zqFNv<l^wQNqUtVVzI9`_inf>*?dzuHR}K66#q2p!fj_W8QEsyKkg}UhKBz3Kly`P0 z8r0MslK7AuYpT;%x+x+5&!#w*GGD|k;1Sv9bS_plS=FiZAzhOAPO2=p)v8gsnT%~# zuA4uiXypjYq#S!JQFWjypVrrn(iYRwjoq|r<Cw2YY@jULVnyTU3b<Lhg-oLS&E6eD zSXY;2W!04saeSiQioeWDYU->Mzc*SppMS+N-My^`$%2Wku(%!J*Rmxu9qnc2Qk5E* zmBa~xXEgJRBTUQl=FOikKi7yD*!*H*7RHO!NQ?J{ED43{ZAkgKGiHw_&sAD?pUqji zbcvabyBXC%{3Z#!hS*7L6-%1<&`6cJc}{+*T+Z2U>&kPJS09}{+nDoLsVZGjqy=Bh zNoG{+SDWFcbDH_7sYx0Z%g>hQY%nLtjJcJ5CR>9bO=7XtCbc8oXU{&uq={zh#@l+G zWtAD7%NmQD1{CNbEGBfp;%0;&XPVrBOFo12x509EzBA?KHtEZsYO?Hptfu<^tXNX| z!IhDd`xR{d@MiuyU^aV#FDGpSS1)kcu~QQk*1<>r8rhn=Vxa5A$G>evh3SC-<$BM7 zP2^-O^=Vb_EVT)mpKi6IPZJt8$mf4E(qmY(qVxHSCpN-@>i%{m)B@Sshz_?`|7KL$ zYHT&8yG_GPCu*DH;+nOgU+5D}YN{(UHVwLUM4%m=qLjmjQS|B`Qbbm^DD*399dmQ^ z#*M2OjSP&D3aFT!tq4#chtq!kSEJrr>sYJwSg{z1DQ|lFEyO1V*}lUA%-BLI<9DnE z0zyWNA^D-AhZv_;mg5veVF~Or8|uaJg@}+ICOra*iKr3dNPZl9EyUMFgdubcjM|T0 z80skWzkW)oI(ehezhmg|VTY;1=Qe&fPN|fqM87HNLcd>OlB21AQ(9sHI>SANCgeA! zCW(?ng+)oiL}5d6d_4MHnE5;78<P`7Ny5Trw~MZ>$KBj#$?ji*5)%Rgoi7L*=E1<1 z+Adzn?CeKg5AUPyujQqh97|#4LN!UfV+ChzCnZ(Ypk39ON$6ZwDXyALFw#ootq9!2 zXUB=$Ob<M{OdeQUOzM(KX7D*7CRw$r3w`(H_w>vz<nb0HA#(M;cXqX<9_aLBHQ@6m z5gQP38tLvu<WyK4VlG+823K?)=}%Z&ER|kC0+8ERhZW<<5J<EPhhV!wA$vx67gBj$ zv9##-RLQT?aZug_o-;zY98V|=N&?Dj+1V3n^%6((VV32T;jZGdH0p`$>}wR}hPGeW z{ngCe4gJ4yT5Fh9i?b%DX_@IQ0|UFvig7Tu+9!h8E0HYXx66qhrq%KJizMPM;#A@$ zaU##rlW`d)*AXpAtLO6<K3;7Nah?4scHWiDuj3Fryp0w!wZ5cPi^K3-C}FBM@xYX5 z7KOtfUm~ON7Cw=nffAwm5)0h8#<+B96046$gi(qulT0m{@@Zij;(XQ)H5L5EqUxbk zc{Qg+Ri#6Us}veTSR>i&!O`_oW_f|7yX>PHM)@Pd&DmG$1jc@5VJU5vNIE!e@fUN- z3AT%=+@A88V6LD;s%RkbEXVHt*KeE>$baG3-q+vX&h&EZ?eCvt5H)24YlGn?h#ezU zW?WJ!t|+CS=vD%h26i-^$mI!6e@8iOP^XaVP7*1MjWo?>v80XIP3V${oB8ZIJhSk` z6XaBb`m_nybCe(!x+&DWA}dK%rhhYu)Q$41q?Hxn$N5oI_#s^t<3(C4{T1D>gpXFz zgT_Mi1LAX<n~9{&e1VJn3aqJz8NfI^GE^$`>hj}_(AAVMSx%S`gTU2%D4>$Wm_%w& zF{%-`fzbY1YBZdDgdUN&5Ekz^o&f&NFui|r-P5o+PDRBh@noWya%5xwxSN}A^yoMG z`U_7ycYqraD2?yD7zVA|TE&Fi^Lw}9k^v{-RS?n$m*OW1`}=M-#(m}G$NH~5)ImZb ziHc9!T1*amx~{Kgdb(-rr9wE1z}n!M!X5)1L{l9x3qfkw#)~0Wn_GN_p2Ng;gdZm+ zGZt5_AFSH%;^HbO{)j+PDQ+0;M7{>ORM?}Y=hXCEDnv>Myc>aP@g(!mNo!td;_j-g zgOHp^W)wG#cET6%kWWkV)ez%Fwi-d&hydg87Bcx5st%(xjZv~}cnLO6mxk2gBx$lr z&?v_06tI$w*0qfTRl`UtVGQgdugUyIEl3kfRy{)um|!CTQsu9zq*&&FCEGD^qX3H0 zQb7{s5(7xXoD*)>SeYtrnXHl>IB;_xza&!65MyCD4aq11EoemR@F=>RK8R2nh8qc^ z2h!jpA2+?KIpGeY>;JfGIMzG_0CA|g-@3hFP?#&6gYDM+Uc7!ITdD#FF%XC=){0Fv zJ&xoSjq3R9L&hc=Aiu5jXkR0}Du2k&FKTaw#4?S++U+2xFl5!TLfK&0X>JCcW05AS z&~Zw-JFxk1DO{+qqA@ZWSCpnZkDr&UL5!p!BuJ`Ex6zxb@<ZqV<IQuG`>>c6p<gS2 z02rYTpwOEV>6ov7l^TEiF_Hen$*HM^@?hE3tHwM#T}ldR=IE4D)aC<9zuYR`{0m_( zkNtzqMUol1kfI{JIVPq@*tj70`xMSbuG*1(VEJ;sy0?cIp^3K)7CWbxr-gGOIUBed z$FZb*3k(ndDL~f05fuO(g$@gSlw+~jIw9W4P+}M$rNX$!sN9FaN*3Z*m+r%JX_u<n zsVGqadb+MK%_-G!Rhvt)b4mdrqdOTB1BlB~oQ<n}rBkX}P%wiBa#=0{DiQL-(Ze3H zTTI!tjsc5uqocLjfB*+KHaAzGXBmtW3&+RfH(6R?NrIp8`oEz3U+yfFo}H-S%u7x* z+`4I19TDMCl97?8?)=b#f7t+cHhebjKVY<J>_+L;o;uNuGqWFgyx(VmABK*n>^kBe z^?hKP`;nhhj{FerHaqJcvGWK>p7-VT&MeAf>PRv;vOe7X+pJsos5kDC8NMEtv=Vr$ z1a9K*8-rwoGGk7nsB&ub7+Y$HMgnVK!PU^%m^#hGGEWV_XX&H##G2}6`YuLOO?4vu zW=2SLTLt45dSz=(2;)%Drr}N8M?GE~B3rxa$)mB#VX~)|nx=)#)!xBjqjH1#Y-?qh zQPEZ{<i43Srd0Hz=qw?xK!#`{9S%pQ4Ug>c&DA~bR(fS?oSs-+-5i*bY@C5vD7ssr z7$bo=Wc)t|E$E{+SEsLo!T`&+y!db@!(bUVHMfFR8DQm*uVP=paB?t^hd?N_kWj+~ zTqq3Aq0xPU2TwT5e};3&)&XZ$CCr=F+Dyy22d*AaTj|t4tXw9_HRy9!Y0#j%5>Wo& zO?kkV9&CQ$^SOGBR<um%?uafgcl!8DH**KpLsy6~Fp^;lys_q!*egU|zqy4(>X8bn zxQGTN$-n|g1G*Ze{3=(azhVmx4iIvqXDH?3)^N4#w5yRXEde3e@~#1(vtA?m|4ymD zgdQlV3lDS*B|%WIc<S6udr$xA=sm@uuXV_+r7zOXJN_N;{L%8#T=zGco1?Veh40LI zb90|1CI;L%HDl>;W?o$1JJ5XI6<d@ZVs&Nz<;w)s+LHK@?tl2mZ(Dle>uL$<%g6RV z47IM#a#Rj^mv36jsvTIb&(iA_3*TX2i6A!HkTJTGQ@eHzTG;^{g6%M4`7VT(*U6GA z2Um1=e^1yB&3=Jcm+qYCJVLcxBF24k{pQobojWbl(o#(&nysEB;N1d#CFcd{m2ZSj z#p|$+IrHafCf?8$h~zRq-28x#)eDv@yeJNfN#in6F?13EJKI!sSZAKY{I@HZT}zNq z4E%iI@PA&!VpVNMSVoe_8dzJhhU)*x%{>w<3d6g(6)a;u??+^+>gJ_PLN#DK_=N?p zUY)(P?AbGin@A>6Z$%#G#1rfbla@9-=fon9E=(T&9O%LVs*;AYB|})&v%j9-s#qqU zpK9;<!ay}7M3N{ECBm4pe#}}S&)PM=?AFc4&o;9tOmBpSSyC8uQBh66$VfQ@o|vSo zdst0h3KANuNfS}DkBnhM3anR3u#k0uckoojaHV7Ckhp8Jq)s6=3aZV`EbET@gh;6l zyHQfLFE5|(!<kpB<rpzZ2bTRe|5I$w>->azXDePWk!~j99=ARr&gJ8S%+?c<__gP8 z6R)E~+MwuAV3mSwjtl1#q{idEzG+&iz1c71YBMBMP-1!tQ4&}KT=w1s9K&0E9~t?4 zU5g^vcQJ&!x1xzySg`QM&4*UVwZ`i6e7;)x%^&kJ7s@0$9H04%&56qLy@P|B*;YdV zAGi872+y#UUb((yvo+p{SRt0PV4|?-dR!7SqLj7?yKqF3?Q+Src`N~<#tMlcs$#<y zYg%36i^MP8UFKaZF|ml4tPtWXQ@s-zSM2YY#?gw`SMIYiCk+$>ZiO@9Ef(Ed>_dME zL9OhYjB1FD$S98pQHAEE=5D^pc8OoQ8?ml37Li04ii7}Lqc;A0c{bh(Tb7%=o?WD$ zl*{Is6N(MBjnFI3gvA*?o|7~x7TBP4SDntxyqc8-lXX3++PR&l+eN%FmOM*g(6vmh zhPcEee3y;tx`iF<IJhCZPc2U|H)k<LG!mefT>?uLRLL?$F26|^u0KVIxfTm&!s0ah z1)IcJKy6wq;tjJDT8G7pFEX&$fQ;rqVxX`{4U-m=#k=Qz@l8wj@*Ht$bn4iX_fMYR zYFxE?HOtWiCI4-|iOOA6am>kA@p><h&-+|o2Q`%6P0%W+j$Jjag^Z@ZWCh#;@3@xq zCL^@C@zNw<y#Gu|H<7eMI_C+#w1dO-a;e1c?~uGR*VhQQB5;O{#Qi5T(gLQ^1otXf zQm6no`mhMM9dS`c5uq?qi7xxuz5TPQ2nZ8$I38UFO3Th`#R~AdEyxgD%9`DL5s^&X zMQTiGJ{3c`M_n@!A4A2D$S!{W`Iv&9!de6idLgMU8d$GW;SvIGlOWA(b}`w1DI?mr zHd6}VX%z@xQPTx7n|HGbV+7uXh-a{Kn&sOwV@)%^#=)7y{H1F68(!?wxg4*=_jn_t zSw`FLWCX~AbyaK`1;_U;rbh3fke`$VE5V2bc##~gQg5M<kIGcxBzV>97tqo1Cq}AA zGAYMYketx{w%enuTUHZ7&#9|kbio&%cJKasuFxvxt?VBqV40)Vk7xR3j_cJa6n*_? zqVD|p{=Fy|jQuRVzxecl&DU<u*S5AmzZ_K7C2Q%0yK2-EvASy%T5M~bGG#T?(wciV z(#~bfl-uo~_no)=B*n5<rO3ny*_p1ZFL@)EfdYU57Ez#H69f^-pmm9c7y!bZz`&F+ z98vzLswV|f;3OT8D4J39;kN*cQVv$tst%GVN7ZNwMIK$F3<jZPmvMwvw`Uxg$&=Nx zamsaoglItcG<<3(B2>xk{$S@@f_y-!)y3KO<(1Wqbb3z>07jDI`N5?p9E|hi(mhYk zXl>ky``0-uJPh8|5tcIN&|Vb#BjbW9lP}o)z6n$|tYcgK`Ep>*r?<t+SgcjWpWmKd zR#n9*Oy+Ovl~m{7iHM|jH%`2NZ)#;7;(=ctplG_m$?4C1LuYy18-|AewQ$-hRY~9A zKdAj1%a6NR@5N%5Gyg3&7_4)0x@;Jin1hE+`wngAMx<KX4SjB)f9ds;`^V43%v-#e zsl7UL&z=R}c$!{0v5glV;pRT**@1zj*RthKkH;>ax5!j|ZDz)f^G(IyMIu~1hCSOp zu<FG1&8JT*eusdvzcLF!Jv)u2$<@Lo*f^WOuP9dgkd)%RX^lizJFswWmFxf+!1(T} zcI*DZ24${t<`n0}H`XIk135T0fVS(6<bK-#>NhB9GpFL>L3vat7P5MisL)x9X&1QN z0ZLGJzavj)?t84~czihX^zQe{qx~MSoSgf}n4eCQL}(a{f@BK_)NXKIzmf4|Jh@=a z8}}{+av(-4{*q3CA&(0vYNk6$uuOTBr1O;W#H9Ob9XJNE#6RGd7D_xEUi=dYyzEMu ztyD!4(j4XnVl529IFXJzr6^?sdCCbMl5p}N6rDws3Y>urim{1Xx6I7mJoDp0T{Tx& zAn{qKs+%0?jYlb`lgC_B8YO6Hl!vCt2T?U1Ff<+mE@u^@8#IWdqB*_dEcoQ57{tME z6fSEJ`unOWl_Y21CqfAvYV@E)z!Fr14NBPHu<mX--e#j7tEviMzV6S-^83_t$Y&yD zU5FSF2>20AqXoT*&&t8V$H1)NH7|!j_m8e0qnIz%OFA<=7KNuwH#Xi@Na{(XE=la| z#-@jJ!rMJEI;FZIbMjs5M<?xo<iQVqETA3HnmX2})9p|&`f&g^`Aw``#`wpY`NrAb zF9`4G_cX59Zj1B6s=38k?UueeQ_DMnS-IzgB`y2zfJHwcQw%pD?6^uYXEN=xYc#>) zHn_jzqcJ{)c7kZ7IcG}wYZZz*lBu*^qNtGaXL30{{s^tynD0#AO~@1@89o#zO=et1 zF@Xuh_IFvG0=OdIBNlG?aHl0GIP}-@8bn5_>kvrSLI}Z)L=5x|*;*y&b-Egq<s`O^ zfNO>$s1khxfT8ep)Wj%A4I^4Q)Pz6-zr>DN%2>Co+K--Fc|HKI3|H6og1J$37|Aj^ zR;>lerLUkKFb9KA>Kk0>6M&4lZ(1l6Wm7#8LpiEiR$1Lcr!T4ot;kEtII2dheuNry zobuAi!ICsd#y<x*lkp$+RW6uI74S)#&0yIDW<q-z$Xeoa<v+g{!$MyNqF1vA*jD0X zaoKtESspAf=Na+bKetj@Th&-u$GSGr69*iQz>Gr{_#w5MKm`1XzkAkzs6Fa3;KJPW zB?$cQdc>B7bL*~5>-^`A9qr4@cp&fC>&(_Nf41>Ilb@<raa7#A7+ZS&v;nR{0OlX` zUuTyILO0bJRQvZc)Qr@Vr@<P9BG6x)=Oz5!V!(2}?UDKOak&HZU1A@2C;oD-OY+BL z;gaqDgr|g><}H_hd%Ousw0Xk`y_L&*Neh>kLSR)1wh&lc*4%r>BQ?qXoeI^xTS)KI z*}4|p+0Z>;PF&K^*ul7;=a~O7(J^g@MQZANQ*q9+V(ocno#o~sJx67-chlN#4%nI% zD+lr=-96WBmnPeTy0Wa*zDu&5=YGAXdxHh(;>O1POaPOE(nF|QVQkXv``x0%jH7H8 z`b;Q0LWwz`M2pB$d8{;zu~i7%D8@ry0kK=Ue=iUqGUAH-J}As6ET}^4-?-!ryIf4_ z;!7$S_xLZuu}noI@s#s*tGj{Wwjvw%YTEm1uS4UvhmMaU#p<5Ds*R}%m3(vsU$5bN z_d$9V-0G#NaJsPo#63^1dB<M3IW#`bhJif>4LnKex4nS+z&N6U;f80nvfw5N^|SvZ z!MMgU%Jm{qP8<LpCgLvUA9)Rd(~@46lz0%E@Tx_%{rp#8Ze<1V`GX4Pq`n1}Rp{Iu zB!0J8Tt|$~PT8kW070JL?;m11tT&sm{9r;okF=O9M!@^l=gxq@@s+2&ec%Pg?C%TS zaMjhN>gt%7gFagAH>Rk;-L|Ot#0(2+Lf|$6uvQjQ%~qw5Eu&*6Siy5lhuoOAbMMV( z&mKLxbNhDbF73uyp}?k8Ro;<X?b_X?9~x3O^)RYc18i@NSs4#$S}h;Rz~Tm?-iE+6 z&;<rN&4L?=dS&&ilPP3oe}bQ5<iBog5_V7}o4craZ~x(vZWvScAKm)#r6mx~-$*BZ z{@EpcL57*}a6Y)#^`@+$yoN_9nz;K&h(jiK9w5$l8UE$b%UH8qa`^<vek*c9Ndju_ zo5<^ScsuhlI--}qR8q6F4!d_-hmiF`n@@fLG$1yz>Ng7u6jl3j=zjoc_p<th;g$p> zs2P62VWZuKn)>B10OJpSfMmMS2oq#oZ$us>?_Js)6z_Fu!>Y=2`~G|W(O|u5x0k0r ze)xy0*y^3-^EU5ymR_@PU9q?SQ>Qkp`6X}F#-l+w$c7KEw$gw6=l`KQd8;?zjH0EP zAD3Sal{Sz_eNyR6E+L0GXG(vXdnrm)M3TzmP}TpY;$A{Rsimdnv$iG_u|^J7GV|`8 zsQ5FOtF%&W-qv6+L_!1WX|SipvNSm1UInca6-bW-XDYAeMK&S$KeBU^W{h=Pw|W%? zmza1pAJEw9U8%ZUY+<pJy&WYM{Xoled`7l2p@9We<0q%heqWpU{ne&F&PIfDaxig; zR8mJ^R&C8Gsbm^dQWwQmNN<#|d5lR4MGZYg02sfc!)K;EmqT5JPeC}1=CLjv+4UM? zv2KW8%pKbB$ZwEPoH{afnPsDNV5;Kv0Qzo9kS{z!)Ml92^hy}j2nl&RQq=p>)J2vB zv23KWc?3<t6#ji?<<_mNqMB*I0%U=LiV2@3I4Nd3@uoSDAxI}U6CV~}c8*32cKyl8 zlOUUj9jZkK-Zisw+dGhT?Y1@AaIUn@v9?ZI_r>o(51J;*vFWt!2BU%2i>$1htx3L& zwN<;-Eh4vCEwUzAvCdfSnCmo+<10}FLk*PEakE}|a1oNKw9vF6ZEh$C)h0u~*O(lq zQjeQZv&Ks@_1Osq{O{~My=-})E;S+(-Z^K^eYpDnsQMNgy{$nPQBxBh{^b2tkm%ts zc&Y4QT?OKvI@I4sFGta9ub!x^@>TuvdwfG#j-Tqk|G~H&_+qQ{xRc7?*ePPTTo>x< zm6CV@w`u4XN<>hYiLkg>s~o~&nq|_X3wt<VYPy+oeI3`DOA`c240V!Nm8m7sL+o8x zCsQ0pM`E!3$|#2p8~gc+%~(ti+QM%nV(dbDti*f(4JFD2&MW$Prqj{@Z|i~s)IkCp zR<vsRAdTyS^I(fK8)Ge!<(RVFoR=HC@unMjPUp}wpX3DqzZq#Q`4FlKCC$;iGOJFp z5SwLWrB)hG(5z0aLyS=)d!uw$DKV$@zgM~ijbH2j3a39!p}eL8>!Vn)c-{!XF;%m1 z)*@LO3L79c%~^yqxB9g_g)|FQZ6Q}249l50IOg!(hj(}GP&Z}gP!>YH2V~7$b`AIa z3$|~C&GYB?w+-eX|26}u|Np&1!)HF=@E|QnE+h`90`Hx`uz0e0?b@B6O%fs_+~xzv zOVzx6e++hXu6xSjBD#c%Bp2<dAlNz)zTUld(_t%<nQ4)>OM!Gb`55};%bO1b!W$2A zbKgXY#p6%@|E2GfM|G**L+U~s`B_Tl4MSWWNtRcH(*KDhMQxh1MJm}$0`^n9lG$(H zbSzJ|dXJ$&NzK_z%Im7UJXwHjSz-A4t#1@Um9XHuM?8{U<EyepHWii(60%rGOl1<I zpmOm_77H%{_lBZ@3Z!sze5Eg5kg_bmuOK7jRPiWBE0y$$#T^{Jm7{%v21442aC8qb z8<7?>Zt<&&#K)v8Uf)z<HvZz8SJM_@$50%j4PB%%K8v`=CX0!xT)dKBPU1b%n&2%l zgCK<HS4`lwMK~rXf<iRIGBU4t49`)(R79Bc@X25RHB2NSe3ldBhS^4hsUgH}K7S-< z9@XC@K91tUf`t`fg2IqnG_3<WCdKIlko_*UE|}}QVS`Ne(3CK(p_Ab2UX{b`k5w=; ziZ-4oH#kOXlj0aoA$AL-o-0$Ko0#fainQ~~e9+D%Og-jHC$!$>WpU;wy<JveZ5cN5 zpq`surbed|_!j&_G2X@JWIU?U!|`DuBKY1BbI4<|X*3PDv__i;&k=BK)7dI@Kr#?J zpr(1n6t<&5m^L*f7v8!d)!Dir4D<@X!S>-w)4KUm2E3;2+}e=R7AeHka59$<l~0LP zbIy=Tx=8?u%p+Ic>Thp9^8TR75{WzWtLS~*Dq5L6y@u&k`TNe7j@dWX4_<S6J-T8< z2z7x90+k1X2OOYa1UjdFM_Gi31Y1hI_Hw0T(br1-^?KV26>e!!5GRVRUie$ZYMr_Q z_;fmw0T933igrU1SrNG(2C8gTxU47+{NVs0Arwxv5;_+XU<CMaY>OkzseUy~(<`5e zCBhM5w7EcCh48^rKEA_R2J<~@0RN>0G)6b#WE(B1?nw`-wWWz#EQ7>5(Fx|w+<kBx ze_nC{)3LN@{X`0KXzRL1eD~HvND6z43L$Ap@;v^wgK_1+cN8OSQc8@R^^+VYNh(pD zoeF6uNj?v7`QBdf_|v0|5@kJ6a6B3`zIZf;2G1m8@ZeYC??T)Ney0h{?WtR13A~qE z0PnY}o7Xp&<rEhrxhe<;R_Mfsk(>d}K=W!+2EF1(1Q_*c6wf~07ppr#{zG(6VWrY6 z=J{<7d+>M!0|X}s<E{Hyc+{dqfOZvS<UC{h%>;ui#Z9;gbd80mJ;}KaJkh%!XY^t7 zPF}}}zJu&ouA&JAH`$0A_Q!U8`1CA&ccg!_AG@AC^y8nhyKLpca;tKDTnUbrS9o}z zBOg3$f_pQ-;Kw8JSmln$LkqoeyTGY#Q+5q;?7*pn4xbAXb{1pv?SogvNO9u8nuRAa zD*kP(=?^+@12gf<QB#}5NE^|&&wJ2eVemymSImLT*c+J<Pa5&~-(drr?P2iPnmS_R z;I`^!Vm+~pk9Hv|@WRbb)7)}KIbX`cHnu_Vl3ZdXF8W_5*LPR1@=DNIk06^{=Sh*l zD#9uvwHEXI7H%o{gqBfcj_HBTD&W)!bfv(0E_s#{a?{8U8x%f)(?VTsYF7BtC#ZoE zKT&)KGck;$$=jh~B_3+%6MR?Ls9nRN0+c>0H;mkk7ObW{7KB@3p5Ny#FJCnj3BUzO z_*WC4K6_X0=d8C|cIS>he!+s}4#8*^=f=}$8BWBe2L@<uD5|vu1jeSbaMNW^Z)9=M z$lK@j4h4zgwuY)4$GK%wx$@W<Dit@{&CNMorLK|7rgB{eRc@%tdHs6lu|154v;rHu z+rW3U(a>HPlz;znZ*x*fUS_iJbP}0;h8jPtff4>edk)#!*3l}`#54V`U6U+RyFnSA zC{|6q;X_JFqUjONfn$6A=Dt@EoS*MGwuQZonV6D7xRNSM@<d9btI#pw+O-V>VB?38 zk+-v^N_=IgGchE`D~D+1+Y3F~7P%Dp3-VI4vm+ynO$}YGJn{2l-k$n~AqEhC1W{Y# zj2~$WsB%kQ40ZhTAYfqa+qYMA3T(;O(dg)156%z>{JYuNhbm05@@sDH!$}S?{@?8E z_uuBgrfx2Om3K5cddAr4=fZ7)zz=WV&eoe#)t<6{<MQ$@-`dR(<a1aRx}GH1EFC4I z?~9teTz<EhF|np@kLsml%h>os-xU=Cx59)r=bs_qswl4fKZ8}~<{GY-WS-T`&Q{l3 z;q4gTi9DAdqwP~d)U2FWMrVxjyGZm|HE-S;mQv*5wE$Ect&4Zh%xoi*hi(C~yp4C} zzG?ys%6o6u{o}C9;i=)FtU)=wZ@mY0{Izqv^sW3ze?$^NTLdL184xhA6t4p(O$9$O zWKD>S?Pso{0AjLTDwfV*ktR5iRZfhlMwCGXk<spr2Vz{2(tyB$;x)vIof!<Ptwz{H zA~mZW11%QDnB8v4tJh9aO`bCkg_-v5c0>UUTq58|nrp1Cxkey@AYyCa%0#wh-{4^2 zNj6}|vX^J3vzbc<x}oKGBuI1A`Dpow31eY)*EWuzj!iHDhQ5!ONcO@ybq|ShgUoTX zCHJ~6AZ1dM!b9%JPiyo4p;1214GuHOO^4#E35)*Wa+N_Ja6{PAy^9!1cij#3A`@cF zd1*_M70q%X^>+>pVu4ea%(%`!DR_cO>pJmVX}bh#K%i<w!G>4&MIP+6P-JaYvHfJr znJQI}M6o?jTj$0vwwefxws2GfD(-n%`3(|hSz&OjwJL|`*kf%ZR9QV>Ux<4T(nsFC z>rYW$+9RaNo(B&U3s;APu91nZ%FDe|I(%D*j*@t>qIbvOIJf3eb$o^iALRAsN%p3s z<P4H8_jNGe1v;;}*|ZJMBC`|f@7-p`s18o(3l!ZyO(V_D)tn*W5OHG2#vkpyo7}!I z%uI)<*9LE=kouHtkneUS?OQ{GHwJ6dwJu`E^2vGOeW7RTY9X`rg+Y|+P@l{SGPmLy z8K>HooFm;ZVhX<XhAm!v^fHGrtH14@48JKe^CT9tIXkA;a(jrjXlg)Jov1jGG=`kf z;G&`-JSVA%>m`d=yOIzjl_L<MUs2qQNod<vH!^?3&-YT582M!BWlE@@mL?UIlnikz zW##4fEm|ab_KeH5e4K3(c(TX3s_(^;s5Klnn~@i=c+{ejOmJw|qIoWZ38bseJ>Wyh zXi~L+J<ZL@wKPY)En9I`tqvvGCacx!N);|I4%Hu~4|wF1b7}3e#^t%sIAGkXdlnSn zK!}M3T)?$POi2cW9KsdhO&iE%K>)SGRpESa(_5q9Ycj~gbie(I1*$@>s8E&*1Foei z0IEKHWNWegdzPwwU_s4(@32^@ULI?irFA$m<)C#W6e~uCa5qs0AweD-T(fJv8qpKF z2s_AD$Vo~D)~nzIe1$~>m!W!y_Y^W3Qaid!1M9@GYzV$OR}h1W+6aS6xIQ1xfJ}r4 z0jHzZXrOxzo*`-gCW$b$(mb8UQd;a|X#p-w!az_FISt*aMnkUS34#m-OhTf?B(Kw1 zFBPm+NIFRp$6`DYsseQ(7(MBS#`@eN#hhq}<sJKM&BTY%DOB2BdAC63JBA|)p?~;N zp>-LJ7BslDo41%=u&=B<M25yrzEPoQpQDp~|Go*HZ8gdd7Qv~j+b1S?si|%r!=4)$ zSbgpK*qPI@OO`A))n1#q<iP<rRLeYn>mhzoN+ENkV`)ke|IpSJCUf_;iS-BB^O6%# zCnwKiADmc!XxMAePpX$nI=KR`G*a>gv`z)To<!=BPOg_2IYO_L5lMi2jOypV<5SF& zUl9Q2_p@w@YFf>Qme>OreAW@ZiMqoG8HqI)S^E`~`XqgAe$(x`nPZb@W4g4JmuQUy zdUDM1?yUP`Vd!A`JWF^XzGM@YYUO=F0kg^qr|4@UO$ttr4)$U?Dk~lTg?JrBMI;;l zDv$#&lUeH^)_T@S*zI6}aj)$tUZC1cCLcy+6Uox3SQ5ik^%@8WLSQLvnz79u5SHvY z!t23mwJe+@PeRedl$b55@`^S?e)TpGa4@=ff$G2>8zwE2T1v`Bz5>0$Zim?LMw7pT zOg{Ly^0q#PZ7>S$Mw#-_kt1Iq-U>cv*o*g`a*8-l;!Cv^T+ficR7B8@@4+3k!c_h4 z;*l$mt8o-s1j#q8phY3!Z+1wTbyZy4bE;{ReSuAu6E~W;)GzLV1tw)EjirR8qKMX= z1(p+o-@}VAT{?drTt=A`ILY%VL;jS9cNN)~SOAT;cs>Th3dlFlE6E&?N9_e;{U1yS z2_m`Kvcg@hQoy~q(B*PvrHEz^o>zIy<t?5hMC8_`M(9LUMTsC8i632#S2Pzm>$-|7 zE`=}2C>_S?uWocc-ds@#wkD1{AHidp2eUv)vn>@w9%^W)WY#eoUj<(w6iJi?k^u%x z<5OENzOLf9Z#+(lwI9(#YQdGfyx9?4*TjA&Z0~C&6=r+-cwJk|jk?cdKKLigHB#V8 zl-`wJ?b_8H+bMDX>GG&4vvaSrZ`5XwR+Zdg1mC_I|7&bOsSwcQ_F5W(m_Wr@d4)6^ zW#-o2q)?vHeEY&C8vzH}Ay)Dn(ZiU;9SPwcN5=Yf+n$DnS=tOeR@0U;Hda4A;#pLf z&#>ynp=;G~zp%&Zb{6@NR9VC2*(E*+RrO%8V+uu!P*x^e=BwjwnE)8y8uI^VS@`dt z=Gun!zj}BMof=^=F}c&MQ)hA}=jBV=sN%aOmUowzk0N1>7QaT>bi8Kopz5gkS1nnY zb@TM~C)Wo@mp4E<i0ldd_b(!^N5S^)CMd)(57Yp^7Hx{fh7^iQfgmACwuz1Akj1kq z?hjTxb#nu!K|2(?7|r_jO(zn$McAvAl-!ZWer|YCTK}%pN^25Hc1cwUT&|t-iPFSu zfZ=x0Rla`vtL{Z{CkZ7l;=@{p#m>eZp+xyW_`$^hwkQ9-i3w5a5{SE4sq7*vlUTi5 zs~Cq`06o%b_Vp06<<K^Eiq7RlZ;H7Wy;k=%GJ>~G7};xCJ}2c~>)%aVG9Oy0&zg8q zWO~B+JHN7S0H-Nz{>8~n@W7bqj_niN)FmwLVyW~X$s#iA!6AjLOQ}tsb3BFR=M@%s zjr3M9?jum1GO#rS)xu%6jMtG4KobAx(dqa2|HmIIt-K$J7I;LD^!f(>v}Xvz_g^jc z{rGVq8lM1##l|>wd=0}*NUshz?RZ}iP!y}i*;2|VURPs}C^ja>V%t{tu2P>8pMm|t zg`x$-RnN820Aw$wK$Vg5>j;{c%cBE<3zQ*aZai4=ZmA%|CoA&>LZ4(^kQeunDk?or zER`TFcy686VsM<kN^?0oa>yop$#=!HR<u1Pc3KCiAa;O}M9ig{XN9P5{E5*4f4Z`2 z3y-w9Xgx9EcR)Zw00jh*+eSSNzr^QlbaD#Yo}r3Em6zqk{a#*%w@qb#nK|;1eU|<3 z;!y@0JcFc?3Udo7f+ReUpP641i)5t*%^!Fg$c%U0u5RugwczioZv8{R|GT<+FQ2M< zUI@Bt!M2&cWKGTg0{HKm8Y_%%adX#}t8Si;EM0}Dx-CpC@?4F4hfa5$@?y?Row=#( zo+p>xsLwu0nWCK3>2}C8spjN7ch`eC&sR_Q?Q(T0*`4oR%U3O1b;%3Nv9$giVvq1S z5|iiLY;TJiA8%qZw~miTwY8rPpA*XP4N;X}tw2Wv?&zMqXU$0&4hK(MV(}yKHJMBQ z{llNL{V5w7Q2?J`F!55A&#g-uX|;_|)XzHBCv32^1BeKxkH$QH&0BkMN@?16gFxo) zo4dC>bZfsTWs)wyK)Ck;4iXr~2UQqN!1krcviP?3`zW8hm*Kb&{rnt2fHwR2=J6Vv zM_amfnG<F9`3IKY1#Ld6_fJ%KrSXtoLFg?V_1op@24zzYtly<Q0(Gu!`T_Bl8yLC2 zj%fDQncMa2<CpL&6ygqXVgv||*V@0xlO6r(m>In(B=JjQk8-%vxk?jM?b6xB+q^lf zwe^6Ti@U7!ka1Uam$h{uG4Ww@la}`J&r=sUnhd+(%Dl)*qE%yc(wd}iMvcb3qVlAz z`>hu1ZYM0(+$}37*sN-I*{^YVt9_hxt1Vm^x;C#Ssr9D+98b<i7R$`RvVw${B@zUI ztXaE8CLfgBEK~VRPiN~jK|ml8paO_!-PF>080zf%`}yM+WAz8dBNhP=%Y5#R$F<-d zY@7Do&}bZ!NHwI|oE-D*aZJ=|ZY(;rmQ-W$G(X-j3SgE@i{Zx#YH9?r{Ft;f2RC@Y z5XKiIjWsUm_35Hir&Q+^E1Xl+Ce}F>Dh!*~cV(sx-`2M4dV9ae6v+b;PfUAr7d&|I ziRbG2L%%+QE2F@d{n28Folgo4k!AAJKr_pbg=v;62`UcTD@i_8t0r88S8Ct`UgI?W zu9)yV%~=yrr`=)0z~mXt^S3RJFkUOSn6@at1|p0=UOV5jzHY|Ea`<qs^+a90r~c;m z!U_t)un{o1wcVzFutAZlAftYp_O_1xjSa9=@3QoFv;kAd`GI~2T=XC5huW>AEt4-x z%$0w`uG)5uZMB_8(RjDE9%(aq8^y!2jl4$7Vijdp%i`qEFuNTBny7^V4L(Y@Lr^0> z0D%#_R@HCQ-Z03`<&N=ooBlSc7v6}M5}a`D)HdMv>AROLy0O@p77^rPBryu8n1)9( zCKov_b|M_h?aN*FiX5hrZ6O2ss0^J%&YPNMo<u8HTs?kDfhC7Gw{00o;@_KjmA%7v zWyAuC8r6+PVtA7T>EN_J!|~|3q*zj-V9X(P{Uv~!bP69Ry}rKBt6rvbQ8|D5oU_g@ z!#)#KGZlw5fXzrgTrht`Jy)~AZn@fhqjyZbFWryf^?m7sojh{tSd@+J=VJf>BsLi@ zbaWK@ojj#<(lW}k1@kN(CFo5(&kyTjXN?hGr|d?7WzH!#=DLxUE0glCGE5$HW26tZ zw}mD!vz9-bPF?62D)75;L**307@EXvR#^niPBitrGZqtr+2uttt9sQ49uKV^bouee zerkXF(7>@{Px5%aOA~~N!p4+>{P@d`Gi)@L^c@_$vv==+Rr|f4YF158U(4qCECC6E zL_vLGyfGf|q2G2E_=4^*9sQ0;GM5cJ;au&!f2-vudy_BXcJDW&n_OiI<?VCW!?WME zi21FJ+NKT|kS&XOJ_yM8u(z?Zx|yJ=d)Rhs*#qoBuu6`5frGXr><A15^+GnfEskuR z%8mcTWaqtS%64=(9EFparlYtc`@>?<Em=0y-{bN!s4>_9oU)Ospsv!CS1YRLOsH&L z`U$9)wJ+T@<9-u?LQ|u0p4MPeCutG@a7z;siAxh;7a%sVk;!iWc)X5v><0$}Bo!is zXOhq(isDCwALw~2pg3W5B*i*K!#6+zLWz-t7*&*rE^tyQm=#5T^AE=$;1aj*axma3 zgdSle=^_=u>qd@@6ZsKv00%DMe}NPWmWZWTPf^U~U3=bS5dEY!EqMHw0;SIOPs_?Y zm7Sf1=K>6IHi^=y*kr{(!D-PudMZ65c~Oh5u)tdwI)woR!>!vhGcu>MvvToafrdz; z+cJe?9an5iVOo@(eS)NaI$^XJiA9kGUMc`t8Q=&=Gd1K9+abP-bC;>K6FWdgI+z0K zAOjQ<BoJhR*+J<n6LMU)a?Xl5SFTLT%0XYu<$^nW>bz@@9jo=O^XX(lxvvX>?p!yc z^!BnZw``})+9^tOOlodUa!eE%{qqjlyFKJ)y@$(2bw-BT%nJMt)Wtc3IfO1;7|Kdq z+*foU;y@Q9j+HXNBGwRNPU!CD=e3N9-`+9~IWXt&a2czdMK0qq6W-eIyCZGvjcjuR z`KL)7JSf*fh`dVz@=K;Uc!)&YYTFH%^WYyfUgtrX*g@o45|BS=l!J#X3yJMQgD|eP zHm*~$vmc_6h)`XiY*yGhl^<i2ni|&lsi`JBkn4h^vh*OgVZLV_F3j9ciD%vXx>x^d z1&az@W%KRUzn06U+i&0QLC#605gJ9Y!kVT0SnW#76zY#p?b0}*L6D6fc4J?Zoji7G zMTVWMZ0RDc@ZVQ!K`9}+f3I`GnX`e(Zhi3n^J!^5iJw1Xn1dTUlF#q&b4Yd#DtMja z8wSDL3=R&|-%vnK#4$Igrk-DNB|7R0XkCI}^oAjQ89!j2I3M4MyD?E3EsdeApiC*w zK0j<e6h@n#!HwF!y=ZROV)KwqG-}DJWy`8C>U+QDdlwdY2eYGYft*s)^>_}5DCKYj zB#9W4SjoX5BMb5(CdoAbjCw8q{LICL0hpO_)x?F{d{r&VaZ$OdSkcJOzlAxCj(U_< zrr`$cBR$Q&y3X#WQZ8wpS3<x+m#-OYd-dvfJEqZC|E+cJl$B?iw$<;RPeigF%l3)q zh6>Moyz@DUX0Kc(*mTfa8v~ha6h_%AvWh-DU1J|io~3nWy+8Ftih6JL?Ru@Go2ikJ zq|nY`KjvvEInj4RIqMb48#uq?v>e#CVqTo&&^8_*LWVuofQSF#yXWy$04W}II$HqO zBporzuh5GZEX18P&w1$#)~{TTld2t8C?;PP3bbsWOMSG<f)h7~ZjKwXj;=|MQ-N01 zrR+P$&b_a9HoPe)@JT2B=43i<rN@%U=(DWP$6eIPDV&%%5A9+*gMGl@k>_}Wk!O25 zAYkwuMOj`K3&tm_PS+fay06vBlRx0)QjN`;IY-g7!9<Vfe0^U=UG&bncC|7q38G4j zqov(xx!Y9h1g<Py%?G}v<w2S3tH92ed%?QOtA2hipYb@JjHD^s+p0EOx$?5oC#qU} zhtAr-DHw{<X?sH+_BLziIknBrZ$pyy;RJ?A!RKr`LaJK!yZE~bN)J}8ZG!}aG8vCs z@e+yt=H}p`Ayv=*6jls@CE>A@Mh*=||5T?GE!Aj^y7TAHSwZzRw07?7o51ob#~Jmj zR_SxgbC-*h?vp#B;o#ue*~W4tdeI7ZtslJ7?Ej<?O<Wo6q7_DYTdO>GayzXw*AS9m zmR$h*Ph0Dsm*>Aa68yWqIL|1e+C43K^JFfPJOM@f$fTXppfte4$=ZHEI6N2TK;QqE z6UcbLu?d`4b&<$B1!zK&6H&+pDwa7Ka&*R3tLTk%jFAkec<`V+;LDeK#`(j!dabwU zT&4Sx6>xM#`P*5akSKUZzq{727<v1BpJa%mb?=yz=hf8YdaqbgO0U%<gnrM$3;d(} z*Tl!Ko)ZZM{xudSWKnHhdGqqnE>cV*iY8<!5v>lwrO<U4kaps%7)5uIhH=>;V1vLF zx+h9A{f|xnbT+<!Z<nlH)w`%om3ClQrBK@R#4Dg583a#tYSCjsN@^6ftE{j<EdFoz zZV6@;qKP9;8R_9`RxjU=aMBiGW@~~QS=@#{v|oKB%rC0DFw#~*KzqPy6|@q%b~QXK z^v}Ar1f>L9VC%moR>e|5tDuvq(aFI$B0vy=kikr`vIC2$65yOf5}}4lg*cET?Z9A( zBBD5NJwzaqkoi$Db$}wRXo5eztZ+5)`H%57vD~ef7ahFXENik_H2(Jww;xp3n>RP* zy;5ZAZ59%q|MVZ23wW%cup=xi%bH*s;X+80O0__Xw5gEL#A-6Z+_H13Px|}23ktRt z6xwa+$=Np)zpnXRQfM4Q(-e$~8wOO8iC>KDT*o(WQWw-RrxnT@*!&JY?({@E$ot33 zzKv`40bFX~!jxa{e#PTW>9^~<+qwa`+Jl)goz`3C7kr})`R@!4gB~6^@+XuN7M5#m z!_-Zcc)~Jy!QqfP&K4(4{@m^@S!2T?>sqdb`FZEqI}!`ccJ}7dwXg!6l)5U^yVoqS z)HRrPWt5G9HQh1}Nx^7F+Inr-y|rr!e}6>-itG1}<3DbE2*c(A%>5#04F$O8P8RYM zEIx?!NDP6G%S`Oon9Vq}Uwwc=KBiJP1d0d!k}UPaI3go*Gwvdw_+xB3Tc3iFee$5e ztmg1R^*Dw6l&oE5k6?kXfPrQBZ6qX9NoM@!J5$ZTvmeh(j14GE6U6dk7cH72g0!?) z4{E|+FHjmQ1|micFAiDS%~nxTb%!zTRbvB(8q-m$cNbnVlB^73m!9CEXMXIhODukN z$mk**!Vw6k$qx=41<Lu2(@jX^)&iQ-6a(z>;>=SvB+r#ytQ&|tS%(G(qr9Vg<fgG{ znX>EpRm<!pGlZr!*Yx}J9->4%H##90<wI9p2+Aq{T0gCNqVjOO@0OkCkm<+OX0hqA zle+F4`UFA*Z+pc#I4C!*36Q^<Gk6@4&J^Kl9vku^^d6!~A&kz#`j7)-+uqhQ(#5eV z57G4pY{h2NTMe@|DpPhcwgIJgba9IX>_9;ETPAQ|Gao<Qgb0;@-QSaR*ji`=m>ljQ zk6#dR*GiPrD3E)OI#qG%rdjwOX1i8^#+l&{z2OK<e*pCcPRPUI!<;!d&_wB{`14w^ zA?_-_L}Ht{E)osPxJx{&)Sr_ZOfXMmY{o+vd|ekP>Eb`osKgrLSj%1tl7I`<=rs@6 z^+=TRhDbIx6M%$RBA%NQEoC8NsGm(9d`;<&^zyI98DLkVEPE(9w_QeET&{YG%k-}P zO5yBb_p$pqpy4$)iTkH{N6eqx9Nri6fNrxZ*#qpUK4KopB6Se2B*kqLopEk4PQk_k z(k#-F7yw}p{!JVUH@X@c3djq|OJXFFvW%_KcC*DPgE*dO_eFix9kKmLUV%;<J;u&k z#D-HAo14bS^<f$63F>AP$X&y$NKyxJ5wU}0%gHht7+HEsq`%uSKM#!Ro{D0bJ$!Tw z_YpU?UWOTqvN^tC1eJ}FRjD*;?j*&NcJk3uNqvdl5r<=<<WXc!xz(hSsJ{=3k7*R3 zCDLkbGB|O_v%zU9dnww0YuL?F;gygY0zNqBf4ar|@QZ_WVCf|@ct33R;!k7vy*MI0 zg(u=2__3cD!+(>7k#(Mk$1#F+KPAC+c~Z6`<PGGl^iwz-?stJ?1wZ+*Wgvtf$bM0C zErvtb4-EQ~`#l~UmW7drh4a{9HKQQ9J@>ITyg<f`P0rH(WiU9%)A6b_jhDV-+GNzk zcCNboTYOBFTN;dn<g|0OGtT0vjXTlVIrEg`=_RaLvTo>*z+wCSSP5ktB{RZYZMr{0 z0Q3xH)|x1-a6mAWV3%&VWs<D3Q#Df8lZ6{pCa*)U--`76CWJOV6yz+Z7(25@J{xj( zYBULel_E+nCOu5P!|WJ?^m5U|Elxh0vP1IlTYa%%c9S<!`lMb35`{Ep9Wo_Z*Lp#c zJE=k!oi&CD)!%V;JU7F08W6dh^$@?E#2+6pYf_mjc!pYkuB=Q#cVxO_*QwNnTv35+ zPX;IapjUZZIB%zcWU6w9$tk@d*+ibGLdQ+WUE-5nYD{~)UAm!*B!M^bk%z_3^MJNN zuR2>=inm)o$;vvYQdNt%rLuKQEO=91_BK>i$QAlz%h2NB)ds`MvNAp`N9%4lXHYK^ zaZ4Bv85P|mO0ifbCibb-&#9z?3PqJrFiN(~QyZSq@DdMml-dx)#d*LqHc=>F3Njc2 z4Eg{gg8?~~YXKw6;8u_y%+K)^7Sb88q7NXu`b?a)&+ge;yy(F>v_!5Y!|Qch(*6dk z6zGksw;YZ?Yg<?t5{b^4r&BtY3I$!xaQ+^DvBNQ>aIb3a%be2Ic753(5!!p{-?z!e zaOoi#j%676_;4q-VT0|)RY-k9mxIF<4fV*XjkeJ*B&>65K<lc?JGWo5SYO?~Q@N@& zV5`uae}Z}rch{n<1JK;97tNVxGNzk-&UU<|Ny<%x{O;O7PyeT{2RU2zZaQafQOy_N z2bWk<GtiZ}?BD<VIlf;YXt;0Vak{3mWz2<W<Mzgc;Qt$x!({RSWyAl$39+>415tWc zOQojbcnF_wV>UvG#YT+f{3qqmkIcmI@Mo>tB`X5FH~uZYvSz;bxpPyG$d7kUPRQpk zKMs>NwoA6PJr7R~Wj=E56UA3VQx59~>=nYfu<<wsupoz()6)ves<WT$PTyFI)nW3m zF!}UKRP=uTC4yGyL<*~x(z?=?H7ME`)Yu1VABM%FEB+Y6d4czHF8oH1TMs?^{@gq( zJ`lT(>y&Ul%DU!2n;yBmBgtJSiCMTX<`B{?#TRr#Jo@;k+v@wjxJBXBBb9HSG!1#a zx=!oH?+Yl-V<pvCxLOYY_Wu-&)We^Gy@HEe%=doX)|+Lg)ym}#-s8R7z7CiRTpE@v z(Ui>t&Fg#TG%60ZrKGeToJiRe6!m_0Hzr|W*?6#mk&-QbyiSZ{%x~Qii{0(fS-|1l zkN*XUU$8*;>7tLKXm=9u8riSROatO~Gr41&Bc2Z_gBLJO#}$h2W?o8N(QNmEyV~mx z7;%s4TptT%5_lV|tmZ*7bEQ!dzxk-ia?*}2SobFOpHfo=cy${WNtq=w<cpoFnr~i< ziqdg!J(oznxV6E<d+lbN;mkS?v?ML%m@_JlJwabqZXF%pM2N5pF@f2!b8Gyw_YP&! zm!(>yS+3AkR|jF$RV(yq7NJyx?!3hceN{E)`1EP*;v!-$rj}dfb?*PmMr~tVq2tyL z4SWwe8HN;T0eA%)pCsEn>#9r*ddBJVhF5(*+i@el8xYNX`<wV=n!Ed(C(pVg&HT8V zW*yA(go2Tx(CyoL7FUj?RgD$(Y}+1MG;gS;*|fv#bROS;oOM5W%hrA6$;T2(cgmH; z;LUCXlRFCs2<G`Yy@gx43wv|cv2g%+76>WSW@l@=dF_P!hHcdrRn_LzJGRgLW*v{s z((Wh|E0tm|0Aj|gn$UMPl+}9uBVC$2Q?9$JS1qIzrac2v?vw7c!t?0W)XrzbN6t0* zL`U%cXrYSBRUryyN{i&my;qoj_eRkm@07`tY=kzzY+}sP?VnhN&E_GK_gVH91LpH% z8z*CJEIzjQMtf(9Lbmm@{<HUI-RIb`j*hdV2~$g#EP48Lr|Q$EsU=I6E`7ExU?k=x z_%1N@#rIBuzrXYv9iUo~&hEa^(eF_-yF41V#16oX<)R#uiWb6pb70N4*qfIM)VNyQ zQcJ*^K$<X~_)`_Nh8@m+_z-O{iKI@&HR5X==q`JcF<-8jUkrAKgc#_bzLyXpKR=11 zS54Rris#D}`9?jkDPox*@kL?D$!d>%d;iJX$aHZgLlW8qE*}N%-Vk-gMYt_Egp0W_ zmyt?K{l$%vwMA<sS&o9zQfV0o+R+Q}2mtxB-6Py1n@bF^!67}v;9!88OWY$pB1~rk z0Ep-rY;im?sobW+0RVvSQl85sFT0w@x)ny(a~kv9CCO<#-pYc)c#}NC;V|;VO*fvq ziGt(4zTIb~K5ukcnRQj|5a!|HKF1OOAYxOme&VPpWw7TDUU(x6{r0z4ur^nB_g6J} z=CvjM9`!<zw|+@LyakSMopldz(CauC6padkhb^^G;IORPC1HbHervFcYc&o%sX`8g zr5Y6%f;kQ=;l}VL>q;DwrzK#CWw1y%tlodgwFEW!-MhIut&i6mBnZZkbaS4%2GcIK zQYx2NwPKS-%_wdr{^4b#+u|j2z~uIgK5Mbj?efETaH+ZVzK`>CWXGf@&;rBn-{Ge_ zSAGa)(DpgeU2*!RM^Ci0vQwPoarXjjIMJ@La|(&=Pk2$AnQ41iVr6ZhaAVhTI)Ni> ztaV}7^Hc|uY^(p%2}u@65i9!0l&3Y|hhqXiSx)%J;nV6vCQQ=-k&Ab)98Kvga}6NT zkV-j+19;gk10jc$XE?0K)VO`e1CYSJuI3Dd+^6Jjt2=8rPSTp>5gw@^30yY(_F;8( zbv{8N$pU+qyWe+rvsgFxBoKb(YIp=$9sAgQdhWo$sR+Lk+MwZ+!@cb68~q|q;qK?2 z?*6pznrB3~=e2#G_@I^<afYpZ&6tm43F7FIk{DbR9g`B1aGJY|t0hi1@1ZEK`p)~7 zRwJrL_`Dx>)(@Mcr)<LlAZgNxaRaZMe1ZdJ**WDKfdk9v2}AO)E6d3S??gT$-~9`O zhXFD8>6K$y2QuYlgJx#h1qkin+VXOpnOgboW2&q!&D?9+omF+f#nnBkw7;_1n^w7o zm@{%W-sk7<Dl0qK+LLFxFSF&v8eXB7s|?N=Q$Dx3Hfbldv)J}ct`DqNHFKL)TaVWk zlNPT%u8VkJJ@;kMYpVyYwGTM=2inkXJIv;QU~pxWya#LAv|8s}sKvBy3zp%H0Iyc` z)4;A=^ZVw!lP_K|56r*kZBZ%|rD}QL*jmkvr*ipcH#C}nz<3Q-8hb6;`@^C~+OpF5 zo7Q~Ke*gL?_>zn6d(>5b$;2ejQ4;#LW9SiSh@{-AzjOb2+*%m4unuoB3jO@mrr)jZ zeYq+>retBXvV^c0eH>2X!ZRQ_e)ju9@#*3_FtkDIJfs0xDL?_R)JzQ+3K1=I1wSsN zZ!)^yo+l6OtfzAy;##`4qQk?Em)e)S)xo?%3*4F7V>(&U*k?Cvy#7qBjtZ15ED8zs zzX~hU(v&y?Y+#%UM=CS@rE`lmr4C)K{-tIL4tz|@V3qR@*E)L*kA3>YF|#hy>B(c6 z@?CITh3EvALl?ES1-Ds-{lUu!FnqwwD?jVm%{?!uc6j9vQEuYo|MX?q`*?MA-qmPz zj)~P}FMj9vJ|bc0w8N9u$#<66cLXh1nY5%kiSdnnLRhGbQG+sK4Kd$_hf!uU%}fsu z$4jJXMmk8XsF-nI&4?wU_rJd_yqd?1n)&l{gnQ_Tev6aNzj)taKe24<#U30BB}Xr0 z^WI%Ahe<9)i)+uWInOpA4Z6qyJZ?{f&X-feU-|p|Puu%kL*M`P<BwyINghW6c!vQu z!i$q~p@s~;AdT7QaSs`<DO7y^0w;V+;u46&U5kMCt8N0tuQN!u4>8EwT|%c1Zf3KH zP#b%}E*?JpkY|0Fl<@TtIdJyMIy7(E;d5zGPUL(60L(7;qz#m{!P_p^Tbw%U0MG~M zO>4H!A!`X^ocX{yV6S-q+sM_*1~LT8kYCL#Y}xiqS&ipJX<!7HdI%m})C%gjD`3Tj zK}Arv^gE%JW*TT63nTIuboE6r?D)T^q0KYwQ>wk0-7Y%kK9=DiWE#_hfYbduj{ql0 z_LrE<AOqoYh|G|{E|17gfcHWLWVh+YW-)8JmPm+7+X(dyNu%UA=_MLYN<8wLaMTO^ z0%)4)kIk)qN_t^t+jG<E-}8suvz2mW+Q0RIvhMPZ3Cg=uO*}$<_JvbA!o=(qkLwT9 zvnM>PHvuyO%>B;H&yE=csq9%Nlt7~Yc}g;z$Uaca!Vu|qujB)aT>Y0R-pMxU%+3y( zV(D&CSr$*xQ(Yw4seA))8M8u%?2(PL-XpigGVE6|<Q(>Y8cA?PcA%wXz7W0M%`S?W z!QRcPz)_kx((XqWLx4@2@BH7ZZ#L#PE1g4U<;Pd-`asHldH(Y0-AVyi5fH{-mn;a9 z0{Ca+Ap@7ZaU948H?iihWjliC#lQ<l7Sc)3tuO{-hHF>3?(Cq7>94}xtAT4wN?(M= z?1Tnm*Y4VKZ)S0O_WQ6INxE%XXN`YMqWN#hyZiGjIXwdx5piAtWucQW=FIO8veq2A z<sqvs?kMGYgJomKHj({j0*8%%*CFqSPCtR%Wp4M5rEZy9?$)~#^>o`!xe52gut+l6 zDGU7$ag*iYHWXZR#Lh@2>}{`MOk5LoSGb{kv0Pk-Gd{&9xPdNc3TkJ-By_gQ$xXU1 zU!UzpR^TR<LLp3Q{>05$i(<DVM?a7k`^#;U&zo#lKrOJLrMC4A<{)w-fz?g&3NK;U z{EeFvYq(qD<~SoaquXXMp>(@vXEGMmg&v<M&ko2|t&j(!g%+ro7FK+AJ73;{PsXPp zWrkGb+h-}0D{sIZEOgUyczz+>jlRUAc1SP$r*HU>HY~19&JU29r%aX#MRMa%wnY(; zoCXr<kl01%8L#l=GY7}+2*pRNl`D*bgSSh<lQ?%<#1g*iCKI5OCts!cNp6wH8;)QC E0Ae~2s{jB1 diff --git a/libs/angular/src/scss/bwicons/styles/style.scss b/libs/angular/src/scss/bwicons/styles/style.scss index af13e0ddb6..e1333da468 100644 --- a/libs/angular/src/scss/bwicons/styles/style.scss +++ b/libs/angular/src/scss/bwicons/styles/style.scss @@ -104,10 +104,13 @@ $icons: ( "universal-access": "\e991", "save-changes": "\e988", "browser": "\e985", + "browser-alt": "\e9a3", "mobile": "\e986", + "mobile-alt": "\e9a4", "cli": "\e987", "providers": "\e983", "vault": "\e984", + "vault-f": "\e9ab", "folder-closed-f": "\e982", "rocket": "\e9ee", "ellipsis-h": "\e9ef", @@ -131,9 +134,11 @@ $icons: ( "hamburger": "\e972", "bw-folder-open-f1": "\e93e", "desktop": "\e96a", + "desktop-alt": "\e9a2", "angle-up": "\e969", "user": "\e900", "user-f": "\e901", + "user-monitor": "\e9a7", "key": "\e902", "share-square": "\e903", "hashtag": "\e904", @@ -157,6 +162,7 @@ $icons: ( "files": "\e916", "trash": "\e917", "plus": "\e918", + "plus-f": "\e9a9", "star": "\e919", "list": "\e91a", "angle-down": "\e92d", @@ -237,6 +243,7 @@ $icons: ( "linkedin": "\e955", "discourse": "\e91e", "twitter": "\e961", + "x-twitter": "\e9a5", "youtube": "\e966", "windows": "\e964", "apple": "\e945", @@ -265,6 +272,10 @@ $icons: ( "caret-down": "\e99e", "passkey": "\e99f", "lock-encrypted": "\e9a0", + "back": "\e9a8", + "popout": "\e9aa", + "wand": "\e9a6", + "msp": "\e9a1", ); @each $name, $glyph in $icons { diff --git a/libs/components/src/stories/icons.mdx b/libs/components/src/stories/icons.mdx index 44bae3a54f..70fe18b34a 100644 --- a/libs/components/src/stories/icons.mdx +++ b/libs/components/src/stories/icons.mdx @@ -41,6 +41,7 @@ or an options menu icon. | <i class="bwi bwi-sticky-note"></i> | bwi-sticky-note | secure note item type | | <i class="bwi bwi-users"></i> | bwi-users | user group | | <i class="bwi bwi-vault"></i> | bwi-vault | general vault | +| <i class="bwi bwi-vault-f"></i> | bwi-vault-f | general vault | ## Actions @@ -68,8 +69,10 @@ or an options menu icon. | <i class="bwi bwi-minus-square"></i> | bwi-minus-square | unselect all action | | <i class="bwi bwi-paste"></i> | bwi-paste | paste from clipboard action | | <i class="bwi bwi-pencil-square"></i> | bwi-pencil-square | edit action | +| <i class="bwi bwi-popout"></i> | bwi-popout | popout action | | <i class="bwi bwi-play"></i> | bwi-play | start or play action | | <i class="bwi bwi-plus"></i> | bwi-plus | new or add option in contained buttons/links | +| <i class="bwi bwi-plus-f"></i> | bwi-plus-f | new or add option in contained buttons/links | | <i class="bwi bwi-plus-circle"></i> | bwi-plus-circle | new or add option in text buttons/links | | <i class="bwi bwi-plus-square"></i> | bwi-plus-square | - | | <i class="bwi bwi-refresh"></i> | bwi-refresh | "re"-action; such as refresh or regenerate | @@ -101,6 +104,7 @@ or an options menu icon. | <i class="bwi bwi-arrow-circle-left"></i> | bwi-arrow-circle-left | - | | <i class="bwi bwi-arrow-circle-right"></i> | bwi-arrow-circle-right | - | | <i class="bwi bwi-arrow-circle-up"></i> | bwi-arrow-circle-up | - | +| <i class="bwi bwi-back"></i> | bwi-back | back arrow | | <i class="bwi bwi-caret-down"></i> | bwi-caret-down | table sort order | | <i class="bwi bwi-caret-right"></i> | bwi-caret-right | - | | <i class="bwi bwi-caret-up"></i> | bwi-caret-up | table sort order | @@ -128,6 +132,7 @@ or an options menu icon. | <i class="bwi bwi-bolt"></i> | bwi-bolt | deprecated "danger" icon | | <i class="bwi bwi-bookmark"></i> | bwi-bookmark | bookmark or save related actions | | <i class="bwi bwi-browser"></i> | bwi-browser | web browser | +| <i class="bwi bwi-browser-alt"></i> | bwi-browser-alt | web browser | | <i class="bwi bwi-bug"></i> | bwi-bug | test or debug action | | <i class="bwi bwi-camera"></i> | bwi-camera | actions related to camera use | | <i class="bwi bwi-chain-broken"></i> | bwi-chain-broken | unlink action | @@ -138,6 +143,7 @@ or an options menu icon. | <i class="bwi bwi-cut"></i> | bwi-cut | cut or omit actions | | <i class="bwi bwi-dashboard"></i> | bwi-dashboard | statuses or dashboard views | | <i class="bwi bwi-desktop"></i> | bwi-desktop | desktop client | +| <i class="bwi bwi-desktop-alt"></i> | bwi-desktop-alt | desktop client | | <i class="bwi bwi-dollar"></i> | bwi-dollar | account credit | | <i class="bwi bwi-file"></i> | bwi-file | file related objects or actions | | <i class="bwi bwi-file-pdf"></i> | bwi-file-pdf | PDF related object or actions | @@ -153,7 +159,9 @@ or an options menu icon. | <i class="bwi bwi-lightbulb"></i> | bwi-lightbulb | - | | <i class="bwi bwi-link"></i> | bwi-link | link action | | <i class="bwi bwi-mobile"></i> | bwi-mobile | mobile client | +| <i class="bwi bwi-mobile-alt"></i> | bwi-mobile-alt | mobile client | | <i class="bwi bwi-money"></i> | bwi-money | - | +| <i class="bwi bwi-msp"></i> | bwi-msp | - | | <i class="bwi bwi-paperclip"></i> | bwi-paperclip | attachments | | <i class="bwi bwi-passkey"></i> | bwi-passkey | passkey | | <i class="bwi bwi-pencil"></i> | bwi-pencil | editing | @@ -176,6 +184,8 @@ or an options menu icon. | <i class="bwi bwi-user"></i> | bwi-user | relates to current user or organization member | | <i class="bwi bwi-user-circle"></i> | bwi-user-circle | - | | <i class="bwi bwi-user-f"></i> | bwi-user-f | - | +| <i class="bwi bwi-user-monitor"></i> | bwi-user-monitor | - | +| <i class="bwi bwi-wand"></i> | bwi-wand | - | | <i class="bwi bwi-wireless"></i> | bwi-wireless | - | | <i class="bwi bwi-wrench"></i> | bwi-wrench | tools or additional configuration options | @@ -203,4 +213,5 @@ or an options menu icon. | <i class="bwi bwi-twitch"></i> | bwi-twitch | link to our Twitch page | | <i class="bwi bwi-twitter"></i> | bwi-twitter | link to our twitter page | | <i class="bwi bwi-windows"></i> | bwi-windows | support for windows | +| <i class="bwi bwi-x-twitter"></i> | bwi-x-twitter | x version of twitter | | <i class="bwi bwi-youtube"></i> | bwi-youtube | link to our youtube page | From 91f1d9fb86142805d0774182c3ee6234e13946e3 Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Fri, 19 Apr 2024 16:44:24 -0400 Subject: [PATCH 243/351] Auth/PM-6689 - Migrate Security Stamp to Token Service and State Provider (#8792) * PM-6689 - Add security stamp to Token state * PM-6689 - Remove Security Stamp from account and state service * PM-6689 - Add security stamp get and set to token service + abstraction + tests * PM-6689 - Add migration for security stamp, test it, and register it with migrator * PM-6689 - Update sync service + deps to use token service. * PM-6689 - Cleanup missed usages of account tokens which has been removed. * PM-6689 - Per PR feedback, remove unnecessary data migration as the security stamp is only in memory and doesn't need to be migrated. --- .../browser/src/background/main.background.ts | 1 + apps/cli/src/bw.ts | 1 + .../src/services/jslib-services.module.ts | 1 + .../login-strategies/login.strategy.spec.ts | 4 - .../common/login-strategies/login.strategy.ts | 9 +-- .../src/auth/abstractions/token.service.ts | 6 ++ .../src/auth/services/token.service.spec.ts | 79 +++++++++++++++++++ .../common/src/auth/services/token.service.ts | 25 ++++++ .../src/auth/services/token.state.spec.ts | 2 + libs/common/src/auth/services/token.state.ts | 5 ++ .../platform/abstractions/state.service.ts | 2 - .../models/domain/account-tokens.spec.ts | 9 --- .../platform/models/domain/account.spec.ts | 4 +- .../src/platform/models/domain/account.ts | 18 ----- .../src/platform/services/state.service.ts | 17 ---- .../src/vault/services/sync/sync.service.ts | 6 +- 16 files changed, 126 insertions(+), 63 deletions(-) delete mode 100644 libs/common/src/platform/models/domain/account-tokens.spec.ts diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 622a115067..7d3471e598 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -788,6 +788,7 @@ export default class MainBackground { this.avatarService, logoutCallback, this.billingAccountProfileStateService, + this.tokenService, ); this.eventUploadService = new EventUploadService( this.apiService, diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index e784997d82..c3c4042adf 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -633,6 +633,7 @@ export class Main { this.avatarService, async (expired: boolean) => await this.logout(), this.billingAccountProfileStateService, + this.tokenService, ); this.totpService = new TotpService(this.cryptoFunctionService, this.logService); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 204ff5a294..9d311d34af 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -628,6 +628,7 @@ const safeProviders: SafeProvider[] = [ AvatarServiceAbstraction, LOGOUT_CALLBACK, BillingAccountProfileStateService, + TokenServiceAbstraction, ], }), safeProvider({ 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 431f736e94..e0833342ce 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.spec.ts @@ -27,7 +27,6 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Account, AccountProfile, - AccountTokens, AccountKeys, } from "@bitwarden/common/platform/models/domain/account"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; @@ -213,9 +212,6 @@ describe("LoginStrategy", () => { kdfType: kdf, }, }, - tokens: { - ...new AccountTokens(), - }, keys: new AccountKeys(), }), ); diff --git a/libs/auth/src/common/login-strategies/login.strategy.ts b/libs/auth/src/common/login-strategies/login.strategy.ts index a6dc193183..a73c32e120 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.ts @@ -27,11 +27,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 { - Account, - AccountProfile, - AccountTokens, -} from "@bitwarden/common/platform/models/domain/account"; +import { Account, AccountProfile } from "@bitwarden/common/platform/models/domain/account"; import { UserId } from "@bitwarden/common/types/guid"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; @@ -192,9 +188,6 @@ export abstract class LoginStrategy { kdfType: tokenResponse.kdf, }, }, - tokens: { - ...new AccountTokens(), - }, }), ); diff --git a/libs/common/src/auth/abstractions/token.service.ts b/libs/common/src/auth/abstractions/token.service.ts index 75bb383882..fc3bd317f4 100644 --- a/libs/common/src/auth/abstractions/token.service.ts +++ b/libs/common/src/auth/abstractions/token.service.ts @@ -213,4 +213,10 @@ export abstract class TokenService { * @returns A promise that resolves with a boolean representing the user's external authN status. */ getIsExternal: () => Promise<boolean>; + + /** Gets the active or passed in user's security stamp */ + getSecurityStamp: (userId?: UserId) => Promise<string | null>; + + /** Sets the security stamp for the active or passed in user */ + setSecurityStamp: (securityStamp: string, userId?: UserId) => Promise<void>; } diff --git a/libs/common/src/auth/services/token.service.spec.ts b/libs/common/src/auth/services/token.service.spec.ts index d32c4d8e1c..3e92053d2f 100644 --- a/libs/common/src/auth/services/token.service.spec.ts +++ b/libs/common/src/auth/services/token.service.spec.ts @@ -23,6 +23,7 @@ import { EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL, REFRESH_TOKEN_DISK, REFRESH_TOKEN_MEMORY, + SECURITY_STAMP_MEMORY, } from "./token.state"; describe("TokenService", () => { @@ -2191,6 +2192,84 @@ describe("TokenService", () => { }); }); + describe("Security Stamp methods", () => { + const mockSecurityStamp = "securityStamp"; + + describe("setSecurityStamp", () => { + it("should throw an error if no user id is provided and there is no active user in global state", async () => { + // Act + // note: don't await here because we want to test the error + const result = tokenService.setSecurityStamp(mockSecurityStamp); + // Assert + await expect(result).rejects.toThrow("User id not found. Cannot set security stamp."); + }); + + it("should set the security stamp in memory when there is an active user in global state", async () => { + // Arrange + globalStateProvider + .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) + .stateSubject.next(userIdFromAccessToken); + + // Act + await tokenService.setSecurityStamp(mockSecurityStamp); + + // Assert + expect( + singleUserStateProvider.getFake(userIdFromAccessToken, SECURITY_STAMP_MEMORY).nextMock, + ).toHaveBeenCalledWith(mockSecurityStamp); + }); + + it("should set the security stamp in memory for the specified user id", async () => { + // Act + await tokenService.setSecurityStamp(mockSecurityStamp, userIdFromAccessToken); + + // Assert + expect( + singleUserStateProvider.getFake(userIdFromAccessToken, SECURITY_STAMP_MEMORY).nextMock, + ).toHaveBeenCalledWith(mockSecurityStamp); + }); + }); + + describe("getSecurityStamp", () => { + it("should throw an error if no user id is provided and there is no active user in global state", async () => { + // Act + // note: don't await here because we want to test the error + const result = tokenService.getSecurityStamp(); + // Assert + await expect(result).rejects.toThrow("User id not found. Cannot get security stamp."); + }); + + it("should return the security stamp from memory with no user id specified (uses global active user)", async () => { + // Arrange + globalStateProvider + .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) + .stateSubject.next(userIdFromAccessToken); + + singleUserStateProvider + .getFake(userIdFromAccessToken, SECURITY_STAMP_MEMORY) + .stateSubject.next([userIdFromAccessToken, mockSecurityStamp]); + + // Act + const result = await tokenService.getSecurityStamp(); + + // Assert + expect(result).toEqual(mockSecurityStamp); + }); + + it("should return the security stamp from memory for the specified user id", async () => { + // Arrange + singleUserStateProvider + .getFake(userIdFromAccessToken, SECURITY_STAMP_MEMORY) + .stateSubject.next([userIdFromAccessToken, mockSecurityStamp]); + + // Act + const result = await tokenService.getSecurityStamp(userIdFromAccessToken); + // Assert + expect(result).toEqual(mockSecurityStamp); + }); + }); + }); + // Helpers function createTokenService(supportsSecureStorage: boolean) { return new TokenService( diff --git a/libs/common/src/auth/services/token.service.ts b/libs/common/src/auth/services/token.service.ts index c24a2c186b..40036a8453 100644 --- a/libs/common/src/auth/services/token.service.ts +++ b/libs/common/src/auth/services/token.service.ts @@ -32,6 +32,7 @@ import { EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL, REFRESH_TOKEN_DISK, REFRESH_TOKEN_MEMORY, + SECURITY_STAMP_MEMORY, } from "./token.state"; export enum TokenStorageLocation { @@ -850,6 +851,30 @@ export class TokenService implements TokenServiceAbstraction { return Array.isArray(decoded.amr) && decoded.amr.includes("external"); } + async getSecurityStamp(userId?: UserId): Promise<string | null> { + userId ??= await firstValueFrom(this.activeUserIdGlobalState.state$); + + if (!userId) { + throw new Error("User id not found. Cannot get security stamp."); + } + + const securityStamp = await this.getStateValueByUserIdAndKeyDef(userId, SECURITY_STAMP_MEMORY); + + return securityStamp; + } + + async setSecurityStamp(securityStamp: string, userId?: UserId): Promise<void> { + userId ??= await firstValueFrom(this.activeUserIdGlobalState.state$); + + if (!userId) { + throw new Error("User id not found. Cannot set security stamp."); + } + + await this.singleUserStateProvider + .get(userId, SECURITY_STAMP_MEMORY) + .update((_) => securityStamp); + } + private async getStateValueByUserIdAndKeyDef( userId: UserId, storageLocation: UserKeyDefinition<string>, diff --git a/libs/common/src/auth/services/token.state.spec.ts b/libs/common/src/auth/services/token.state.spec.ts index dc00fec383..bb82410fac 100644 --- a/libs/common/src/auth/services/token.state.spec.ts +++ b/libs/common/src/auth/services/token.state.spec.ts @@ -10,6 +10,7 @@ import { EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL, REFRESH_TOKEN_DISK, REFRESH_TOKEN_MEMORY, + SECURITY_STAMP_MEMORY, } from "./token.state"; describe.each([ @@ -22,6 +23,7 @@ describe.each([ [API_KEY_CLIENT_ID_MEMORY, "apiKeyClientIdMemory"], [API_KEY_CLIENT_SECRET_DISK, "apiKeyClientSecretDisk"], [API_KEY_CLIENT_SECRET_MEMORY, "apiKeyClientSecretMemory"], + [SECURITY_STAMP_MEMORY, "securityStamp"], ])( "deserializes state key definitions", ( diff --git a/libs/common/src/auth/services/token.state.ts b/libs/common/src/auth/services/token.state.ts index 458d6846c1..57d85f2a55 100644 --- a/libs/common/src/auth/services/token.state.ts +++ b/libs/common/src/auth/services/token.state.ts @@ -69,3 +69,8 @@ export const API_KEY_CLIENT_SECRET_MEMORY = new UserKeyDefinition<string>( clearOn: [], // Manually handled }, ); + +export const SECURITY_STAMP_MEMORY = new UserKeyDefinition<string>(TOKEN_MEMORY, "securityStamp", { + deserializer: (securityStamp) => securityStamp, + clearOn: ["logout"], +}); diff --git a/libs/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index 051604f0ae..f1d4b3848e 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -181,8 +181,6 @@ export abstract class StateService<T extends Account = Account> { * Sets the user's Pin, encrypted by the user key */ setProtectedPin: (value: string, options?: StorageOptions) => Promise<void>; - getSecurityStamp: (options?: StorageOptions) => Promise<string>; - setSecurityStamp: (value: string, options?: StorageOptions) => Promise<void>; getUserId: (options?: StorageOptions) => Promise<string>; getVaultTimeout: (options?: StorageOptions) => Promise<number>; setVaultTimeout: (value: number, options?: StorageOptions) => Promise<void>; diff --git a/libs/common/src/platform/models/domain/account-tokens.spec.ts b/libs/common/src/platform/models/domain/account-tokens.spec.ts deleted file mode 100644 index 733b3908e9..0000000000 --- a/libs/common/src/platform/models/domain/account-tokens.spec.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { AccountTokens } from "./account"; - -describe("AccountTokens", () => { - describe("fromJSON", () => { - it("should deserialize to an instance of itself", () => { - expect(AccountTokens.fromJSON({})).toBeInstanceOf(AccountTokens); - }); - }); -}); diff --git a/libs/common/src/platform/models/domain/account.spec.ts b/libs/common/src/platform/models/domain/account.spec.ts index 0c76c16cc2..77c242b6ff 100644 --- a/libs/common/src/platform/models/domain/account.spec.ts +++ b/libs/common/src/platform/models/domain/account.spec.ts @@ -1,4 +1,4 @@ -import { Account, AccountKeys, AccountProfile, AccountSettings, AccountTokens } from "./account"; +import { Account, AccountKeys, AccountProfile, AccountSettings } from "./account"; describe("Account", () => { describe("fromJSON", () => { @@ -10,14 +10,12 @@ describe("Account", () => { const keysSpy = jest.spyOn(AccountKeys, "fromJSON"); const profileSpy = jest.spyOn(AccountProfile, "fromJSON"); const settingsSpy = jest.spyOn(AccountSettings, "fromJSON"); - const tokensSpy = jest.spyOn(AccountTokens, "fromJSON"); Account.fromJSON({}); expect(keysSpy).toHaveBeenCalled(); expect(profileSpy).toHaveBeenCalled(); expect(settingsSpy).toHaveBeenCalled(); - expect(tokensSpy).toHaveBeenCalled(); }); }); }); diff --git a/libs/common/src/platform/models/domain/account.ts b/libs/common/src/platform/models/domain/account.ts index 5a9a764696..cd416ec1f9 100644 --- a/libs/common/src/platform/models/domain/account.ts +++ b/libs/common/src/platform/models/domain/account.ts @@ -171,24 +171,11 @@ export class AccountSettings { } } -export class AccountTokens { - securityStamp?: string; - - static fromJSON(obj: Jsonify<AccountTokens>): AccountTokens { - if (obj == null) { - return null; - } - - return Object.assign(new AccountTokens(), obj); - } -} - export class Account { data?: AccountData = new AccountData(); keys?: AccountKeys = new AccountKeys(); profile?: AccountProfile = new AccountProfile(); settings?: AccountSettings = new AccountSettings(); - tokens?: AccountTokens = new AccountTokens(); constructor(init: Partial<Account>) { Object.assign(this, { @@ -208,10 +195,6 @@ export class Account { ...new AccountSettings(), ...init?.settings, }, - tokens: { - ...new AccountTokens(), - ...init?.tokens, - }, }); } @@ -225,7 +208,6 @@ export class Account { data: AccountData.fromJSON(json?.data), profile: AccountProfile.fromJSON(json?.profile), settings: AccountSettings.fromJSON(json?.settings), - tokens: AccountTokens.fromJSON(json?.tokens), }); } } diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index f660cd7a34..d0a55d7a47 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -839,23 +839,6 @@ export class StateService< ); } - async getSecurityStamp(options?: StorageOptions): Promise<string> { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) - )?.tokens?.securityStamp; - } - - async setSecurityStamp(value: string, options?: StorageOptions): Promise<void> { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - account.tokens.securityStamp = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - } - async getUserId(options?: StorageOptions): Promise<string> { return ( await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) diff --git a/libs/common/src/vault/services/sync/sync.service.ts b/libs/common/src/vault/services/sync/sync.service.ts index ff8e9f1f4f..73869ff488 100644 --- a/libs/common/src/vault/services/sync/sync.service.ts +++ b/libs/common/src/vault/services/sync/sync.service.ts @@ -15,6 +15,7 @@ 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 { TokenService } from "../../../auth/abstractions/token.service"; 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"; @@ -73,6 +74,7 @@ export class SyncService implements SyncServiceAbstraction { private avatarService: AvatarService, private logoutCallback: (expired: boolean) => Promise<void>, private billingAccountProfileStateService: BillingAccountProfileStateService, + private tokenService: TokenService, ) {} async getLastSync(): Promise<Date> { @@ -309,7 +311,7 @@ export class SyncService implements SyncServiceAbstraction { } private async syncProfile(response: ProfileResponse) { - const stamp = await this.stateService.getSecurityStamp(); + const stamp = await this.tokenService.getSecurityStamp(response.id as UserId); if (stamp != null && stamp !== response.securityStamp) { if (this.logoutCallback != null) { await this.logoutCallback(true); @@ -323,7 +325,7 @@ export class SyncService implements SyncServiceAbstraction { await this.cryptoService.setProviderKeys(response.providers); await this.cryptoService.setOrgKeys(response.organizations, response.providerOrganizations); await this.avatarService.setSyncAvatarColor(response.id as UserId, response.avatarColor); - await this.stateService.setSecurityStamp(response.securityStamp); + await this.tokenService.setSecurityStamp(response.securityStamp, response.id as UserId); await this.stateService.setEmailVerified(response.emailVerified); await this.billingAccountProfileStateService.setHasPremium( From f829cdd8a724dae42ab93aaadf930d433dd2cc4d Mon Sep 17 00:00:00 2001 From: aj-rosado <109146700+aj-rosado@users.noreply.github.com> Date: Mon, 22 Apr 2024 10:18:11 +0100 Subject: [PATCH 244/351] [PM-7603] Fix individual vault export not appearing on Event Logs (#8829) * Added validation to update User_ClientExportedVault on events even with no organization id or cipher id * Fixed missing data and validation --- .../common/src/services/event/event-collection.service.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/libs/common/src/services/event/event-collection.service.ts b/libs/common/src/services/event/event-collection.service.ts index 641c1b4d44..1482bb8b61 100644 --- a/libs/common/src/services/event/event-collection.service.ts +++ b/libs/common/src/services/event/event-collection.service.ts @@ -36,7 +36,7 @@ export class EventCollectionService implements EventCollectionServiceAbstraction const userId = await firstValueFrom(this.stateProvider.activeUserId$); const eventStore = this.stateProvider.getUser(userId, EVENT_COLLECTION); - if (!(await this.shouldUpdate(cipherId, organizationId))) { + if (!(await this.shouldUpdate(cipherId, organizationId, eventType))) { return; } @@ -64,6 +64,7 @@ export class EventCollectionService implements EventCollectionServiceAbstraction private async shouldUpdate( cipherId: string = null, organizationId: string = null, + eventType: EventType = null, ): Promise<boolean> { const orgIds$ = this.organizationService.organizations$.pipe( map((orgs) => orgs?.filter((o) => o.useEvents)?.map((x) => x.id) ?? []), @@ -85,6 +86,11 @@ export class EventCollectionService implements EventCollectionServiceAbstraction return false; } + // Individual vault export doesn't need cipher id or organization id. + if (eventType == EventType.User_ClientExportedVault) { + return true; + } + // If the cipher is null there must be an organization id provided if (cipher == null && organizationId == null) { return false; From b5362ca1ce6f4260b3ae52eecc6b0e9ace325945 Mon Sep 17 00:00:00 2001 From: Matt Gibson <mgibson@bitwarden.com> Date: Mon, 22 Apr 2024 08:55:19 -0400 Subject: [PATCH 245/351] Browser MV3: Default store values to session storage (#8844) * Introduce browser large object storage location. This location is encrypted and serialized to disk in order to allow for storage of uncountable things like vault items that take a significant amount of time to prepare, but are not guaranteed to fit within session storage. however, limit the need to write to disk is a big benefit, so _most_ things are written to storage.session instead, where things specifically flagged as large will be moved to disk-backed memory * Store derived values in large object store for browser * Fix AbstractMemoryStorageService implementation --- .../browser/src/background/main.background.ts | 18 ++++++---- .../derived-state-provider.factory.ts | 10 +++--- .../browser-memory-storage.service.ts | 11 +++++- .../background-derived-state.provider.ts | 9 ++++- .../state/background-derived-state.ts | 2 +- .../state/derived-state-interactions.spec.ts | 28 +++++++++------ .../foreground-derived-state.provider.ts | 9 +++-- .../state/foreground-derived-state.spec.ts | 3 +- .../state/foreground-derived-state.ts | 3 +- .../browser-storage-service.provider.ts | 35 +++++++++++++++++++ .../src/popup/services/services.module.ts | 32 ++++++++++++++++- apps/cli/src/bw.ts | 4 +-- apps/desktop/src/main.ts | 2 +- .../src/services/jslib-services.module.ts | 2 +- libs/common/spec/fake-state-provider.ts | 4 +-- .../src/platform/state/derive-definition.ts | 4 +-- .../default-derived-state.provider.ts | 17 ++++++--- .../default-derived-state.spec.ts | 16 ++++----- .../src/platform/state/state-definition.ts | 4 ++- .../src/platform/state/state-definitions.ts | 16 ++++++--- 20 files changed, 171 insertions(+), 58 deletions(-) create mode 100644 apps/browser/src/platform/storage/browser-storage-service.provider.ts diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 7d3471e598..0a9ad44962 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -111,7 +111,6 @@ import { KeyGenerationService } from "@bitwarden/common/platform/services/key-ge 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"; -import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider"; import { SystemService } from "@bitwarden/common/platform/services/system.service"; import { UserKeyInitService } from "@bitwarden/common/platform/services/user-key-init.service"; import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service"; @@ -226,6 +225,7 @@ import { BackgroundPlatformUtilsService } from "../platform/services/platform-ut import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service"; import { BackgroundDerivedStateProvider } from "../platform/state/background-derived-state.provider"; import { BackgroundMemoryStorageService } from "../platform/storage/background-memory-storage.service"; +import { BrowserStorageServiceProvider } from "../platform/storage/browser-storage-service.provider"; import { ForegroundMemoryStorageService } from "../platform/storage/foreground-memory-storage.service"; import { fromChromeRuntimeMessaging } from "../platform/utils/from-chrome-runtime-messaging"; import VaultTimeoutService from "../services/vault-timeout/vault-timeout.service"; @@ -246,6 +246,8 @@ export default class MainBackground { secureStorageService: AbstractStorageService; memoryStorageService: AbstractMemoryStorageService; memoryStorageForStateProviders: AbstractMemoryStorageService & ObservableStorageService; + largeObjectMemoryStorageForStateProviders: AbstractMemoryStorageService & + ObservableStorageService; i18nService: I18nServiceAbstraction; platformUtilsService: PlatformUtilsServiceAbstraction; logService: LogServiceAbstraction; @@ -424,12 +426,16 @@ export default class MainBackground { ? mv3MemoryStorageCreator("stateService") : new MemoryStorageService(); this.memoryStorageForStateProviders = BrowserApi.isManifestVersion(3) - ? mv3MemoryStorageCreator("stateProviders") - : new BackgroundMemoryStorageService(); + ? new BrowserMemoryStorageService() // mv3 stores to storage.session + : new BackgroundMemoryStorageService(); // mv2 stores to memory + this.largeObjectMemoryStorageForStateProviders = BrowserApi.isManifestVersion(3) + ? mv3MemoryStorageCreator("stateProviders") // mv3 stores to local-backed session storage + : this.memoryStorageForStateProviders; // mv2 stores to the same location - const storageServiceProvider = new StorageServiceProvider( + const storageServiceProvider = new BrowserStorageServiceProvider( this.storageService, this.memoryStorageForStateProviders, + this.largeObjectMemoryStorageForStateProviders, ); this.globalStateProvider = new DefaultGlobalStateProvider(storageServiceProvider); @@ -466,9 +472,7 @@ export default class MainBackground { this.accountService, this.singleUserStateProvider, ); - this.derivedStateProvider = new BackgroundDerivedStateProvider( - this.memoryStorageForStateProviders, - ); + this.derivedStateProvider = new BackgroundDerivedStateProvider(storageServiceProvider); this.stateProvider = new DefaultStateProvider( this.activeUserStateProvider, this.singleUserStateProvider, diff --git a/apps/browser/src/platform/background/service-factories/derived-state-provider.factory.ts b/apps/browser/src/platform/background/service-factories/derived-state-provider.factory.ts index 4f329c93d5..4025d01950 100644 --- a/apps/browser/src/platform/background/service-factories/derived-state-provider.factory.ts +++ b/apps/browser/src/platform/background/service-factories/derived-state-provider.factory.ts @@ -4,14 +4,14 @@ import { BackgroundDerivedStateProvider } from "../../state/background-derived-s import { CachedServices, FactoryOptions, factory } from "./factory-options"; import { - MemoryStorageServiceInitOptions, - observableMemoryStorageServiceFactory, -} from "./storage-service.factory"; + StorageServiceProviderInitOptions, + storageServiceProviderFactory, +} from "./storage-service-provider.factory"; type DerivedStateProviderFactoryOptions = FactoryOptions; export type DerivedStateProviderInitOptions = DerivedStateProviderFactoryOptions & - MemoryStorageServiceInitOptions; + StorageServiceProviderInitOptions; export async function derivedStateProviderFactory( cache: { derivedStateProvider?: DerivedStateProvider } & CachedServices, @@ -22,6 +22,6 @@ export async function derivedStateProviderFactory( "derivedStateProvider", opts, async () => - new BackgroundDerivedStateProvider(await observableMemoryStorageServiceFactory(cache, opts)), + new BackgroundDerivedStateProvider(await storageServiceProviderFactory(cache, opts)), ); } diff --git a/apps/browser/src/platform/services/browser-memory-storage.service.ts b/apps/browser/src/platform/services/browser-memory-storage.service.ts index f824a1df0d..b067dc5a12 100644 --- a/apps/browser/src/platform/services/browser-memory-storage.service.ts +++ b/apps/browser/src/platform/services/browser-memory-storage.service.ts @@ -1,7 +1,16 @@ +import { AbstractMemoryStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; + import AbstractChromeStorageService from "./abstractions/abstract-chrome-storage-api.service"; -export default class BrowserMemoryStorageService extends AbstractChromeStorageService { +export default class BrowserMemoryStorageService + extends AbstractChromeStorageService + implements AbstractMemoryStorageService +{ constructor() { super(chrome.storage.session); } + type = "MemoryStorageService" as const; + getBypassCache<T>(key: string): Promise<T> { + return this.get(key); + } } diff --git a/apps/browser/src/platform/state/background-derived-state.provider.ts b/apps/browser/src/platform/state/background-derived-state.provider.ts index 95eec71113..f3d217789e 100644 --- a/apps/browser/src/platform/state/background-derived-state.provider.ts +++ b/apps/browser/src/platform/state/background-derived-state.provider.ts @@ -1,5 +1,9 @@ import { Observable } from "rxjs"; +import { + AbstractStorageService, + ObservableStorageService, +} from "@bitwarden/common/platform/abstractions/storage.service"; import { DeriveDefinition, DerivedState } from "@bitwarden/common/platform/state"; // eslint-disable-next-line import/no-restricted-paths -- extending this class for this client import { DefaultDerivedStateProvider } from "@bitwarden/common/platform/state/implementations/default-derived-state.provider"; @@ -12,11 +16,14 @@ export class BackgroundDerivedStateProvider extends DefaultDerivedStateProvider parentState$: Observable<TFrom>, deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>, dependencies: TDeps, + storageLocation: [string, AbstractStorageService & ObservableStorageService], ): DerivedState<TTo> { + const [cacheKey, storageService] = storageLocation; return new BackgroundDerivedState( parentState$, deriveDefinition, - this.memoryStorage, + storageService, + cacheKey, dependencies, ); } diff --git a/apps/browser/src/platform/state/background-derived-state.ts b/apps/browser/src/platform/state/background-derived-state.ts index 7a7146aa88..c62795acdc 100644 --- a/apps/browser/src/platform/state/background-derived-state.ts +++ b/apps/browser/src/platform/state/background-derived-state.ts @@ -23,10 +23,10 @@ export class BackgroundDerivedState< parentState$: Observable<TFrom>, deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>, memoryStorage: AbstractStorageService & ObservableStorageService, + portName: string, dependencies: TDeps, ) { super(parentState$, deriveDefinition, memoryStorage, dependencies); - const portName = deriveDefinition.buildCacheKey(); // listen for foreground derived states to connect BrowserApi.addListener(chrome.runtime.onConnect, (port) => { diff --git a/apps/browser/src/platform/state/derived-state-interactions.spec.ts b/apps/browser/src/platform/state/derived-state-interactions.spec.ts index d709c401af..a5df01bc98 100644 --- a/apps/browser/src/platform/state/derived-state-interactions.spec.ts +++ b/apps/browser/src/platform/state/derived-state-interactions.spec.ts @@ -38,14 +38,21 @@ describe("foreground background derived state interactions", () => { let memoryStorage: FakeStorageService; const initialParent = "2020-01-01"; const ngZone = mock<NgZone>(); + const portName = "testPort"; beforeEach(() => { mockPorts(); parentState$ = new Subject<string>(); memoryStorage = new FakeStorageService(); - background = new BackgroundDerivedState(parentState$, deriveDefinition, memoryStorage, {}); - foreground = new ForegroundDerivedState(deriveDefinition, memoryStorage, ngZone); + background = new BackgroundDerivedState( + parentState$, + deriveDefinition, + memoryStorage, + portName, + {}, + ); + foreground = new ForegroundDerivedState(deriveDefinition, memoryStorage, portName, ngZone); }); afterEach(() => { @@ -65,7 +72,12 @@ describe("foreground background derived state interactions", () => { }); it("should initialize a late-connected foreground", async () => { - const newForeground = new ForegroundDerivedState(deriveDefinition, memoryStorage, ngZone); + const newForeground = new ForegroundDerivedState( + deriveDefinition, + memoryStorage, + portName, + ngZone, + ); const backgroundEmissions = trackEmissions(background.state$); parentState$.next(initialParent); await awaitAsync(); @@ -82,8 +94,6 @@ describe("foreground background derived state interactions", () => { const dateString = "2020-12-12"; const emissions = trackEmissions(background.state$); - // 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 await foreground.forceValue(new Date(dateString)); await awaitAsync(); @@ -99,9 +109,7 @@ describe("foreground background derived state interactions", () => { expect(foreground["port"]).toBeDefined(); const newDate = new Date(); - // 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 - foreground.forceValue(newDate); + await foreground.forceValue(newDate); await awaitAsync(); expect(connectMock.mock.calls.length).toBe(initialConnectCalls); @@ -114,9 +122,7 @@ describe("foreground background derived state interactions", () => { expect(foreground["port"]).toBeUndefined(); const newDate = new Date(); - // 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 - foreground.forceValue(newDate); + await foreground.forceValue(newDate); await awaitAsync(); expect(connectMock.mock.calls.length).toBe(initialConnectCalls + 1); diff --git a/apps/browser/src/platform/state/foreground-derived-state.provider.ts b/apps/browser/src/platform/state/foreground-derived-state.provider.ts index ccefb1157c..d9262e3b6e 100644 --- a/apps/browser/src/platform/state/foreground-derived-state.provider.ts +++ b/apps/browser/src/platform/state/foreground-derived-state.provider.ts @@ -5,6 +5,7 @@ import { AbstractStorageService, ObservableStorageService, } from "@bitwarden/common/platform/abstractions/storage.service"; +import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider"; import { DeriveDefinition, DerivedState } from "@bitwarden/common/platform/state"; // eslint-disable-next-line import/no-restricted-paths -- extending this class for this client import { DefaultDerivedStateProvider } from "@bitwarden/common/platform/state/implementations/default-derived-state.provider"; @@ -14,16 +15,18 @@ import { ForegroundDerivedState } from "./foreground-derived-state"; export class ForegroundDerivedStateProvider extends DefaultDerivedStateProvider { constructor( - memoryStorage: AbstractStorageService & ObservableStorageService, + storageServiceProvider: StorageServiceProvider, private ngZone: NgZone, ) { - super(memoryStorage); + super(storageServiceProvider); } override buildDerivedState<TFrom, TTo, TDeps extends DerivedStateDependencies>( _parentState$: Observable<TFrom>, deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>, _dependencies: TDeps, + storageLocation: [string, AbstractStorageService & ObservableStorageService], ): DerivedState<TTo> { - return new ForegroundDerivedState(deriveDefinition, this.memoryStorage, this.ngZone); + const [cacheKey, storageService] = storageLocation; + return new ForegroundDerivedState(deriveDefinition, storageService, cacheKey, this.ngZone); } } diff --git a/apps/browser/src/platform/state/foreground-derived-state.spec.ts b/apps/browser/src/platform/state/foreground-derived-state.spec.ts index fce672a5ef..2c29f39bc1 100644 --- a/apps/browser/src/platform/state/foreground-derived-state.spec.ts +++ b/apps/browser/src/platform/state/foreground-derived-state.spec.ts @@ -33,13 +33,14 @@ jest.mock("../browser/run-inside-angular.operator", () => { describe("ForegroundDerivedState", () => { let sut: ForegroundDerivedState<Date>; let memoryStorage: FakeStorageService; + const portName = "testPort"; const ngZone = mock<NgZone>(); beforeEach(() => { memoryStorage = new FakeStorageService(); memoryStorage.internalUpdateValuesRequireDeserialization(true); mockPorts(); - sut = new ForegroundDerivedState(deriveDefinition, memoryStorage, ngZone); + sut = new ForegroundDerivedState(deriveDefinition, memoryStorage, portName, ngZone); }); afterEach(() => { diff --git a/apps/browser/src/platform/state/foreground-derived-state.ts b/apps/browser/src/platform/state/foreground-derived-state.ts index b005697be8..b9dda763df 100644 --- a/apps/browser/src/platform/state/foreground-derived-state.ts +++ b/apps/browser/src/platform/state/foreground-derived-state.ts @@ -35,6 +35,7 @@ export class ForegroundDerivedState<TTo> implements DerivedState<TTo> { constructor( private deriveDefinition: DeriveDefinition<unknown, TTo, DerivedStateDependencies>, private memoryStorage: AbstractStorageService & ObservableStorageService, + private portName: string, private ngZone: NgZone, ) { this.storageKey = deriveDefinition.storageKey; @@ -88,7 +89,7 @@ export class ForegroundDerivedState<TTo> implements DerivedState<TTo> { return; } - this.port = chrome.runtime.connect({ name: this.deriveDefinition.buildCacheKey() }); + this.port = chrome.runtime.connect({ name: this.portName }); this.backgroundResponses$ = fromChromeEvent(this.port.onMessage).pipe( map(([message]) => message as DerivedStateMessage), diff --git a/apps/browser/src/platform/storage/browser-storage-service.provider.ts b/apps/browser/src/platform/storage/browser-storage-service.provider.ts new file mode 100644 index 0000000000..e0214baef4 --- /dev/null +++ b/apps/browser/src/platform/storage/browser-storage-service.provider.ts @@ -0,0 +1,35 @@ +import { + AbstractStorageService, + ObservableStorageService, +} from "@bitwarden/common/platform/abstractions/storage.service"; +import { + PossibleLocation, + StorageServiceProvider, +} from "@bitwarden/common/platform/services/storage-service.provider"; +// eslint-disable-next-line import/no-restricted-paths +import { ClientLocations } from "@bitwarden/common/platform/state/state-definition"; + +export class BrowserStorageServiceProvider extends StorageServiceProvider { + constructor( + diskStorageService: AbstractStorageService & ObservableStorageService, + limitedMemoryStorageService: AbstractStorageService & ObservableStorageService, + private largeObjectMemoryStorageService: AbstractStorageService & ObservableStorageService, + ) { + super(diskStorageService, limitedMemoryStorageService); + } + + override get( + defaultLocation: PossibleLocation, + overrides: Partial<ClientLocations>, + ): [location: PossibleLocation, service: AbstractStorageService & ObservableStorageService] { + const location = overrides["browser"] ?? defaultLocation; + switch (location) { + case "memory-large-object": + return ["memory-large-object", this.largeObjectMemoryStorageService]; + default: + // Pass in computed location to super because they could have + // override default "disk" with web "memory". + return super.get(location, overrides); + } + } +} diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 123e901e4e..a7da6b7612 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -71,6 +71,7 @@ import { GlobalState } from "@bitwarden/common/platform/models/domain/global-sta import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; +import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider"; import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service"; import { DerivedStateProvider, @@ -108,6 +109,7 @@ import { DefaultBrowserStateService } from "../../platform/services/default-brow import I18nService from "../../platform/services/i18n.service"; import { ForegroundPlatformUtilsService } from "../../platform/services/platform-utils/foreground-platform-utils.service"; import { ForegroundDerivedStateProvider } from "../../platform/state/foreground-derived-state.provider"; +import { BrowserStorageServiceProvider } from "../../platform/storage/browser-storage-service.provider"; import { ForegroundMemoryStorageService } from "../../platform/storage/foreground-memory-storage.service"; import { fromChromeRuntimeMessaging } from "../../platform/utils/from-chrome-runtime-messaging"; import { BrowserSendStateService } from "../../tools/popup/services/browser-send-state.service"; @@ -120,6 +122,10 @@ import { InitService } from "./init.service"; import { PopupCloseWarningService } from "./popup-close-warning.service"; import { PopupSearchService } from "./popup-search.service"; +const OBSERVABLE_LARGE_OBJECT_MEMORY_STORAGE = new SafeInjectionToken< + AbstractStorageService & ObservableStorageService +>("OBSERVABLE_LARGE_OBJECT_MEMORY_STORAGE"); + const needsBackgroundInit = BrowserPopupUtils.backgroundInitializationRequired(); const isPrivateMode = BrowserPopupUtils.inPrivateMode(); const mainBackground: MainBackground = needsBackgroundInit @@ -380,6 +386,21 @@ const safeProviders: SafeProvider[] = [ }, deps: [], }), + safeProvider({ + provide: OBSERVABLE_LARGE_OBJECT_MEMORY_STORAGE, + useFactory: ( + regularMemoryStorageService: AbstractMemoryStorageService & ObservableStorageService, + ) => { + if (BrowserApi.isManifestVersion(2)) { + return regularMemoryStorageService; + } + + return getBgService<AbstractStorageService & ObservableStorageService>( + "largeObjectMemoryStorageForStateProviders", + )(); + }, + deps: [OBSERVABLE_MEMORY_STORAGE], + }), safeProvider({ provide: OBSERVABLE_DISK_STORAGE, useExisting: AbstractStorageService, @@ -466,7 +487,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: DerivedStateProvider, useClass: ForegroundDerivedStateProvider, - deps: [OBSERVABLE_MEMORY_STORAGE, NgZone], + deps: [StorageServiceProvider, NgZone], }), safeProvider({ provide: AutofillSettingsServiceAbstraction, @@ -542,6 +563,15 @@ const safeProviders: SafeProvider[] = [ }, deps: [], }), + safeProvider({ + provide: StorageServiceProvider, + useClass: BrowserStorageServiceProvider, + deps: [ + OBSERVABLE_DISK_STORAGE, + OBSERVABLE_MEMORY_STORAGE, + OBSERVABLE_LARGE_OBJECT_MEMORY_STORAGE, + ], + }), ]; @NgModule({ diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index c3c4042adf..437f807bc6 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -309,9 +309,7 @@ export class Main { this.singleUserStateProvider, ); - this.derivedStateProvider = new DefaultDerivedStateProvider( - this.memoryStorageForStateProviders, - ); + this.derivedStateProvider = new DefaultDerivedStateProvider(storageServiceProvider); this.stateProvider = new DefaultStateProvider( this.activeUserStateProvider, diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 0655e5600d..bffd2002ff 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -157,7 +157,7 @@ export class Main { activeUserStateProvider, singleUserStateProvider, globalStateProvider, - new DefaultDerivedStateProvider(this.memoryStorageForStateProviders), + new DefaultDerivedStateProvider(storageServiceProvider), ); this.environmentService = new DefaultEnvironmentService(stateProvider, accountService); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 9d311d34af..27b182de5d 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -1047,7 +1047,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: DerivedStateProvider, useClass: DefaultDerivedStateProvider, - deps: [OBSERVABLE_MEMORY_STORAGE], + deps: [StorageServiceProvider], }), safeProvider({ provide: StateProvider, diff --git a/libs/common/spec/fake-state-provider.ts b/libs/common/spec/fake-state-provider.ts index 2078fe3abd..306ae00c21 100644 --- a/libs/common/spec/fake-state-provider.ts +++ b/libs/common/spec/fake-state-provider.ts @@ -249,11 +249,11 @@ export class FakeDerivedStateProvider implements DerivedStateProvider { deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>, dependencies: TDeps, ): DerivedState<TTo> { - let result = this.states.get(deriveDefinition.buildCacheKey()) as DerivedState<TTo>; + let result = this.states.get(deriveDefinition.buildCacheKey("memory")) as DerivedState<TTo>; if (result == null) { result = new FakeDerivedState(parentState$, deriveDefinition, dependencies); - this.states.set(deriveDefinition.buildCacheKey(), result); + this.states.set(deriveDefinition.buildCacheKey("memory"), result); } return result; } diff --git a/libs/common/src/platform/state/derive-definition.ts b/libs/common/src/platform/state/derive-definition.ts index 8f62d3a342..9cb5eff3e8 100644 --- a/libs/common/src/platform/state/derive-definition.ts +++ b/libs/common/src/platform/state/derive-definition.ts @@ -171,8 +171,8 @@ export class DeriveDefinition<TFrom, TTo, TDeps extends DerivedStateDependencies return this.options.clearOnCleanup ?? true; } - buildCacheKey(): string { - return `derived_${this.stateDefinition.name}_${this.uniqueDerivationName}`; + buildCacheKey(location: string): string { + return `derived_${location}_${this.stateDefinition.name}_${this.uniqueDerivationName}`; } /** diff --git a/libs/common/src/platform/state/implementations/default-derived-state.provider.ts b/libs/common/src/platform/state/implementations/default-derived-state.provider.ts index 48ccb9d300..02d35fdf0c 100644 --- a/libs/common/src/platform/state/implementations/default-derived-state.provider.ts +++ b/libs/common/src/platform/state/implementations/default-derived-state.provider.ts @@ -5,6 +5,7 @@ import { AbstractStorageService, ObservableStorageService, } from "../../abstractions/storage.service"; +import { StorageServiceProvider } from "../../services/storage-service.provider"; import { DeriveDefinition } from "../derive-definition"; import { DerivedState } from "../derived-state"; import { DerivedStateProvider } from "../derived-state.provider"; @@ -14,14 +15,18 @@ import { DefaultDerivedState } from "./default-derived-state"; export class DefaultDerivedStateProvider implements DerivedStateProvider { private cache: Record<string, DerivedState<unknown>> = {}; - constructor(protected memoryStorage: AbstractStorageService & ObservableStorageService) {} + constructor(protected storageServiceProvider: StorageServiceProvider) {} get<TFrom, TTo, TDeps extends DerivedStateDependencies>( parentState$: Observable<TFrom>, deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>, dependencies: TDeps, ): DerivedState<TTo> { - const cacheKey = deriveDefinition.buildCacheKey(); + // TODO: we probably want to support optional normal memory storage for browser + const [location, storageService] = this.storageServiceProvider.get("memory", { + browser: "memory-large-object", + }); + const cacheKey = deriveDefinition.buildCacheKey(location); const existingDerivedState = this.cache[cacheKey]; if (existingDerivedState != null) { // I have to cast out of the unknown generic but this should be safe if rules @@ -29,7 +34,10 @@ export class DefaultDerivedStateProvider implements DerivedStateProvider { return existingDerivedState as DefaultDerivedState<TFrom, TTo, TDeps>; } - const newDerivedState = this.buildDerivedState(parentState$, deriveDefinition, dependencies); + const newDerivedState = this.buildDerivedState(parentState$, deriveDefinition, dependencies, [ + location, + storageService, + ]); this.cache[cacheKey] = newDerivedState; return newDerivedState; } @@ -38,11 +46,12 @@ export class DefaultDerivedStateProvider implements DerivedStateProvider { parentState$: Observable<TFrom>, deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>, dependencies: TDeps, + storageLocation: [string, AbstractStorageService & ObservableStorageService], ): DerivedState<TTo> { return new DefaultDerivedState<TFrom, TTo, TDeps>( parentState$, deriveDefinition, - this.memoryStorage, + storageLocation[1], dependencies, ); } diff --git a/libs/common/src/platform/state/implementations/default-derived-state.spec.ts b/libs/common/src/platform/state/implementations/default-derived-state.spec.ts index 958a938611..e3b1587e3a 100644 --- a/libs/common/src/platform/state/implementations/default-derived-state.spec.ts +++ b/libs/common/src/platform/state/implementations/default-derived-state.spec.ts @@ -72,12 +72,12 @@ describe("DefaultDerivedState", () => { parentState$.next(dateString); await awaitAsync(); - expect(memoryStorage.internalStore[deriveDefinition.buildCacheKey()]).toEqual( + expect(memoryStorage.internalStore[deriveDefinition.storageKey]).toEqual( derivedValue(new Date(dateString)), ); const calls = memoryStorage.mock.save.mock.calls; expect(calls.length).toBe(1); - expect(calls[0][0]).toBe(deriveDefinition.buildCacheKey()); + expect(calls[0][0]).toBe(deriveDefinition.storageKey); expect(calls[0][1]).toEqual(derivedValue(new Date(dateString))); }); @@ -94,7 +94,7 @@ describe("DefaultDerivedState", () => { it("should store the forced value", async () => { await sut.forceValue(forced); - expect(memoryStorage.internalStore[deriveDefinition.buildCacheKey()]).toEqual( + expect(memoryStorage.internalStore[deriveDefinition.storageKey]).toEqual( derivedValue(forced), ); }); @@ -109,7 +109,7 @@ describe("DefaultDerivedState", () => { it("should store the forced value", async () => { await sut.forceValue(forced); - expect(memoryStorage.internalStore[deriveDefinition.buildCacheKey()]).toEqual( + expect(memoryStorage.internalStore[deriveDefinition.storageKey]).toEqual( derivedValue(forced), ); }); @@ -153,7 +153,7 @@ describe("DefaultDerivedState", () => { parentState$.next(newDate); await awaitAsync(); - expect(memoryStorage.internalStore[deriveDefinition.buildCacheKey()]).toEqual( + expect(memoryStorage.internalStore[deriveDefinition.storageKey]).toEqual( derivedValue(new Date(newDate)), ); @@ -161,7 +161,7 @@ describe("DefaultDerivedState", () => { // Wait for cleanup await awaitAsync(cleanupDelayMs * 2); - expect(memoryStorage.internalStore[deriveDefinition.buildCacheKey()]).toBeUndefined(); + expect(memoryStorage.internalStore[deriveDefinition.storageKey]).toBeUndefined(); }); it("should not clear state after cleanup if clearOnCleanup is false", async () => { @@ -171,7 +171,7 @@ describe("DefaultDerivedState", () => { parentState$.next(newDate); await awaitAsync(); - expect(memoryStorage.internalStore[deriveDefinition.buildCacheKey()]).toEqual( + expect(memoryStorage.internalStore[deriveDefinition.storageKey]).toEqual( derivedValue(new Date(newDate)), ); @@ -179,7 +179,7 @@ describe("DefaultDerivedState", () => { // Wait for cleanup await awaitAsync(cleanupDelayMs * 2); - expect(memoryStorage.internalStore[deriveDefinition.buildCacheKey()]).toEqual( + expect(memoryStorage.internalStore[deriveDefinition.storageKey]).toEqual( derivedValue(new Date(newDate)), ); }); diff --git a/libs/common/src/platform/state/state-definition.ts b/libs/common/src/platform/state/state-definition.ts index 15dc9ff757..f1e7dc80ab 100644 --- a/libs/common/src/platform/state/state-definition.ts +++ b/libs/common/src/platform/state/state-definition.ts @@ -24,8 +24,10 @@ export type ClientLocations = { web: StorageLocation | "disk-local"; /** * Overriding storage location for browser clients. + * + * "memory-large-object" is used to store non-countable objects in memory. This exists due to limited persistent memory available to browser extensions. */ - //browser: StorageLocation; + browser: StorageLocation | "memory-large-object"; /** * Overriding storage location for desktop clients. */ diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index 18df252062..e04110f28b 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -116,7 +116,9 @@ export const EVENT_COLLECTION_DISK = new StateDefinition("eventCollection", "dis export const SEND_DISK = new StateDefinition("encryptedSend", "disk", { web: "memory", }); -export const SEND_MEMORY = new StateDefinition("decryptedSend", "memory"); +export const SEND_MEMORY = new StateDefinition("decryptedSend", "memory", { + browser: "memory-large-object", +}); // Vault @@ -133,10 +135,16 @@ 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 VAULT_BROWSER_MEMORY = new StateDefinition("vaultBrowser", "memory", { + browser: "memory-large-object", +}); +export const VAULT_SEARCH_MEMORY = new StateDefinition("vaultSearch", "memory", { + browser: "memory-large-object", +}); 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"); +export const CIPHERS_MEMORY = new StateDefinition("ciphersMemory", "memory", { + browser: "memory-large-object", +}); From 300b17aaeb327d6593d8e90e4943ecd703e9fcdb Mon Sep 17 00:00:00 2001 From: Matt Gibson <mgibson@bitwarden.com> Date: Mon, 22 Apr 2024 10:14:38 -0400 Subject: [PATCH 246/351] [PM-7653] Do not store disk-backed sessions as single blobs (#8852) * Implement a lazy value class This will be used as a source for composing key-protected storage from a single key source. * Simplify local-backed-session-storage The new implementation stores each value to a unique location, prefixed with `session_` to help indicate the purpose. I've also removed the complexity around session keys, favoring passing in a pre-defined value that is determined lazily once for the service worker. This is more in line with how I expect a key-protected storage would work. * Remove decrypted session flag This has been nothing but an annoyance. If it's ever added back, it needs to have some way to determine if the session key matches the one it was written with * Remove unnecessary string interpolation * Remove sync Lazy This is better done as a separate class. * Handle async through type * prefer two factory calls to incorrect value on races. * Fix type * Remove log * Update libs/common/src/platform/misc/lazy.ts Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> --------- Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> --- apps/browser/config/development.json | 1 - .../browser/src/background/main.background.ts | 47 +- .../storage-service.factory.ts | 26 +- .../decorators/dev-flag.decorator.spec.ts | 2 +- apps/browser/src/platform/flags.ts | 1 - ...cal-backed-session-storage.service.spec.ts | 508 +++++------------- .../local-backed-session-storage.service.ts | 225 +++----- libs/common/spec/index.ts | 1 + libs/common/src/platform/misc/lazy.spec.ts | 85 +++ libs/common/src/platform/misc/lazy.ts | 20 + 10 files changed, 380 insertions(+), 536 deletions(-) create mode 100644 libs/common/src/platform/misc/lazy.spec.ts create mode 100644 libs/common/src/platform/misc/lazy.ts diff --git a/apps/browser/config/development.json b/apps/browser/config/development.json index 1b628c173c..aba10eb25b 100644 --- a/apps/browser/config/development.json +++ b/apps/browser/config/development.json @@ -1,6 +1,5 @@ { "devFlags": { - "storeSessionDecrypted": false, "managedEnvironment": { "base": "https://localhost:8080" } diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 0a9ad44962..fa1add0602 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -98,7 +98,9 @@ import { StateFactory } from "@bitwarden/common/platform/factories/state-factory import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging"; // eslint-disable-next-line no-restricted-imports -- Used for dependency creation import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal"; +import { Lazy } from "@bitwarden/common/platform/misc/lazy"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { AppIdService } from "@bitwarden/common/platform/services/app-id.service"; import { ConfigApiService } from "@bitwarden/common/platform/services/config/config-api.service"; import { DefaultConfigService } from "@bitwarden/common/platform/services/config/default-config.service"; @@ -404,32 +406,51 @@ export default class MainBackground { self, ); - const mv3MemoryStorageCreator = (partitionName: string) => { - if (this.popupOnlyContext) { - return new ForegroundMemoryStorageService(partitionName); + // Creates a session key for mv3 storage of large memory items + const sessionKey = new Lazy(async () => { + // Key already in session storage + const sessionStorage = new BrowserMemoryStorageService(); + const existingKey = await sessionStorage.get<SymmetricCryptoKey>("session-key"); + if (existingKey) { + if (sessionStorage.valuesRequireDeserialization) { + return SymmetricCryptoKey.fromJSON(existingKey); + } + return existingKey; + } + + // New key + const { derivedKey } = await this.keyGenerationService.createKeyWithPurpose( + 128, + "ephemeral", + "bitwarden-ephemeral", + ); + await sessionStorage.save("session-key", derivedKey); + return derivedKey; + }); + + const mv3MemoryStorageCreator = () => { + if (this.popupOnlyContext) { + return new ForegroundMemoryStorageService(); } - // TODO: Consider using multithreaded encrypt service in popup only context return new LocalBackedSessionStorageService( - this.logService, + sessionKey, + this.storageService, new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, false), - this.keyGenerationService, - new BrowserLocalStorageService(), - new BrowserMemoryStorageService(), this.platformUtilsService, - partitionName, + this.logService, ); }; this.secureStorageService = this.storageService; // secure storage is not supported in browsers, so we use local storage and warn users when it is used - this.memoryStorageService = BrowserApi.isManifestVersion(3) - ? mv3MemoryStorageCreator("stateService") - : new MemoryStorageService(); this.memoryStorageForStateProviders = BrowserApi.isManifestVersion(3) ? new BrowserMemoryStorageService() // mv3 stores to storage.session : new BackgroundMemoryStorageService(); // mv2 stores to memory + this.memoryStorageService = BrowserApi.isManifestVersion(3) + ? this.memoryStorageForStateProviders // manifest v3 can reuse the same storage. They are split for v2 due to lacking a good sync mechanism, which isn't true for v3 + : new MemoryStorageService(); this.largeObjectMemoryStorageForStateProviders = BrowserApi.isManifestVersion(3) - ? mv3MemoryStorageCreator("stateProviders") // mv3 stores to local-backed session storage + ? mv3MemoryStorageCreator() // mv3 stores to local-backed session storage : this.memoryStorageForStateProviders; // mv2 stores to the same location const storageServiceProvider = new BrowserStorageServiceProvider( diff --git a/apps/browser/src/platform/background/service-factories/storage-service.factory.ts b/apps/browser/src/platform/background/service-factories/storage-service.factory.ts index 83e8a780a6..e63e39944d 100644 --- a/apps/browser/src/platform/background/service-factories/storage-service.factory.ts +++ b/apps/browser/src/platform/background/service-factories/storage-service.factory.ts @@ -3,6 +3,8 @@ import { AbstractStorageService, ObservableStorageService, } from "@bitwarden/common/platform/abstractions/storage.service"; +import { Lazy } from "@bitwarden/common/platform/misc/lazy"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; import { BrowserApi } from "../../browser/browser-api"; @@ -17,10 +19,10 @@ import { KeyGenerationServiceInitOptions, keyGenerationServiceFactory, } from "./key-generation-service.factory"; -import { logServiceFactory, LogServiceInitOptions } from "./log-service.factory"; +import { LogServiceInitOptions, logServiceFactory } from "./log-service.factory"; import { - platformUtilsServiceFactory, PlatformUtilsServiceInitOptions, + platformUtilsServiceFactory, } from "./platform-utils-service.factory"; export type DiskStorageServiceInitOptions = FactoryOptions; @@ -70,13 +72,23 @@ export function memoryStorageServiceFactory( return factory(cache, "memoryStorageService", opts, async () => { if (BrowserApi.isManifestVersion(3)) { return new LocalBackedSessionStorageService( - await logServiceFactory(cache, opts), - await encryptServiceFactory(cache, opts), - await keyGenerationServiceFactory(cache, opts), + new Lazy(async () => { + const existingKey = await ( + await sessionStorageServiceFactory(cache, opts) + ).get<SymmetricCryptoKey>("session-key"); + if (existingKey) { + return existingKey; + } + const { derivedKey } = await ( + await keyGenerationServiceFactory(cache, opts) + ).createKeyWithPurpose(128, "ephemeral", "bitwarden-ephemeral"); + await (await sessionStorageServiceFactory(cache, opts)).save("session-key", derivedKey); + return derivedKey; + }), await diskStorageServiceFactory(cache, opts), - await sessionStorageServiceFactory(cache, opts), + await encryptServiceFactory(cache, opts), await platformUtilsServiceFactory(cache, opts), - "serviceFactories", + await logServiceFactory(cache, opts), ); } return new MemoryStorageService(); diff --git a/apps/browser/src/platform/decorators/dev-flag.decorator.spec.ts b/apps/browser/src/platform/decorators/dev-flag.decorator.spec.ts index c5401f8a09..da00bc6fe3 100644 --- a/apps/browser/src/platform/decorators/dev-flag.decorator.spec.ts +++ b/apps/browser/src/platform/decorators/dev-flag.decorator.spec.ts @@ -9,7 +9,7 @@ jest.mock("../flags", () => ({ })); class TestClass { - @devFlag("storeSessionDecrypted") test() { + @devFlag("managedEnvironment") test() { return "test"; } } diff --git a/apps/browser/src/platform/flags.ts b/apps/browser/src/platform/flags.ts index 36aa698a7b..383e982f06 100644 --- a/apps/browser/src/platform/flags.ts +++ b/apps/browser/src/platform/flags.ts @@ -19,7 +19,6 @@ export type Flags = { // required to avoid linting errors when there are no flags // eslint-disable-next-line @typescript-eslint/ban-types export type DevFlags = { - storeSessionDecrypted?: boolean; managedEnvironment?: GroupPolicyEnvironment; } & SharedDevFlags; diff --git a/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts b/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts index a4581e6ac1..7114bda06e 100644 --- a/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts +++ b/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts @@ -1,412 +1,200 @@ import { mock, MockProxy } from "jest-mock-extended"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; -import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { - AbstractMemoryStorageService, - AbstractStorageService, - StorageUpdate, -} from "@bitwarden/common/platform/abstractions/storage.service"; +import { Lazy } from "@bitwarden/common/platform/misc/lazy"; 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 { BrowserApi } from "../browser/browser-api"; +import { FakeStorageService, makeEncString } from "@bitwarden/common/spec"; import { LocalBackedSessionStorageService } from "./local-backed-session-storage.service"; -describe.skip("LocalBackedSessionStorage", () => { - const sendMessageWithResponseSpy: jest.SpyInstance = jest.spyOn( - BrowserApi, - "sendMessageWithResponse", +describe("LocalBackedSessionStorage", () => { + const sessionKey = new SymmetricCryptoKey( + Utils.fromUtf8ToArray("00000000000000000000000000000000"), ); - + let localStorage: FakeStorageService; let encryptService: MockProxy<EncryptService>; - let keyGenerationService: MockProxy<KeyGenerationService>; - let localStorageService: MockProxy<AbstractStorageService>; - let sessionStorageService: MockProxy<AbstractMemoryStorageService>; - let logService: MockProxy<LogService>; let platformUtilsService: MockProxy<PlatformUtilsService>; - - let cache: Record<string, unknown>; - const testObj = { a: 1, b: 2 }; - const stringifiedTestObj = JSON.stringify(testObj); - - const key = new SymmetricCryptoKey(Utils.fromUtf8ToArray("00000000000000000000000000000000")); - let getSessionKeySpy: jest.SpyInstance; - let sendUpdateSpy: jest.SpyInstance<void, [storageUpdate: StorageUpdate]>; - const mockEnc = (input: string) => Promise.resolve(new EncString("ENCRYPTED" + input)); + let logService: MockProxy<LogService>; let sut: LocalBackedSessionStorageService; - const mockExistingSessionKey = (key: SymmetricCryptoKey) => { - sessionStorageService.get.mockImplementation((storageKey) => { - if (storageKey === "localEncryptionKey_test") { - return Promise.resolve(key?.toJSON()); - } - - return Promise.reject("No implementation for " + storageKey); - }); - }; - beforeEach(() => { - sendMessageWithResponseSpy.mockResolvedValue(null); - logService = mock<LogService>(); + localStorage = new FakeStorageService(); encryptService = mock<EncryptService>(); - keyGenerationService = mock<KeyGenerationService>(); - localStorageService = mock<AbstractStorageService>(); - sessionStorageService = mock<AbstractMemoryStorageService>(); + platformUtilsService = mock<PlatformUtilsService>(); + logService = mock<LogService>(); sut = new LocalBackedSessionStorageService( - logService, + new Lazy(async () => sessionKey), + localStorage, encryptService, - keyGenerationService, - localStorageService, - sessionStorageService, platformUtilsService, - "test", + logService, ); - - cache = sut["cachedSession"]; - - keyGenerationService.createKeyWithPurpose.mockResolvedValue({ - derivedKey: key, - salt: "bitwarden-ephemeral", - material: null, // Not used - }); - - getSessionKeySpy = jest.spyOn(sut, "getSessionEncKey"); - getSessionKeySpy.mockResolvedValue(key); - - // sendUpdateSpy = jest.spyOn(sut, "sendUpdate"); - // sendUpdateSpy.mockReturnValue(); }); describe("get", () => { - describe("in local cache or external context cache", () => { - it("should return from local cache", async () => { - cache["test"] = stringifiedTestObj; - const result = await sut.get("test"); - expect(result).toStrictEqual(testObj); - }); - - it("should return from external context cache when local cache is not available", async () => { - sendMessageWithResponseSpy.mockResolvedValue(stringifiedTestObj); - const result = await sut.get("test"); - expect(result).toStrictEqual(testObj); - }); + it("return the cached value when one is cached", async () => { + sut["cache"]["test"] = "cached"; + const result = await sut.get("test"); + expect(result).toEqual("cached"); }); - describe("not in cache", () => { - const session = { test: stringifiedTestObj }; + it("returns a decrypted value when one is stored in local storage", async () => { + const encrypted = makeEncString("encrypted"); + localStorage.internalStore["session_test"] = encrypted.encryptedString; + encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted")); + const result = await sut.get("test"); + expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(encrypted, sessionKey); + expect(result).toEqual("decrypted"); + }); - beforeEach(() => { - mockExistingSessionKey(key); - }); + it("caches the decrypted value when one is stored in local storage", async () => { + const encrypted = makeEncString("encrypted"); + localStorage.internalStore["session_test"] = encrypted.encryptedString; + encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted")); + await sut.get("test"); + expect(sut["cache"]["test"]).toEqual("decrypted"); + }); + }); - describe("no session retrieved", () => { - let result: any; - let spy: jest.SpyInstance; - beforeEach(async () => { - spy = jest.spyOn(sut, "getLocalSession").mockResolvedValue(null); - localStorageService.get.mockResolvedValue(null); - result = await sut.get("test"); - }); + describe("getBypassCache", () => { + it("ignores cached values", async () => { + sut["cache"]["test"] = "cached"; + const encrypted = makeEncString("encrypted"); + localStorage.internalStore["session_test"] = encrypted.encryptedString; + encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted")); + const result = await sut.getBypassCache("test"); + expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(encrypted, sessionKey); + expect(result).toEqual("decrypted"); + }); - it("should grab from session if not in cache", async () => { - expect(spy).toHaveBeenCalledWith(key); - }); + it("returns a decrypted value when one is stored in local storage", async () => { + const encrypted = makeEncString("encrypted"); + localStorage.internalStore["session_test"] = encrypted.encryptedString; + encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted")); + const result = await sut.getBypassCache("test"); + expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(encrypted, sessionKey); + expect(result).toEqual("decrypted"); + }); - it("should return null if session is null", async () => { - expect(result).toBeNull(); - }); - }); + it("caches the decrypted value when one is stored in local storage", async () => { + const encrypted = makeEncString("encrypted"); + localStorage.internalStore["session_test"] = encrypted.encryptedString; + encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted")); + await sut.getBypassCache("test"); + expect(sut["cache"]["test"]).toEqual("decrypted"); + }); - describe("session retrieved from storage", () => { - beforeEach(() => { - jest.spyOn(sut, "getLocalSession").mockResolvedValue(session); - }); - - it("should return null if session does not have the key", async () => { - const result = await sut.get("DNE"); - expect(result).toBeNull(); - }); - - it("should return the value retrieved from session", async () => { - const result = await sut.get("test"); - expect(result).toEqual(session.test); - }); - - it("should set retrieved values in cache", async () => { - await sut.get("test"); - expect(cache["test"]).toBeTruthy(); - expect(cache["test"]).toEqual(session.test); - }); - - it("should use a deserializer if provided", async () => { - const deserializer = jest.fn().mockReturnValue(testObj); - const result = await sut.get("test", { deserializer: deserializer }); - expect(deserializer).toHaveBeenCalledWith(session.test); - expect(result).toEqual(testObj); - }); - }); + it("deserializes when a deserializer is provided", async () => { + const encrypted = makeEncString("encrypted"); + localStorage.internalStore["session_test"] = encrypted.encryptedString; + encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted")); + const deserializer = jest.fn().mockReturnValue("deserialized"); + const result = await sut.getBypassCache("test", { deserializer }); + expect(deserializer).toHaveBeenCalledWith("decrypted"); + expect(result).toEqual("deserialized"); }); }); describe("has", () => { - it("should be false if `get` returns null", async () => { - const spy = jest.spyOn(sut, "get"); - spy.mockResolvedValue(null); - expect(await sut.has("test")).toBe(false); + it("returns false when the key is not in cache", async () => { + const result = await sut.has("test"); + expect(result).toBe(false); + }); + + it("returns true when the key is in cache", async () => { + sut["cache"]["test"] = "cached"; + const result = await sut.has("test"); + expect(result).toBe(true); + }); + + it("returns true when the key is in local storage", async () => { + localStorage.internalStore["session_test"] = makeEncString("encrypted").encryptedString; + encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted")); + const result = await sut.has("test"); + expect(result).toBe(true); + }); + + it.each([null, undefined])("returns false when %s is cached", async (nullish) => { + sut["cache"]["test"] = nullish; + await expect(sut.has("test")).resolves.toBe(false); + }); + + it.each([null, undefined])( + "returns false when null is stored in local storage", + async (nullish) => { + localStorage.internalStore["session_test"] = nullish; + await expect(sut.has("test")).resolves.toBe(false); + expect(encryptService.decryptToUtf8).not.toHaveBeenCalled(); + }, + ); + }); + + describe("save", () => { + const encString = makeEncString("encrypted"); + beforeEach(() => { + encryptService.encrypt.mockResolvedValue(encString); + }); + + it("logs a warning when saving the same value twice and in a dev environment", async () => { + platformUtilsService.isDev.mockReturnValue(true); + sut["cache"]["test"] = "cached"; + await sut.save("test", "cached"); + expect(logService.warning).toHaveBeenCalled(); + }); + + it("does not log when saving the same value twice and not in a dev environment", async () => { + platformUtilsService.isDev.mockReturnValue(false); + sut["cache"]["test"] = "cached"; + await sut.save("test", "cached"); + expect(logService.warning).not.toHaveBeenCalled(); + }); + + it("removes the key when saving a null value", async () => { + const spy = jest.spyOn(sut, "remove"); + await sut.save("test", null); expect(spy).toHaveBeenCalledWith("test"); }); - it("should be true if `get` returns non-null", async () => { - const spy = jest.spyOn(sut, "get"); - spy.mockResolvedValue({}); - expect(await sut.has("test")).toBe(true); - expect(spy).toHaveBeenCalledWith("test"); + it("saves the value to cache", async () => { + await sut.save("test", "value"); + expect(sut["cache"]["test"]).toEqual("value"); + }); + + it("encrypts and saves the value to local storage", async () => { + await sut.save("test", "value"); + expect(encryptService.encrypt).toHaveBeenCalledWith(JSON.stringify("value"), sessionKey); + expect(localStorage.internalStore["session_test"]).toEqual(encString.encryptedString); + }); + + it("emits an update", async () => { + const spy = jest.spyOn(sut["updatesSubject"], "next"); + await sut.save("test", "value"); + expect(spy).toHaveBeenCalledWith({ key: "test", updateType: "save" }); }); }); describe("remove", () => { - describe("existing cache value is null", () => { - it("should not save null if the local cached value is already null", async () => { - cache["test"] = null; - await sut.remove("test"); - expect(sendUpdateSpy).not.toHaveBeenCalled(); - }); - - it("should not save null if the externally cached value is already null", async () => { - sendMessageWithResponseSpy.mockResolvedValue(null); - await sut.remove("test"); - expect(sendUpdateSpy).not.toHaveBeenCalled(); - }); - }); - - it("should save null", async () => { - cache["test"] = stringifiedTestObj; - + it("nulls the value in cache", async () => { + sut["cache"]["test"] = "cached"; await sut.remove("test"); - expect(sendUpdateSpy).toHaveBeenCalledWith({ key: "test", updateType: "remove" }); - }); - }); - - describe("save", () => { - describe("currently cached", () => { - it("does not save the value a local cached value exists which is an exact match", async () => { - cache["test"] = stringifiedTestObj; - await sut.save("test", testObj); - expect(sendUpdateSpy).not.toHaveBeenCalled(); - }); - - it("does not save the value if a local cached value exists, even if the keys not in the same order", async () => { - cache["test"] = JSON.stringify({ b: 2, a: 1 }); - await sut.save("test", testObj); - expect(sendUpdateSpy).not.toHaveBeenCalled(); - }); - - it("does not save the value a externally cached value exists which is an exact match", async () => { - sendMessageWithResponseSpy.mockResolvedValue(stringifiedTestObj); - await sut.save("test", testObj); - expect(sendUpdateSpy).not.toHaveBeenCalled(); - expect(cache["test"]).toBe(stringifiedTestObj); - }); - - it("saves the value if the currently cached string value evaluates to a falsy value", async () => { - cache["test"] = "null"; - await sut.save("test", testObj); - expect(sendUpdateSpy).toHaveBeenCalledWith({ key: "test", updateType: "save" }); - }); + expect(sut["cache"]["test"]).toBeNull(); }); - describe("caching", () => { - beforeEach(() => { - localStorageService.get.mockResolvedValue(null); - sessionStorageService.get.mockResolvedValue(null); - - localStorageService.save.mockResolvedValue(); - sessionStorageService.save.mockResolvedValue(); - - encryptService.encrypt.mockResolvedValue(mockEnc("{}")); - }); - - it("should remove key from cache if value is null", async () => { - cache["test"] = {}; - // const cacheSetSpy = jest.spyOn(cache, "set"); - expect(cache["test"]).toBe(true); - await sut.save("test", null); - // Don't remove from cache, just replace with null - expect(cache["test"]).toBe(null); - // expect(cacheSetSpy).toHaveBeenCalledWith("test", null); - }); - - it("should set cache if value is non-null", async () => { - expect(cache["test"]).toBe(false); - // const setSpy = jest.spyOn(cache, "set"); - await sut.save("test", testObj); - expect(cache["test"]).toBe(stringifiedTestObj); - // expect(setSpy).toHaveBeenCalledWith("test", stringifiedTestObj); - }); + it("removes the key from local storage", async () => { + localStorage.internalStore["session_test"] = makeEncString("encrypted").encryptedString; + await sut.remove("test"); + expect(localStorage.internalStore["session_test"]).toBeUndefined(); }); - describe("local storing", () => { - let setSpy: jest.SpyInstance; - - beforeEach(() => { - setSpy = jest.spyOn(sut, "setLocalSession").mockResolvedValue(); - }); - - it("should store a new session", async () => { - jest.spyOn(sut, "getLocalSession").mockResolvedValue(null); - await sut.save("test", testObj); - - expect(setSpy).toHaveBeenCalledWith({ test: testObj }, key); - }); - - it("should update an existing session", async () => { - const existingObj = { test: testObj }; - jest.spyOn(sut, "getLocalSession").mockResolvedValue(existingObj); - await sut.save("test2", testObj); - - expect(setSpy).toHaveBeenCalledWith({ test2: testObj, ...existingObj }, key); - }); - - it("should overwrite an existing item in session", async () => { - const existingObj = { test: {} }; - jest.spyOn(sut, "getLocalSession").mockResolvedValue(existingObj); - await sut.save("test", testObj); - - expect(setSpy).toHaveBeenCalledWith({ test: testObj }, key); - }); - }); - }); - - describe("getSessionKey", () => { - beforeEach(() => { - getSessionKeySpy.mockRestore(); - }); - - it("should return the stored symmetric crypto key", async () => { - sessionStorageService.get.mockResolvedValue({ ...key }); - const result = await sut.getSessionEncKey(); - - expect(result).toStrictEqual(key); - }); - - describe("new key creation", () => { - beforeEach(() => { - keyGenerationService.createKeyWithPurpose.mockResolvedValue({ - salt: "salt", - material: null, - derivedKey: key, - }); - jest.spyOn(sut, "setSessionEncKey").mockResolvedValue(); - }); - - it("should create a symmetric crypto key", async () => { - const result = await sut.getSessionEncKey(); - - expect(result).toStrictEqual(key); - expect(keyGenerationService.createKeyWithPurpose).toHaveBeenCalledTimes(1); - }); - - it("should store a symmetric crypto key if it makes one", async () => { - const spy = jest.spyOn(sut, "setSessionEncKey").mockResolvedValue(); - await sut.getSessionEncKey(); - - expect(spy).toHaveBeenCalledWith(key); - }); - }); - }); - - describe("getLocalSession", () => { - it("should return null if session is null", async () => { - const result = await sut.getLocalSession(key); - - expect(result).toBeNull(); - expect(localStorageService.get).toHaveBeenCalledWith("session_test"); - }); - - describe("non-null sessions", () => { - const session = { test: "test" }; - const encSession = new EncString(JSON.stringify(session)); - const decryptedSession = JSON.stringify(session); - - beforeEach(() => { - localStorageService.get.mockResolvedValue(encSession.encryptedString); - }); - - it("should decrypt returned sessions", async () => { - encryptService.decryptToUtf8 - .calledWith(expect.anything(), key) - .mockResolvedValue(decryptedSession); - await sut.getLocalSession(key); - expect(encryptService.decryptToUtf8).toHaveBeenNthCalledWith(1, encSession, key); - }); - - it("should parse session", async () => { - encryptService.decryptToUtf8 - .calledWith(expect.anything(), key) - .mockResolvedValue(decryptedSession); - const result = await sut.getLocalSession(key); - expect(result).toEqual(session); - }); - - it("should remove state if decryption fails", async () => { - encryptService.decryptToUtf8.mockResolvedValue(null); - const setSessionEncKeySpy = jest.spyOn(sut, "setSessionEncKey").mockResolvedValue(); - - const result = await sut.getLocalSession(key); - - expect(result).toBeNull(); - expect(setSessionEncKeySpy).toHaveBeenCalledWith(null); - expect(localStorageService.remove).toHaveBeenCalledWith("session_test"); - }); - }); - }); - - describe("setLocalSession", () => { - const testSession = { test: "a" }; - const testJSON = JSON.stringify(testSession); - - it("should encrypt a stringified session", async () => { - encryptService.encrypt.mockImplementation(mockEnc); - localStorageService.save.mockResolvedValue(); - await sut.setLocalSession(testSession, key); - - expect(encryptService.encrypt).toHaveBeenNthCalledWith(1, testJSON, key); - }); - - it("should remove local session if null", async () => { - encryptService.encrypt.mockResolvedValue(null); - await sut.setLocalSession(null, key); - - expect(localStorageService.remove).toHaveBeenCalledWith("session_test"); - }); - - it("should save encrypted string", async () => { - encryptService.encrypt.mockImplementation(mockEnc); - await sut.setLocalSession(testSession, key); - - expect(localStorageService.save).toHaveBeenCalledWith( - "session_test", - (await mockEnc(testJSON)).encryptedString, - ); - }); - }); - - describe("setSessionKey", () => { - it("should remove if null", async () => { - await sut.setSessionEncKey(null); - expect(sessionStorageService.remove).toHaveBeenCalledWith("localEncryptionKey_test"); - }); - - it("should save key when not null", async () => { - await sut.setSessionEncKey(key); - expect(sessionStorageService.save).toHaveBeenCalledWith("localEncryptionKey_test", key); + it("emits an update", async () => { + const spy = jest.spyOn(sut["updatesSubject"], "next"); + await sut.remove("test"); + expect(spy).toHaveBeenCalledWith({ key: "test", updateType: "remove" }); }); }); }); diff --git a/apps/browser/src/platform/services/local-backed-session-storage.service.ts b/apps/browser/src/platform/services/local-backed-session-storage.service.ts index 146eb11b2b..5432e8d918 100644 --- a/apps/browser/src/platform/services/local-backed-session-storage.service.ts +++ b/apps/browser/src/platform/services/local-backed-session-storage.service.ts @@ -2,7 +2,6 @@ import { Subject } from "rxjs"; import { Jsonify } from "type-fest"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; -import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { @@ -11,13 +10,12 @@ import { ObservableStorageService, StorageUpdate, } from "@bitwarden/common/platform/abstractions/storage.service"; +import { Lazy } from "@bitwarden/common/platform/misc/lazy"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { MemoryStorageOptions } from "@bitwarden/common/platform/models/domain/storage-options"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { BrowserApi } from "../browser/browser-api"; -import { devFlag } from "../decorators/dev-flag.decorator"; -import { devFlagEnabled } from "../flags"; import { MemoryStoragePortMessage } from "../storage/port-messages"; import { portName } from "../storage/port-name"; @@ -25,85 +23,64 @@ export class LocalBackedSessionStorageService extends AbstractMemoryStorageService implements ObservableStorageService { + private ports: Set<chrome.runtime.Port> = new Set([]); + private cache: Record<string, unknown> = {}; private updatesSubject = new Subject<StorageUpdate>(); - private commandName = `localBackedSessionStorage_${this.partitionName}`; - private encKey = `localEncryptionKey_${this.partitionName}`; - private sessionKey = `session_${this.partitionName}`; - private cachedSession: Record<string, unknown> = {}; - private _ports: Set<chrome.runtime.Port> = new Set([]); - private knownNullishCacheKeys: Set<string> = new Set([]); + readonly valuesRequireDeserialization = true; + updates$ = this.updatesSubject.asObservable(); constructor( - private logService: LogService, - private encryptService: EncryptService, - private keyGenerationService: KeyGenerationService, - private localStorage: AbstractStorageService, - private sessionStorage: AbstractStorageService, - private platformUtilsService: PlatformUtilsService, - private partitionName: string, + private readonly sessionKey: Lazy<Promise<SymmetricCryptoKey>>, + private readonly localStorage: AbstractStorageService, + private readonly encryptService: EncryptService, + private readonly platformUtilsService: PlatformUtilsService, + private readonly logService: LogService, ) { super(); BrowserApi.addListener(chrome.runtime.onConnect, (port) => { - if (port.name !== `${portName(chrome.storage.session)}_${partitionName}`) { + if (port.name !== portName(chrome.storage.session)) { return; } - this._ports.add(port); + this.ports.add(port); const listenerCallback = this.onMessageFromForeground.bind(this); port.onDisconnect.addListener(() => { - this._ports.delete(port); + this.ports.delete(port); port.onMessage.removeListener(listenerCallback); }); port.onMessage.addListener(listenerCallback); // Initialize the new memory storage service with existing data this.sendMessageTo(port, { action: "initialization", - data: Array.from(Object.keys(this.cachedSession)), + data: Array.from(Object.keys(this.cache)), + }); + this.updates$.subscribe((update) => { + this.broadcastMessage({ + action: "subject_update", + data: update, + }); }); }); - this.updates$.subscribe((update) => { - this.broadcastMessage({ - action: "subject_update", - data: update, - }); - }); - } - - get valuesRequireDeserialization(): boolean { - return true; - } - - get updates$() { - return this.updatesSubject.asObservable(); } async get<T>(key: string, options?: MemoryStorageOptions<T>): Promise<T> { - if (this.cachedSession[key] != null) { - return this.cachedSession[key] as T; - } - - if (this.knownNullishCacheKeys.has(key)) { - return null; + if (this.cache[key] !== undefined) { + return this.cache[key] as T; } return await this.getBypassCache(key, options); } async getBypassCache<T>(key: string, options?: MemoryStorageOptions<T>): Promise<T> { - const session = await this.getLocalSession(await this.getSessionEncKey()); - if (session[key] == null) { - this.knownNullishCacheKeys.add(key); - return null; - } + let value = await this.getLocalSessionValue(await this.sessionKey.get(), key); - let value = session[key]; if (options?.deserializer != null) { value = options.deserializer(value as Jsonify<T>); } - void this.save(key, value); + this.cache[key] = value; return value as T; } @@ -114,7 +91,7 @@ export class LocalBackedSessionStorageService async save<T>(key: string, obj: T): Promise<void> { // This is for observation purposes only. At some point, we don't want to write to local session storage if the value is the same. if (this.platformUtilsService.isDev()) { - const existingValue = this.cachedSession[key] as T; + const existingValue = this.cache[key] as T; if (this.compareValues<T>(existingValue, obj)) { this.logService.warning(`Possible unnecessary write to local session storage. Key: ${key}`); this.logService.warning(obj as any); @@ -125,128 +102,42 @@ export class LocalBackedSessionStorageService return await this.remove(key); } - this.knownNullishCacheKeys.delete(key); - this.cachedSession[key] = obj; + this.cache[key] = obj; await this.updateLocalSessionValue(key, obj); this.updatesSubject.next({ key, updateType: "save" }); } async remove(key: string): Promise<void> { - this.knownNullishCacheKeys.add(key); - delete this.cachedSession[key]; + this.cache[key] = null; await this.updateLocalSessionValue(key, null); this.updatesSubject.next({ key, updateType: "remove" }); } - private async updateLocalSessionValue<T>(key: string, obj: T) { - const sessionEncKey = await this.getSessionEncKey(); - const localSession = (await this.getLocalSession(sessionEncKey)) ?? {}; - localSession[key] = obj; - void this.setLocalSession(localSession, sessionEncKey); - } - - async getLocalSession(encKey: SymmetricCryptoKey): Promise<Record<string, unknown>> { - if (Object.keys(this.cachedSession).length > 0) { - return this.cachedSession; - } - - this.cachedSession = {}; - const local = await this.localStorage.get<string>(this.sessionKey); + private async getLocalSessionValue(encKey: SymmetricCryptoKey, key: string): Promise<unknown> { + const local = await this.localStorage.get<string>(this.sessionStorageKey(key)); if (local == null) { - return this.cachedSession; + return null; } - if (devFlagEnabled("storeSessionDecrypted")) { - return local as any as Record<string, unknown>; + const valueJson = await this.encryptService.decryptToUtf8(new EncString(local), encKey); + if (valueJson == null) { + // error with decryption, value is lost, delete state and start over + await this.localStorage.remove(this.sessionStorageKey(key)); + return null; } - const sessionJson = await this.encryptService.decryptToUtf8(new EncString(local), encKey); - if (sessionJson == null) { - // Error with decryption -- session is lost, delete state and key and start over - await this.setSessionEncKey(null); - await this.localStorage.remove(this.sessionKey); - return this.cachedSession; - } - - this.cachedSession = JSON.parse(sessionJson); - return this.cachedSession; + return JSON.parse(valueJson); } - async setLocalSession(session: Record<string, unknown>, key: SymmetricCryptoKey) { - if (devFlagEnabled("storeSessionDecrypted")) { - await this.setDecryptedLocalSession(session); - } else { - await this.setEncryptedLocalSession(session, key); - } - } - - @devFlag("storeSessionDecrypted") - async setDecryptedLocalSession(session: Record<string, unknown>): Promise<void> { - // Make sure we're storing the jsonified version of the session - const jsonSession = JSON.parse(JSON.stringify(session)); - if (session == null) { - await this.localStorage.remove(this.sessionKey); - } else { - await this.localStorage.save(this.sessionKey, jsonSession); - } - } - - async setEncryptedLocalSession(session: Record<string, unknown>, key: SymmetricCryptoKey) { - const jsonSession = JSON.stringify(session); - const encSession = await this.encryptService.encrypt(jsonSession, key); - - if (encSession == null) { - return await this.localStorage.remove(this.sessionKey); - } - await this.localStorage.save(this.sessionKey, encSession.encryptedString); - } - - async getSessionEncKey(): Promise<SymmetricCryptoKey> { - let storedKey = await this.sessionStorage.get<SymmetricCryptoKey>(this.encKey); - if (storedKey == null || Object.keys(storedKey).length == 0) { - const generatedKey = await this.keyGenerationService.createKeyWithPurpose( - 128, - "ephemeral", - "bitwarden-ephemeral", - ); - storedKey = generatedKey.derivedKey; - await this.setSessionEncKey(storedKey); - return storedKey; - } else { - return SymmetricCryptoKey.fromJSON(storedKey); - } - } - - async setSessionEncKey(input: SymmetricCryptoKey): Promise<void> { - if (input == null) { - await this.sessionStorage.remove(this.encKey); - } else { - await this.sessionStorage.save(this.encKey, input); - } - } - - private compareValues<T>(value1: T, value2: T): boolean { - if (value1 == null && value2 == null) { - return true; + private async updateLocalSessionValue(key: string, value: unknown): Promise<void> { + if (value == null) { + await this.localStorage.remove(this.sessionStorageKey(key)); + return; } - if (value1 && value2 == null) { - return false; - } - - if (value1 == null && value2) { - return false; - } - - if (typeof value1 !== "object" || typeof value2 !== "object") { - return value1 === value2; - } - - if (JSON.stringify(value1) === JSON.stringify(value2)) { - return true; - } - - return Object.entries(value1).sort().toString() === Object.entries(value2).sort().toString(); + const valueJson = JSON.stringify(value); + const encValue = await this.encryptService.encrypt(valueJson, await this.sessionKey.get()); + await this.localStorage.save(this.sessionStorageKey(key), encValue.encryptedString); } private async onMessageFromForeground( @@ -282,7 +173,7 @@ export class LocalBackedSessionStorageService } protected broadcastMessage(data: Omit<MemoryStoragePortMessage, "originator">) { - this._ports.forEach((port) => { + this.ports.forEach((port) => { this.sendMessageTo(port, data); }); } @@ -296,4 +187,32 @@ export class LocalBackedSessionStorageService originator: "background", }); } + + private sessionStorageKey(key: string) { + return `session_${key}`; + } + + private compareValues<T>(value1: T, value2: T): boolean { + if (value1 == null && value2 == null) { + return true; + } + + if (value1 && value2 == null) { + return false; + } + + if (value1 == null && value2) { + return false; + } + + if (typeof value1 !== "object" || typeof value2 !== "object") { + return value1 === value2; + } + + if (JSON.stringify(value1) === JSON.stringify(value2)) { + return true; + } + + return Object.entries(value1).sort().toString() === Object.entries(value2).sort().toString(); + } } diff --git a/libs/common/spec/index.ts b/libs/common/spec/index.ts index 72bd28aca4..6e9af8400e 100644 --- a/libs/common/spec/index.ts +++ b/libs/common/spec/index.ts @@ -4,3 +4,4 @@ export * from "./matchers"; export * from "./fake-state-provider"; export * from "./fake-state"; export * from "./fake-account-service"; +export * from "./fake-storage.service"; diff --git a/libs/common/src/platform/misc/lazy.spec.ts b/libs/common/src/platform/misc/lazy.spec.ts new file mode 100644 index 0000000000..76ee085d3d --- /dev/null +++ b/libs/common/src/platform/misc/lazy.spec.ts @@ -0,0 +1,85 @@ +import { Lazy } from "./lazy"; + +describe("Lazy", () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + describe("async", () => { + let factory: jest.Mock<Promise<number>>; + let lazy: Lazy<Promise<number>>; + + beforeEach(() => { + factory = jest.fn(); + lazy = new Lazy(factory); + }); + + describe("get", () => { + it("should call the factory once", async () => { + await lazy.get(); + await lazy.get(); + + expect(factory).toHaveBeenCalledTimes(1); + }); + + it("should return the value from the factory", async () => { + factory.mockResolvedValue(42); + + const value = await lazy.get(); + + expect(value).toBe(42); + }); + }); + + describe("factory throws", () => { + it("should throw the error", async () => { + factory.mockRejectedValue(new Error("factory error")); + + await expect(lazy.get()).rejects.toThrow("factory error"); + }); + }); + + describe("factory returns undefined", () => { + it("should return undefined", async () => { + factory.mockResolvedValue(undefined); + + const value = await lazy.get(); + + expect(value).toBeUndefined(); + }); + }); + + describe("factory returns null", () => { + it("should return null", async () => { + factory.mockResolvedValue(null); + + const value = await lazy.get(); + + expect(value).toBeNull(); + }); + }); + }); + + describe("sync", () => { + const syncFactory = jest.fn(); + let lazy: Lazy<number>; + + beforeEach(() => { + syncFactory.mockReturnValue(42); + lazy = new Lazy<number>(syncFactory); + }); + + it("should return the value from the factory", () => { + const value = lazy.get(); + + expect(value).toBe(42); + }); + + it("should call the factory once", () => { + lazy.get(); + lazy.get(); + + expect(syncFactory).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/libs/common/src/platform/misc/lazy.ts b/libs/common/src/platform/misc/lazy.ts new file mode 100644 index 0000000000..fb85b93678 --- /dev/null +++ b/libs/common/src/platform/misc/lazy.ts @@ -0,0 +1,20 @@ +export class Lazy<T> { + private _value: T | undefined = undefined; + private _isCreated = false; + + constructor(private readonly factory: () => T) {} + + /** + * Resolves the factory and returns the result. Guaranteed to resolve the value only once. + * + * @returns The value produced by your factory. + */ + get(): T { + if (!this._isCreated) { + this._value = this.factory(); + this._isCreated = true; + } + + return this._value as T; + } +} From 100b43dd8f7ac23cb888b0f031353aa68beffb82 Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Mon, 22 Apr 2024 12:06:43 -0400 Subject: [PATCH 247/351] =?UTF-8?q?Revert=20"Auth/PM-6689=20-=20Migrate=20?= =?UTF-8?q?Security=20Stamp=20to=20Token=20Service=20and=20State=20Prov?= =?UTF-8?q?=E2=80=A6"=20(#8860)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 91f1d9fb86142805d0774182c3ee6234e13946e3. --- .../browser/src/background/main.background.ts | 1 - apps/cli/src/bw.ts | 1 - .../src/services/jslib-services.module.ts | 1 - .../login-strategies/login.strategy.spec.ts | 4 + .../common/login-strategies/login.strategy.ts | 9 ++- .../src/auth/abstractions/token.service.ts | 6 -- .../src/auth/services/token.service.spec.ts | 79 ------------------- .../common/src/auth/services/token.service.ts | 25 ------ .../src/auth/services/token.state.spec.ts | 2 - libs/common/src/auth/services/token.state.ts | 5 -- .../platform/abstractions/state.service.ts | 2 + .../models/domain/account-tokens.spec.ts | 9 +++ .../platform/models/domain/account.spec.ts | 4 +- .../src/platform/models/domain/account.ts | 18 +++++ .../src/platform/services/state.service.ts | 17 ++++ .../src/vault/services/sync/sync.service.ts | 6 +- 16 files changed, 63 insertions(+), 126 deletions(-) create mode 100644 libs/common/src/platform/models/domain/account-tokens.spec.ts diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index fa1add0602..15f21b2501 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -813,7 +813,6 @@ export default class MainBackground { this.avatarService, logoutCallback, this.billingAccountProfileStateService, - this.tokenService, ); this.eventUploadService = new EventUploadService( this.apiService, diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index 437f807bc6..58329128b8 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -631,7 +631,6 @@ export class Main { this.avatarService, async (expired: boolean) => await this.logout(), this.billingAccountProfileStateService, - this.tokenService, ); this.totpService = new TotpService(this.cryptoFunctionService, this.logService); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 27b182de5d..f31bcb1c51 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -628,7 +628,6 @@ const safeProviders: SafeProvider[] = [ AvatarServiceAbstraction, LOGOUT_CALLBACK, BillingAccountProfileStateService, - TokenServiceAbstraction, ], }), safeProvider({ 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 e0833342ce..431f736e94 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.spec.ts @@ -27,6 +27,7 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Account, AccountProfile, + AccountTokens, AccountKeys, } from "@bitwarden/common/platform/models/domain/account"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; @@ -212,6 +213,9 @@ describe("LoginStrategy", () => { kdfType: kdf, }, }, + tokens: { + ...new AccountTokens(), + }, keys: new AccountKeys(), }), ); diff --git a/libs/auth/src/common/login-strategies/login.strategy.ts b/libs/auth/src/common/login-strategies/login.strategy.ts index a73c32e120..a6dc193183 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.ts @@ -27,7 +27,11 @@ 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 { Account, AccountProfile } from "@bitwarden/common/platform/models/domain/account"; +import { + 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"; @@ -188,6 +192,9 @@ export abstract class LoginStrategy { kdfType: tokenResponse.kdf, }, }, + tokens: { + ...new AccountTokens(), + }, }), ); diff --git a/libs/common/src/auth/abstractions/token.service.ts b/libs/common/src/auth/abstractions/token.service.ts index fc3bd317f4..75bb383882 100644 --- a/libs/common/src/auth/abstractions/token.service.ts +++ b/libs/common/src/auth/abstractions/token.service.ts @@ -213,10 +213,4 @@ export abstract class TokenService { * @returns A promise that resolves with a boolean representing the user's external authN status. */ getIsExternal: () => Promise<boolean>; - - /** Gets the active or passed in user's security stamp */ - getSecurityStamp: (userId?: UserId) => Promise<string | null>; - - /** Sets the security stamp for the active or passed in user */ - setSecurityStamp: (securityStamp: string, userId?: UserId) => Promise<void>; } diff --git a/libs/common/src/auth/services/token.service.spec.ts b/libs/common/src/auth/services/token.service.spec.ts index 3e92053d2f..d32c4d8e1c 100644 --- a/libs/common/src/auth/services/token.service.spec.ts +++ b/libs/common/src/auth/services/token.service.spec.ts @@ -23,7 +23,6 @@ import { EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL, REFRESH_TOKEN_DISK, REFRESH_TOKEN_MEMORY, - SECURITY_STAMP_MEMORY, } from "./token.state"; describe("TokenService", () => { @@ -2192,84 +2191,6 @@ describe("TokenService", () => { }); }); - describe("Security Stamp methods", () => { - const mockSecurityStamp = "securityStamp"; - - describe("setSecurityStamp", () => { - it("should throw an error if no user id is provided and there is no active user in global state", async () => { - // Act - // note: don't await here because we want to test the error - const result = tokenService.setSecurityStamp(mockSecurityStamp); - // Assert - await expect(result).rejects.toThrow("User id not found. Cannot set security stamp."); - }); - - it("should set the security stamp in memory when there is an active user in global state", async () => { - // Arrange - globalStateProvider - .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) - .stateSubject.next(userIdFromAccessToken); - - // Act - await tokenService.setSecurityStamp(mockSecurityStamp); - - // Assert - expect( - singleUserStateProvider.getFake(userIdFromAccessToken, SECURITY_STAMP_MEMORY).nextMock, - ).toHaveBeenCalledWith(mockSecurityStamp); - }); - - it("should set the security stamp in memory for the specified user id", async () => { - // Act - await tokenService.setSecurityStamp(mockSecurityStamp, userIdFromAccessToken); - - // Assert - expect( - singleUserStateProvider.getFake(userIdFromAccessToken, SECURITY_STAMP_MEMORY).nextMock, - ).toHaveBeenCalledWith(mockSecurityStamp); - }); - }); - - describe("getSecurityStamp", () => { - it("should throw an error if no user id is provided and there is no active user in global state", async () => { - // Act - // note: don't await here because we want to test the error - const result = tokenService.getSecurityStamp(); - // Assert - await expect(result).rejects.toThrow("User id not found. Cannot get security stamp."); - }); - - it("should return the security stamp from memory with no user id specified (uses global active user)", async () => { - // Arrange - globalStateProvider - .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) - .stateSubject.next(userIdFromAccessToken); - - singleUserStateProvider - .getFake(userIdFromAccessToken, SECURITY_STAMP_MEMORY) - .stateSubject.next([userIdFromAccessToken, mockSecurityStamp]); - - // Act - const result = await tokenService.getSecurityStamp(); - - // Assert - expect(result).toEqual(mockSecurityStamp); - }); - - it("should return the security stamp from memory for the specified user id", async () => { - // Arrange - singleUserStateProvider - .getFake(userIdFromAccessToken, SECURITY_STAMP_MEMORY) - .stateSubject.next([userIdFromAccessToken, mockSecurityStamp]); - - // Act - const result = await tokenService.getSecurityStamp(userIdFromAccessToken); - // Assert - expect(result).toEqual(mockSecurityStamp); - }); - }); - }); - // Helpers function createTokenService(supportsSecureStorage: boolean) { return new TokenService( diff --git a/libs/common/src/auth/services/token.service.ts b/libs/common/src/auth/services/token.service.ts index 40036a8453..c24a2c186b 100644 --- a/libs/common/src/auth/services/token.service.ts +++ b/libs/common/src/auth/services/token.service.ts @@ -32,7 +32,6 @@ import { EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL, REFRESH_TOKEN_DISK, REFRESH_TOKEN_MEMORY, - SECURITY_STAMP_MEMORY, } from "./token.state"; export enum TokenStorageLocation { @@ -851,30 +850,6 @@ export class TokenService implements TokenServiceAbstraction { return Array.isArray(decoded.amr) && decoded.amr.includes("external"); } - async getSecurityStamp(userId?: UserId): Promise<string | null> { - userId ??= await firstValueFrom(this.activeUserIdGlobalState.state$); - - if (!userId) { - throw new Error("User id not found. Cannot get security stamp."); - } - - const securityStamp = await this.getStateValueByUserIdAndKeyDef(userId, SECURITY_STAMP_MEMORY); - - return securityStamp; - } - - async setSecurityStamp(securityStamp: string, userId?: UserId): Promise<void> { - userId ??= await firstValueFrom(this.activeUserIdGlobalState.state$); - - if (!userId) { - throw new Error("User id not found. Cannot set security stamp."); - } - - await this.singleUserStateProvider - .get(userId, SECURITY_STAMP_MEMORY) - .update((_) => securityStamp); - } - private async getStateValueByUserIdAndKeyDef( userId: UserId, storageLocation: UserKeyDefinition<string>, diff --git a/libs/common/src/auth/services/token.state.spec.ts b/libs/common/src/auth/services/token.state.spec.ts index bb82410fac..dc00fec383 100644 --- a/libs/common/src/auth/services/token.state.spec.ts +++ b/libs/common/src/auth/services/token.state.spec.ts @@ -10,7 +10,6 @@ import { EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL, REFRESH_TOKEN_DISK, REFRESH_TOKEN_MEMORY, - SECURITY_STAMP_MEMORY, } from "./token.state"; describe.each([ @@ -23,7 +22,6 @@ describe.each([ [API_KEY_CLIENT_ID_MEMORY, "apiKeyClientIdMemory"], [API_KEY_CLIENT_SECRET_DISK, "apiKeyClientSecretDisk"], [API_KEY_CLIENT_SECRET_MEMORY, "apiKeyClientSecretMemory"], - [SECURITY_STAMP_MEMORY, "securityStamp"], ])( "deserializes state key definitions", ( diff --git a/libs/common/src/auth/services/token.state.ts b/libs/common/src/auth/services/token.state.ts index 57d85f2a55..458d6846c1 100644 --- a/libs/common/src/auth/services/token.state.ts +++ b/libs/common/src/auth/services/token.state.ts @@ -69,8 +69,3 @@ export const API_KEY_CLIENT_SECRET_MEMORY = new UserKeyDefinition<string>( clearOn: [], // Manually handled }, ); - -export const SECURITY_STAMP_MEMORY = new UserKeyDefinition<string>(TOKEN_MEMORY, "securityStamp", { - deserializer: (securityStamp) => securityStamp, - clearOn: ["logout"], -}); diff --git a/libs/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index f1d4b3848e..051604f0ae 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -181,6 +181,8 @@ export abstract class StateService<T extends Account = Account> { * Sets the user's Pin, encrypted by the user key */ setProtectedPin: (value: string, options?: StorageOptions) => Promise<void>; + getSecurityStamp: (options?: StorageOptions) => Promise<string>; + setSecurityStamp: (value: string, options?: StorageOptions) => Promise<void>; getUserId: (options?: StorageOptions) => Promise<string>; getVaultTimeout: (options?: StorageOptions) => Promise<number>; setVaultTimeout: (value: number, options?: StorageOptions) => Promise<void>; diff --git a/libs/common/src/platform/models/domain/account-tokens.spec.ts b/libs/common/src/platform/models/domain/account-tokens.spec.ts new file mode 100644 index 0000000000..733b3908e9 --- /dev/null +++ b/libs/common/src/platform/models/domain/account-tokens.spec.ts @@ -0,0 +1,9 @@ +import { AccountTokens } from "./account"; + +describe("AccountTokens", () => { + describe("fromJSON", () => { + it("should deserialize to an instance of itself", () => { + expect(AccountTokens.fromJSON({})).toBeInstanceOf(AccountTokens); + }); + }); +}); diff --git a/libs/common/src/platform/models/domain/account.spec.ts b/libs/common/src/platform/models/domain/account.spec.ts index 77c242b6ff..0c76c16cc2 100644 --- a/libs/common/src/platform/models/domain/account.spec.ts +++ b/libs/common/src/platform/models/domain/account.spec.ts @@ -1,4 +1,4 @@ -import { Account, AccountKeys, AccountProfile, AccountSettings } from "./account"; +import { Account, AccountKeys, AccountProfile, AccountSettings, AccountTokens } from "./account"; describe("Account", () => { describe("fromJSON", () => { @@ -10,12 +10,14 @@ describe("Account", () => { const keysSpy = jest.spyOn(AccountKeys, "fromJSON"); const profileSpy = jest.spyOn(AccountProfile, "fromJSON"); const settingsSpy = jest.spyOn(AccountSettings, "fromJSON"); + const tokensSpy = jest.spyOn(AccountTokens, "fromJSON"); Account.fromJSON({}); expect(keysSpy).toHaveBeenCalled(); expect(profileSpy).toHaveBeenCalled(); expect(settingsSpy).toHaveBeenCalled(); + expect(tokensSpy).toHaveBeenCalled(); }); }); }); diff --git a/libs/common/src/platform/models/domain/account.ts b/libs/common/src/platform/models/domain/account.ts index cd416ec1f9..5a9a764696 100644 --- a/libs/common/src/platform/models/domain/account.ts +++ b/libs/common/src/platform/models/domain/account.ts @@ -171,11 +171,24 @@ export class AccountSettings { } } +export class AccountTokens { + securityStamp?: string; + + static fromJSON(obj: Jsonify<AccountTokens>): AccountTokens { + if (obj == null) { + return null; + } + + return Object.assign(new AccountTokens(), obj); + } +} + export class Account { data?: AccountData = new AccountData(); keys?: AccountKeys = new AccountKeys(); profile?: AccountProfile = new AccountProfile(); settings?: AccountSettings = new AccountSettings(); + tokens?: AccountTokens = new AccountTokens(); constructor(init: Partial<Account>) { Object.assign(this, { @@ -195,6 +208,10 @@ export class Account { ...new AccountSettings(), ...init?.settings, }, + tokens: { + ...new AccountTokens(), + ...init?.tokens, + }, }); } @@ -208,6 +225,7 @@ export class Account { data: AccountData.fromJSON(json?.data), profile: AccountProfile.fromJSON(json?.profile), settings: AccountSettings.fromJSON(json?.settings), + tokens: AccountTokens.fromJSON(json?.tokens), }); } } diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index d0a55d7a47..f660cd7a34 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -839,6 +839,23 @@ export class StateService< ); } + async getSecurityStamp(options?: StorageOptions): Promise<string> { + return ( + await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) + )?.tokens?.securityStamp; + } + + async setSecurityStamp(value: string, options?: StorageOptions): Promise<void> { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultInMemoryOptions()), + ); + account.tokens.securityStamp = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultInMemoryOptions()), + ); + } + async getUserId(options?: StorageOptions): Promise<string> { return ( await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) diff --git a/libs/common/src/vault/services/sync/sync.service.ts b/libs/common/src/vault/services/sync/sync.service.ts index 73869ff488..ff8e9f1f4f 100644 --- a/libs/common/src/vault/services/sync/sync.service.ts +++ b/libs/common/src/vault/services/sync/sync.service.ts @@ -15,7 +15,6 @@ 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 { TokenService } from "../../../auth/abstractions/token.service"; 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"; @@ -74,7 +73,6 @@ export class SyncService implements SyncServiceAbstraction { private avatarService: AvatarService, private logoutCallback: (expired: boolean) => Promise<void>, private billingAccountProfileStateService: BillingAccountProfileStateService, - private tokenService: TokenService, ) {} async getLastSync(): Promise<Date> { @@ -311,7 +309,7 @@ export class SyncService implements SyncServiceAbstraction { } private async syncProfile(response: ProfileResponse) { - const stamp = await this.tokenService.getSecurityStamp(response.id as UserId); + const stamp = await this.stateService.getSecurityStamp(); if (stamp != null && stamp !== response.securityStamp) { if (this.logoutCallback != null) { await this.logoutCallback(true); @@ -325,7 +323,7 @@ export class SyncService implements SyncServiceAbstraction { await this.cryptoService.setProviderKeys(response.providers); await this.cryptoService.setOrgKeys(response.organizations, response.providerOrganizations); await this.avatarService.setSyncAvatarColor(response.id as UserId, response.avatarColor); - await this.tokenService.setSecurityStamp(response.securityStamp, response.id as UserId); + await this.stateService.setSecurityStamp(response.securityStamp); await this.stateService.setEmailVerified(response.emailVerified); await this.billingAccountProfileStateService.setHasPremium( From b395cb40a7a0e35d836009028f4f32242858d0f7 Mon Sep 17 00:00:00 2001 From: Shane Melton <smelton@bitwarden.com> Date: Mon, 22 Apr 2024 09:32:44 -0700 Subject: [PATCH 248/351] [AC-1999] Fix deleting collections from collection dialog (#8647) * [AC-1999] Fix null check this.collection can be both null or unassigned and `!= null` will handle both cases. * [AC-1999] Navigate away when selected collection is deleted --------- Co-authored-by: bnagawiecki <107435978+bnagawiecki@users.noreply.github.com> --- .../vault/individual-vault/vault.component.ts | 12 +++++++++--- .../vault-header/vault-header.component.ts | 2 +- .../src/app/vault/org-vault/vault.component.ts | 18 ++++++++++++++---- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index a25ba6edbc..2c20328336 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -679,6 +679,14 @@ export class VaultComponent implements OnInit, OnDestroy { } else if (result.action === CollectionDialogAction.Deleted) { await this.collectionService.delete(result.collection?.id); this.refresh(); + // Navigate away if we deleted the collection we were viewing + if (this.selectedCollection?.node.id === c?.id) { + void this.router.navigate([], { + queryParams: { collectionId: this.selectedCollection.parent?.node.id ?? null }, + queryParamsHandling: "merge", + replaceUrl: true, + }); + } } } @@ -710,9 +718,7 @@ export class VaultComponent implements OnInit, OnDestroy { ); // Navigate away if we deleted the collection we were viewing if (this.selectedCollection?.node.id === collection.id) { - // 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([], { + void this.router.navigate([], { queryParams: { collectionId: this.selectedCollection.parent?.node.id ?? null }, queryParamsHandling: "merge", replaceUrl: true, diff --git a/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts b/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts index c4c67759c7..a5cd468008 100644 --- a/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts +++ b/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts @@ -80,7 +80,7 @@ export class VaultHeaderComponent implements OnInit { ? this.i18nService.t("collections").toLowerCase() : this.i18nService.t("vault").toLowerCase(); - if (this.collection !== undefined) { + if (this.collection != null) { return this.collection.node.name; } diff --git a/apps/web/src/app/vault/org-vault/vault.component.ts b/apps/web/src/app/vault/org-vault/vault.component.ts index 587758dda1..9de404e969 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.ts +++ b/apps/web/src/app/vault/org-vault/vault.component.ts @@ -958,11 +958,9 @@ export class VaultComponent implements OnInit, OnDestroy { this.i18nService.t("deletedCollectionId", collection.name), ); - // Navigate away if we deleted the colletion we were viewing + // Navigate away if we deleted the collection we were viewing if (this.selectedCollection?.node.id === collection.id) { - // 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([], { + void this.router.navigate([], { queryParams: { collectionId: this.selectedCollection.parent?.node.id ?? null }, queryParamsHandling: "merge", replaceUrl: true, @@ -1095,6 +1093,18 @@ export class VaultComponent implements OnInit, OnDestroy { result.action === CollectionDialogAction.Deleted ) { this.refresh(); + + // If we deleted the selected collection, navigate up/away + if ( + result.action === CollectionDialogAction.Deleted && + this.selectedCollection?.node.id === c?.id + ) { + void this.router.navigate([], { + queryParams: { collectionId: this.selectedCollection.parent?.node.id ?? null }, + queryParamsHandling: "merge", + replaceUrl: true, + }); + } } } From fb211c5feea0594ccdd992c2729154424376bc57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa?= <dani-garcia@users.noreply.github.com> Date: Mon, 22 Apr 2024 18:58:29 +0200 Subject: [PATCH 249/351] Add rust analyzer support for desktop-native (#8830) --- .vscode/settings.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 27e3a9b293..3a70af3481 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,5 +5,6 @@ "**/locales/*[^n]/messages.json": true, "**/_locales/[^e]*/messages.json": true, "**/_locales/*[^n]/messages.json": true - } + }, + "rust-analyzer.linkedProjects": ["apps/desktop/desktop_native/Cargo.toml"] } From 29d4f1aad5fade6c85749e002a4d0b1a05f2fdbb Mon Sep 17 00:00:00 2001 From: Cesar Gonzalez <cesar.a.gonzalezcs@gmail.com> Date: Mon, 22 Apr 2024 12:58:20 -0500 Subject: [PATCH 250/351] [PM-7660] Master Password Re-Prompt from Autofill Not Working (#8862) --- .../browser/src/background/main.background.ts | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 15f21b2501..a64ee2b8a0 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1105,20 +1105,22 @@ export default class MainBackground { await (this.eventUploadService as EventUploadService).init(true); this.twoFactorService.init(); - if (!this.popupOnlyContext) { - await this.vaultTimeoutService.init(true); - this.fido2Background.init(); - await this.runtimeBackground.init(); - await this.notificationBackground.init(); - this.filelessImporterBackground.init(); - await this.commandsBackground.init(); - await this.overlayBackground.init(); - await this.tabsBackground.init(); - this.contextMenusBackground?.init(); - await this.idleBackground.init(); - if (BrowserApi.isManifestVersion(2)) { - await this.webRequestBackground.init(); - } + if (this.popupOnlyContext) { + return; + } + + await this.vaultTimeoutService.init(true); + this.fido2Background.init(); + await this.runtimeBackground.init(); + await this.notificationBackground.init(); + this.filelessImporterBackground.init(); + await this.commandsBackground.init(); + await this.overlayBackground.init(); + await this.tabsBackground.init(); + this.contextMenusBackground?.init(); + await this.idleBackground.init(); + if (BrowserApi.isManifestVersion(2)) { + await this.webRequestBackground.init(); } if (this.platformUtilsService.isFirefox() && !this.isPrivateMode) { From e29779875797cb8a508d3857aa25ecefca5aa5c1 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Mon, 22 Apr 2024 16:54:41 -0400 Subject: [PATCH 251/351] Stop CryptoService from using `getBgService` (#8843) --- .../src/popup/services/services.module.ts | 43 +++++++++++++++++-- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index a7da6b7612..b344a18492 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -31,6 +31,7 @@ import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/ab import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; @@ -55,6 +56,7 @@ import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt. 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"; +import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; @@ -63,6 +65,7 @@ import { AbstractStorageService, ObservableStorageService, } from "@bitwarden/common/platform/abstractions/storage.service"; +import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging"; // eslint-disable-next-line no-restricted-imports -- Used for dependency injection @@ -102,6 +105,7 @@ import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; import { BrowserFileDownloadService } from "../../platform/popup/services/browser-file-download.service"; import { BrowserStateService as StateServiceAbstraction } from "../../platform/services/abstractions/browser-state.service"; import { ScriptInjectorService } from "../../platform/services/abstractions/script-injector.service"; +import { BrowserCryptoService } from "../../platform/services/browser-crypto.service"; import { BrowserEnvironmentService } from "../../platform/services/browser-environment.service"; import BrowserLocalStorageService from "../../platform/services/browser-local-storage.service"; import { BrowserScriptInjectorService } from "../../platform/services/browser-script-injector.service"; @@ -232,12 +236,45 @@ const safeProviders: SafeProvider[] = [ }), safeProvider({ provide: CryptoService, - useFactory: (encryptService: EncryptService) => { - const cryptoService = getBgService<CryptoService>("cryptoService")(); + useFactory: ( + masterPasswordService: InternalMasterPasswordServiceAbstraction, + keyGenerationService: KeyGenerationService, + cryptoFunctionService: CryptoFunctionService, + encryptService: EncryptService, + platformUtilsService: PlatformUtilsService, + logService: LogService, + stateService: StateServiceAbstraction, + accountService: AccountServiceAbstraction, + stateProvider: StateProvider, + biometricStateService: BiometricStateService, + ) => { + const cryptoService = new BrowserCryptoService( + masterPasswordService, + keyGenerationService, + cryptoFunctionService, + encryptService, + platformUtilsService, + logService, + stateService, + accountService, + stateProvider, + biometricStateService, + ); new ContainerService(cryptoService, encryptService).attachToGlobal(self); return cryptoService; }, - deps: [EncryptService], + deps: [ + InternalMasterPasswordServiceAbstraction, + KeyGenerationService, + CryptoFunctionService, + EncryptService, + PlatformUtilsService, + LogService, + StateServiceAbstraction, + AccountServiceAbstraction, + StateProvider, + BiometricStateService, + ], }), safeProvider({ provide: TotpServiceAbstraction, From 33dae77a4d9caab3a0ea88ec89d10efb38097a84 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Mon, 22 Apr 2024 17:11:30 -0400 Subject: [PATCH 252/351] Revert "Stop CryptoService from using `getBgService` (#8843)" (#8867) This reverts commit e29779875797cb8a508d3857aa25ecefca5aa5c1. --- .../src/popup/services/services.module.ts | 43 ++----------------- 1 file changed, 3 insertions(+), 40 deletions(-) diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index b344a18492..a7da6b7612 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -31,7 +31,6 @@ import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/ab import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; -import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; @@ -56,7 +55,6 @@ import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt. 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"; -import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; @@ -65,7 +63,6 @@ import { AbstractStorageService, ObservableStorageService, } from "@bitwarden/common/platform/abstractions/storage.service"; -import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging"; // eslint-disable-next-line no-restricted-imports -- Used for dependency injection @@ -105,7 +102,6 @@ import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; import { BrowserFileDownloadService } from "../../platform/popup/services/browser-file-download.service"; import { BrowserStateService as StateServiceAbstraction } from "../../platform/services/abstractions/browser-state.service"; import { ScriptInjectorService } from "../../platform/services/abstractions/script-injector.service"; -import { BrowserCryptoService } from "../../platform/services/browser-crypto.service"; import { BrowserEnvironmentService } from "../../platform/services/browser-environment.service"; import BrowserLocalStorageService from "../../platform/services/browser-local-storage.service"; import { BrowserScriptInjectorService } from "../../platform/services/browser-script-injector.service"; @@ -236,45 +232,12 @@ const safeProviders: SafeProvider[] = [ }), safeProvider({ provide: CryptoService, - useFactory: ( - masterPasswordService: InternalMasterPasswordServiceAbstraction, - keyGenerationService: KeyGenerationService, - cryptoFunctionService: CryptoFunctionService, - encryptService: EncryptService, - platformUtilsService: PlatformUtilsService, - logService: LogService, - stateService: StateServiceAbstraction, - accountService: AccountServiceAbstraction, - stateProvider: StateProvider, - biometricStateService: BiometricStateService, - ) => { - const cryptoService = new BrowserCryptoService( - masterPasswordService, - keyGenerationService, - cryptoFunctionService, - encryptService, - platformUtilsService, - logService, - stateService, - accountService, - stateProvider, - biometricStateService, - ); + useFactory: (encryptService: EncryptService) => { + const cryptoService = getBgService<CryptoService>("cryptoService")(); new ContainerService(cryptoService, encryptService).attachToGlobal(self); return cryptoService; }, - deps: [ - InternalMasterPasswordServiceAbstraction, - KeyGenerationService, - CryptoFunctionService, - EncryptService, - PlatformUtilsService, - LogService, - StateServiceAbstraction, - AccountServiceAbstraction, - StateProvider, - BiometricStateService, - ], + deps: [EncryptService], }), safeProvider({ provide: TotpServiceAbstraction, From 4afb5d04f00c4a4a883d3dfc20a8752f929880d6 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Mon, 22 Apr 2024 17:14:14 -0400 Subject: [PATCH 253/351] Remove `alarms` Permission (#8866) --- apps/browser/src/manifest.v3.json | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index cdd0869fc5..26efbbbf9b 100644 --- a/apps/browser/src/manifest.v3.json +++ b/apps/browser/src/manifest.v3.json @@ -59,7 +59,6 @@ "clipboardRead", "clipboardWrite", "idle", - "alarms", "scripting", "offscreen" ], From 714ca66f331ec4ffde3a3e40fc767b27a4967903 Mon Sep 17 00:00:00 2001 From: Bitwarden DevOps <106330231+bitwarden-devops-bot@users.noreply.github.com> Date: Tue, 23 Apr 2024 07:32:09 -0400 Subject: [PATCH 254/351] Bumped browser,cli,desktop,web version to (#8875) --- apps/browser/package.json | 2 +- apps/browser/src/manifest.json | 2 +- apps/browser/src/manifest.v3.json | 2 +- apps/cli/package.json | 2 +- apps/desktop/package.json | 2 +- apps/desktop/src/package-lock.json | 4 ++-- apps/desktop/src/package.json | 2 +- apps/web/package.json | 2 +- package-lock.json | 8 ++++---- 9 files changed, 13 insertions(+), 13 deletions(-) diff --git a/apps/browser/package.json b/apps/browser/package.json index ee6d100572..506f19f279 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/browser", - "version": "2024.4.1", + "version": "2024.4.2", "scripts": { "build": "webpack", "build:mv3": "cross-env MANIFEST_VERSION=3 webpack", diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index 78f1e2cc41..18bfaf8acb 100644 --- a/apps/browser/src/manifest.json +++ b/apps/browser/src/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "__MSG_extName__", "short_name": "__MSG_appName__", - "version": "2024.4.1", + "version": "2024.4.2", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index 26efbbbf9b..c0c88706b8 100644 --- a/apps/browser/src/manifest.v3.json +++ b/apps/browser/src/manifest.v3.json @@ -3,7 +3,7 @@ "minimum_chrome_version": "102.0", "name": "__MSG_extName__", "short_name": "__MSG_appName__", - "version": "2024.4.1", + "version": "2024.4.2", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/cli/package.json b/apps/cli/package.json index 690842d831..b06caacfd4 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.1", + "version": "2024.4.0", "keywords": [ "bitwarden", "password", diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 4bb0ab2d93..5e098eb213 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.4.2", + "version": "2024.4.3", "keywords": [ "bitwarden", "password", diff --git a/apps/desktop/src/package-lock.json b/apps/desktop/src/package-lock.json index 11b38bd273..d6945fd16e 100644 --- a/apps/desktop/src/package-lock.json +++ b/apps/desktop/src/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bitwarden/desktop", - "version": "2024.4.2", + "version": "2024.4.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bitwarden/desktop", - "version": "2024.4.2", + "version": "2024.4.3", "license": "GPL-3.0", "dependencies": { "@bitwarden/desktop-native": "file:../desktop_native", diff --git a/apps/desktop/src/package.json b/apps/desktop/src/package.json index a65dab016c..fa190af9a6 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.4.2", + "version": "2024.4.3", "author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)", "homepage": "https://bitwarden.com", "license": "GPL-3.0", diff --git a/apps/web/package.json b/apps/web/package.json index 55fe0987d7..434712cdf4 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/web-vault", - "version": "2024.4.1", + "version": "2024.4.2", "scripts": { "build:oss": "webpack", "build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js", diff --git a/package-lock.json b/package-lock.json index d72ba9cb19..f5932a25b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -193,11 +193,11 @@ }, "apps/browser": { "name": "@bitwarden/browser", - "version": "2024.4.1" + "version": "2024.4.2" }, "apps/cli": { "name": "@bitwarden/cli", - "version": "2024.3.1", + "version": "2024.4.0", "license": "GPL-3.0-only", "dependencies": { "@koa/multer": "3.0.2", @@ -233,7 +233,7 @@ }, "apps/desktop": { "name": "@bitwarden/desktop", - "version": "2024.4.2", + "version": "2024.4.3", "hasInstallScript": true, "license": "GPL-3.0" }, @@ -247,7 +247,7 @@ }, "apps/web": { "name": "@bitwarden/web-vault", - "version": "2024.4.1" + "version": "2024.4.2" }, "libs/admin-console": { "name": "@bitwarden/admin-console", From e4ebf4aeccb4c214e0b483113d9b9b53cc6f7438 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Tue, 23 Apr 2024 09:18:49 -0400 Subject: [PATCH 255/351] [PM-7349] Update snap description with new URL to help docs (#8703) * Updated snap summary with new URL to help docs. * Updated to use summary and description. --- apps/desktop/electron-builder.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/desktop/electron-builder.json b/apps/desktop/electron-builder.json index 4f0d05581c..960d56b036 100644 --- a/apps/desktop/electron-builder.json +++ b/apps/desktop/electron-builder.json @@ -228,7 +228,8 @@ "artifactName": "${productName}-${version}-${arch}.${ext}" }, "snap": { - "summary": "After installation enable required `password-manager-service` by running `sudo snap connect bitwarden:password-manager-service`.", + "summary": "Bitwarden is a secure and free password manager for all of your devices.", + "description": "**Installation**\nBitwarden requires access to the `password-manager-service`. Please enable it through permissions or by running `sudo snap connect bitwarden:password-manager-service` after installation. See https://btwrdn.com/install-snap for details.", "autoStart": true, "base": "core22", "confinement": "strict", From 73d0782b6ce4a0be1cf0480d0a9658420b5ef438 Mon Sep 17 00:00:00 2001 From: Will Martin <contact@willmartian.com> Date: Tue, 23 Apr 2024 09:45:11 -0400 Subject: [PATCH 256/351] [CL-110] fix code block text color in Storybook (#8868) --- libs/components/src/styles.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/libs/components/src/styles.scss b/libs/components/src/styles.scss index f03d2dd340..ae97838e09 100644 --- a/libs/components/src/styles.scss +++ b/libs/components/src/styles.scss @@ -47,3 +47,8 @@ $card-icons-base: "../../src/billing/images/cards/"; @import "bootstrap/scss/_print"; @import "multi-select/scss/bw.theme.scss"; + +// Workaround for https://bitwarden.atlassian.net/browse/CL-110 +#storybook-docs pre.prismjs { + color: white; +} From 7d58b21856c5551dadd7127c725be2882076cf20 Mon Sep 17 00:00:00 2001 From: vinith-kovan <156108204+vinith-kovan@users.noreply.github.com> Date: Tue, 23 Apr 2024 19:20:15 +0530 Subject: [PATCH 257/351] migrating activate autofill component (#8782) --- .../policies/activate-autofill.component.html | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/bitwarden_license/bit-web/src/app/admin-console/policies/activate-autofill.component.html b/bitwarden_license/bit-web/src/app/admin-console/policies/activate-autofill.component.html index 9f08e98dae..94f2e8a422 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/policies/activate-autofill.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/policies/activate-autofill.component.html @@ -5,15 +5,7 @@ }}</a> </app-callout> -<div class="form-group"> - <div class="form-check"> - <input - class="form-check-input" - type="checkbox" - id="enabled" - [formControl]="enabled" - name="Enabled" - /> - <label class="form-check-label" for="enabled">{{ "turnOn" | i18n }}</label> - </div> -</div> +<bit-form-control> + <input type="checkbox" bitCheckbox [formControl]="enabled" /> + <bit-label>{{ "turnOn" | i18n }}</bit-label> +</bit-form-control> From 38ea110755a4256f96ba7946b680070b5c32cdc6 Mon Sep 17 00:00:00 2001 From: vinith-kovan <156108204+vinith-kovan@users.noreply.github.com> Date: Tue, 23 Apr 2024 19:22:26 +0530 Subject: [PATCH 258/351] migrating two factor authentication component (#8760) --- .../two-factor-authentication.component.html | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/policies/two-factor-authentication.component.html b/apps/web/src/app/admin-console/organizations/policies/two-factor-authentication.component.html index 3286c08689..d0be72a52e 100644 --- a/apps/web/src/app/admin-console/organizations/policies/two-factor-authentication.component.html +++ b/apps/web/src/app/admin-console/organizations/policies/two-factor-authentication.component.html @@ -2,15 +2,7 @@ {{ "twoStepLoginPolicyWarning" | i18n }} </app-callout> -<div class="form-group"> - <div class="form-check"> - <input - class="form-check-input" - type="checkbox" - id="enabled" - [formControl]="enabled" - name="Enabled" - /> - <label class="form-check-label" for="enabled">{{ "turnOn" | i18n }}</label> - </div> -</div> +<bit-form-control> + <input type="checkbox" bitCheckbox [formControl]="enabled" /> + <bit-label>{{ "turnOn" | i18n }}</bit-label> +</bit-form-control> From 5f3844aa38de27dff8986b399a5db327a6101c04 Mon Sep 17 00:00:00 2001 From: Tom <144813356+ttalty@users.noreply.github.com> Date: Tue, 23 Apr 2024 11:26:31 -0400 Subject: [PATCH 259/351] Getting the user's access token for file upload (#8877) --- libs/common/src/abstractions/api.service.ts | 9 ++++++++- libs/common/src/services/api.service.ts | 5 +++-- libs/common/src/services/event/event-upload.service.ts | 2 +- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/libs/common/src/abstractions/api.service.ts b/libs/common/src/abstractions/api.service.ts index 9b3160ee19..c1a0e1f9cd 100644 --- a/libs/common/src/abstractions/api.service.ts +++ b/libs/common/src/abstractions/api.service.ts @@ -103,6 +103,7 @@ import { EventResponse } from "../models/response/event.response"; import { ListResponse } from "../models/response/list.response"; import { ProfileResponse } from "../models/response/profile.response"; import { UserKeyResponse } from "../models/response/user-key.response"; +import { UserId } from "../types/guid"; import { AttachmentRequest } from "../vault/models/request/attachment.request"; import { CipherBulkDeleteRequest } from "../vault/models/request/cipher-bulk-delete.request"; import { CipherBulkMoveRequest } from "../vault/models/request/cipher-bulk-move.request"; @@ -451,7 +452,13 @@ export abstract class ApiService { end: string, token: string, ) => Promise<ListResponse<EventResponse>>; - postEventsCollect: (request: EventRequest[]) => Promise<any>; + + /** + * Posts events for a user + * @param request The array of events to upload + * @param userId The optional user id the events belong to. If no user id is provided the active user id is used. + */ + postEventsCollect: (request: EventRequest[], userId?: UserId) => Promise<any>; deleteSsoUser: (organizationId: string) => Promise<void>; getSsoUserIdentifier: () => Promise<string>; diff --git a/libs/common/src/services/api.service.ts b/libs/common/src/services/api.service.ts index e8135f3d6c..84fa7bd077 100644 --- a/libs/common/src/services/api.service.ts +++ b/libs/common/src/services/api.service.ts @@ -118,6 +118,7 @@ import { EnvironmentService } from "../platform/abstractions/environment.service import { PlatformUtilsService } from "../platform/abstractions/platform-utils.service"; import { StateService } from "../platform/abstractions/state.service"; import { Utils } from "../platform/misc/utils"; +import { UserId } from "../types/guid"; import { AttachmentRequest } from "../vault/models/request/attachment.request"; import { CipherBulkDeleteRequest } from "../vault/models/request/cipher-bulk-delete.request"; import { CipherBulkMoveRequest } from "../vault/models/request/cipher-bulk-move.request"; @@ -1423,8 +1424,8 @@ export class ApiService implements ApiServiceAbstraction { return new ListResponse(r, EventResponse); } - async postEventsCollect(request: EventRequest[]): Promise<any> { - const authHeader = await this.getActiveBearerToken(); + async postEventsCollect(request: EventRequest[], userId?: UserId): Promise<any> { + const authHeader = await this.tokenService.getAccessToken(userId); const headers = new Headers({ "Device-Type": this.deviceType, Authorization: "Bearer " + authHeader, diff --git a/libs/common/src/services/event/event-upload.service.ts b/libs/common/src/services/event/event-upload.service.ts index 6f229751bf..c87d3b2024 100644 --- a/libs/common/src/services/event/event-upload.service.ts +++ b/libs/common/src/services/event/event-upload.service.ts @@ -70,7 +70,7 @@ export class EventUploadService implements EventUploadServiceAbstraction { return req; }); try { - await this.apiService.postEventsCollect(request); + await this.apiService.postEventsCollect(request, userId); } catch (e) { this.logService.error(e); // Add the events back to state if there was an error and they were not uploaded. From ca38a5bc1f76f816de4f5c049d69b2fba8c33b54 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 23 Apr 2024 15:29:47 +0000 Subject: [PATCH 260/351] Autosync the updated translations (#8878) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/browser/src/_locales/cy/messages.json | 8 +- apps/browser/src/_locales/de/messages.json | 20 ++-- apps/browser/src/_locales/ko/messages.json | 44 ++++----- apps/browser/src/_locales/nl/messages.json | 6 +- apps/browser/src/_locales/pl/messages.json | 4 +- apps/browser/src/_locales/pt_BR/messages.json | 96 +++++++++---------- apps/browser/src/_locales/sr/messages.json | 16 ++-- apps/browser/src/_locales/zh_CN/messages.json | 12 +-- apps/browser/store/locales/de/copy.resx | 56 +++++------ apps/browser/store/locales/ko/copy.resx | 4 +- apps/browser/store/locales/pl/copy.resx | 60 ++++++------ apps/browser/store/locales/pt_BR/copy.resx | 6 +- apps/browser/store/locales/zh_CN/copy.resx | 60 ++++++------ 13 files changed, 195 insertions(+), 197 deletions(-) diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index c718c1d876..2be868872c 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -23,7 +23,7 @@ "message": "Enterprise single sign-on" }, "cancel": { - "message": "Cancel" + "message": "Canslo" }, "close": { "message": "Cau" @@ -318,7 +318,7 @@ "message": "Golygu" }, "view": { - "message": "View" + "message": "Gweld" }, "noItemsInList": { "message": "Does dim eitemau i'w rhestru." @@ -549,10 +549,10 @@ "message": "Ydych chi'n siŵr eich bod am allgofnodi?" }, "yes": { - "message": "Yes" + "message": "Ydw" }, "no": { - "message": "No" + "message": "Na" }, "unexpectedError": { "message": "An unexpected error has occurred." diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index 8f2a59af1e..d55d499b3c 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden Password Manager", + "message": "Bitwarden Passwortmanager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", + "message": "Zu Hause, am Arbeitsplatz oder unterwegs schützt Bitwarden einfach alle deine Passwörter, Passkeys und vertraulichen Informationen.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -173,10 +173,10 @@ "message": "Master-Passwort ändern" }, "continueToWebApp": { - "message": "Continue to web app?" + "message": "Weiter zur Web-App?" }, "changeMasterPasswordOnWebConfirmation": { - "message": "You can change your master password on the Bitwarden web app." + "message": "Du kannst dein Master-Passwort in der Bitwarden Web-App ändern." }, "fingerprintPhrase": { "message": "Fingerabdruck-Phrase", @@ -3001,7 +3001,7 @@ "description": "Notification message for when saving credentials has failed." }, "success": { - "message": "Success" + "message": "Erfolg" }, "removePasskey": { "message": "Passkey entfernen" @@ -3010,21 +3010,21 @@ "message": "Passkey entfernt" }, "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + "message": "Hinweis: Nicht zugeordnete Organisationseinträge sind nicht mehr in der Ansicht aller Tresore sichtbar und nur über die Administrator-Konsole zugänglich." }, "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." + "message": "Hinweis: Ab dem 16. Mai 2024 sind nicht zugewiesene Organisationselemente nicht mehr in der Ansicht aller Tresore sichtbar und nur über die Administrator-Konsole zugänglich." }, "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", + "message": "Weise diese Einträge einer Sammlung aus der", "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.", + "message": "zu, um sie sichtbar zu machen.", "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" + "message": "Administrator-Konsole" }, "errorAssigningTargetCollection": { "message": "Fehler beim Zuweisen der Ziel-Sammlung." diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index 4bc4302f8b..1724225b0e 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden Password Manager", + "message": "Bitwarden 비밀번호 관리자", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", + "message": "집에서도, 직장에서도, 이동 중에도 Bitwarden은 비밀번호, 패스키, 민감 정보를 쉽게 보호합니다.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -173,7 +173,7 @@ "message": "마스터 비밀번호 변경" }, "continueToWebApp": { - "message": "Continue to web app?" + "message": "웹 앱에서 계속하시겠용?" }, "changeMasterPasswordOnWebConfirmation": { "message": "You can change your master password on the Bitwarden web app." @@ -688,10 +688,10 @@ "message": "Ask to update a login's password when a change is detected on a website. Applies to all logged in accounts." }, "enableUsePasskeys": { - "message": "Ask to save and use passkeys" + "message": "패스키를 저장 및 사용할지 묻기" }, "usePasskeysDesc": { - "message": "Ask to save new passkeys or log in with passkeys stored in your vault. Applies to all logged in accounts." + "message": "보관함에 새 패스키를 저장하거나 로그인할지 물어봅니다. 모든 로그인된 계정에 적용됩니다." }, "notificationChangeDesc": { "message": "Bitwarden에 저장되어 있는 비밀번호를 이 비밀번호로 변경하시겠습니까?" @@ -2786,55 +2786,55 @@ "message": "Confirm file password" }, "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": "패스키는 복제된 아이템에 복사되지 않습니다. 계속 이 항목을 복제하시겠어요?" }, "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { - "message": "Verification required by the initiating site. This feature is not yet implemented for accounts without master password." + "message": "사이트에서 인증을 요구합니다. 이 기능은 비밀번호가 없는 계정에서는 아직 지원하지 않습니다." }, "logInWithPasskey": { - "message": "Log in with passkey?" + "message": "패스키로 로그인하시겠어요?" }, "passkeyAlreadyExists": { - "message": "A passkey already exists for this application." + "message": "이미 이 애플리케이션에 해당하는 패스키가 있습니다." }, "noPasskeysFoundForThisApplication": { - "message": "No passkeys found for this application." + "message": "이 애플리케이션에 대한 패스키를 찾을 수 없습니다." }, "noMatchingPasskeyLogin": { - "message": "You do not have a matching login for this site." + "message": "사이트와 일치하는 로그인이 없습니다." }, "confirm": { "message": "Confirm" }, "savePasskey": { - "message": "Save passkey" + "message": "패스키 저장" }, "savePasskeyNewLogin": { - "message": "Save passkey as new login" + "message": "새 로그인으로 패스키 저장" }, "choosePasskey": { - "message": "Choose a login to save this passkey to" + "message": "패스키를 저장할 로그인 선택하기" }, "passkeyItem": { - "message": "Passkey Item" + "message": "패스키 항목" }, "overwritePasskey": { - "message": "Overwrite passkey?" + "message": "비밀번호를 덮어쓰시겠어요?" }, "overwritePasskeyAlert": { - "message": "This item already contains a passkey. Are you sure you want to overwrite the current passkey?" + "message": "이 항목은 이미 패스키가 있습니다. 정말로 현재 패스키를 덮어쓰시겠어요?" }, "featureNotSupported": { "message": "Feature not yet supported" }, "yourPasskeyIsLocked": { - "message": "Authentication required to use passkey. Verify your identity to continue." + "message": "패스키를 사용하려면 인증이 필요합니다. 인증을 진행해주세요." }, "multifactorAuthenticationCancelled": { "message": "Multifactor authentication cancelled" @@ -3004,10 +3004,10 @@ "message": "Success" }, "removePasskey": { - "message": "Remove passkey" + "message": "패스키 제거" }, "passkeyRemoved": { - "message": "Passkey removed" + "message": "패스키 제거됨" }, "unassignedItemsBannerNotice": { "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index 5a52b4f7ef..f1424df0b9 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -531,7 +531,7 @@ "message": "Kan de QR-code van de huidige webpagina niet scannen" }, "totpCaptureSuccess": { - "message": "Authenticatie-sleutel toegevoegd" + "message": "Authenticatiesleutel toegevoegd" }, "totpCapture": { "message": "Scan de authenticatie-QR-code van de huidige webpagina" @@ -1673,10 +1673,10 @@ "message": "Browserintegratie is niet ingeschakeld in de Bitwarden-desktopapplicatie. Schakel deze optie in de instellingen binnen de desktop-applicatie in." }, "startDesktopTitle": { - "message": "Bitwarden-desktopapplicatie opstarten" + "message": "Bitwarden desktopapplicatie opstarten" }, "startDesktopDesc": { - "message": "Je moet de Bitwarden-desktopapplicatie starten om deze functie te gebruiken." + "message": "Je moet de Bitwarden desktopapplicatie starten om deze functie te gebruiken." }, "errorEnableBiometricTitle": { "message": "Kon biometrie niet inschakelen" diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index e768a70d52..9b3e8f20fc 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden Password Manager", + "message": "Menedżer Haseł Bitwarden", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", + "message": "W domu, w pracy, lub w ruchu, Bitwarden z łatwością zabezpiecza wszystkie Twoje hasła, passkeys i poufne informacje.", "description": "Extension description" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index c6e62fbd4f..ef2b6f2dca 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden Password Manager", + "message": "Bitwarden Gerenciador de Senhas", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", + "message": "Em casa, no trabalho, ou em qualquer lugar, o Bitwarden protege facilmente todas as suas senhas, senhas e informações confidenciais.", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -173,10 +173,10 @@ "message": "Alterar Senha Mestra" }, "continueToWebApp": { - "message": "Continue to web app?" + "message": "Continuar no aplicativo web?" }, "changeMasterPasswordOnWebConfirmation": { - "message": "You can change your master password on the Bitwarden web app." + "message": "Você pode alterar a sua senha mestra no aplicativo web Bitwarden." }, "fingerprintPhrase": { "message": "Frase Biométrica", @@ -500,10 +500,10 @@ "message": "A sua nova conta foi criada! Agora você pode iniciar a sessão." }, "youSuccessfullyLoggedIn": { - "message": "You successfully logged in" + "message": "Você logou na sua conta com sucesso" }, "youMayCloseThisWindow": { - "message": "You may close this window" + "message": "Você pode fechar esta janela" }, "masterPassSent": { "message": "Enviamos um e-mail com a dica da sua senha mestra." @@ -1500,7 +1500,7 @@ "message": "Código PIN inválido." }, "tooManyInvalidPinEntryAttemptsLoggingOut": { - "message": "Too many invalid PIN entry attempts. Logging out." + "message": "Muitas tentativas de entrada de PIN inválidas. Desconectando." }, "unlockWithBiometrics": { "message": "Desbloquear com a biometria" @@ -2005,7 +2005,7 @@ "message": "Selecionar pasta..." }, "noFoldersFound": { - "message": "No folders found", + "message": "Nenhuma pasta encontrada", "description": "Used as a message within the notification bar when no folders are found" }, "orgPermissionsUpdatedMustSetPassword": { @@ -2017,7 +2017,7 @@ "description": "Used as a card title description on the set password page to explain why the user is there" }, "verificationRequired": { - "message": "Verification required", + "message": "Verificação necessária", "description": "Default title for the user verification dialog." }, "hours": { @@ -2652,40 +2652,40 @@ } }, "tryAgain": { - "message": "Try again" + "message": "Tentar novamente" }, "verificationRequiredForActionSetPinToContinue": { - "message": "Verification required for this action. Set a PIN to continue." + "message": "Verificação necessária para esta ação. Defina um PIN para continuar." }, "setPin": { - "message": "Set PIN" + "message": "Definir PIN" }, "verifyWithBiometrics": { - "message": "Verify with biometrics" + "message": "Verificiar com biometria" }, "awaitingConfirmation": { - "message": "Awaiting confirmation" + "message": "Aguardando confirmação" }, "couldNotCompleteBiometrics": { - "message": "Could not complete biometrics." + "message": "Não foi possível completar a biometria." }, "needADifferentMethod": { - "message": "Need a different method?" + "message": "Precisa de um método diferente?" }, "useMasterPassword": { - "message": "Use master password" + "message": "Usar a senha mestra" }, "usePin": { - "message": "Use PIN" + "message": "Usar PIN" }, "useBiometrics": { - "message": "Use biometrics" + "message": "Usar biometria" }, "enterVerificationCodeSentToEmail": { - "message": "Enter the verification code that was sent to your email." + "message": "Digite o código de verificação que foi enviado para o seu e-mail." }, "resendCode": { - "message": "Resend code" + "message": "Reenviar código" }, "total": { "message": "Total" @@ -2700,19 +2700,19 @@ } }, "launchDuoAndFollowStepsToFinishLoggingIn": { - "message": "Launch Duo and follow the steps to finish logging in." + "message": "Inicie o Duo e siga os passos para finalizar o login." }, "duoRequiredForAccount": { - "message": "Duo two-step login is required for your account." + "message": "A autenticação em duas etapas do Duo é necessária para sua conta." }, "popoutTheExtensionToCompleteLogin": { - "message": "Popout the extension to complete login." + "message": "Abra a extensão para concluir o login." }, "popoutExtension": { - "message": "Popout extension" + "message": "Extensão pop-out" }, "launchDuo": { - "message": "Launch Duo" + "message": "Abrir o Duo" }, "importFormatError": { "message": "Os dados não estão formatados corretamente. Por favor, verifique o seu arquivo de importação e tente novamente." @@ -2846,13 +2846,13 @@ "message": "Nome de usuário ou senha incorretos" }, "incorrectPassword": { - "message": "Incorrect password" + "message": "Senha incorreta" }, "incorrectCode": { - "message": "Incorrect code" + "message": "Código incorreto" }, "incorrectPin": { - "message": "Incorrect PIN" + "message": "PIN incorreto" }, "multifactorAuthenticationFailed": { "message": "Falha na autenticação de múltiplos fatores" @@ -2965,71 +2965,71 @@ "description": "Label indicating the most common import formats" }, "overrideDefaultBrowserAutofillTitle": { - "message": "Make Bitwarden your default password manager?", + "message": "Tornar o Bitwarden seu gerenciador de senhas padrão?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Ignoring this option may cause conflicts between the Bitwarden auto-fill menu and your browser's.", + "message": "Ignorar esta opção pode causar conflitos entre o menu de autopreenchimento do Bitwarden e o do seu navegador.", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { - "message": "Make Bitwarden your default password manager", + "message": "Faça do Bitwarden seu gerenciador de senhas padrão", "description": "Label for the setting that allows overriding the default browser autofill settings" }, "privacyPermissionAdditionNotGrantedTitle": { - "message": "Unable to set Bitwarden as the default password manager", + "message": "Não é possível definir o Bitwarden como o gerenciador de senhas padrão", "description": "Title for the dialog that appears when the user has not granted the extension permission to set privacy settings" }, "privacyPermissionAdditionNotGrantedDescription": { - "message": "You must grant browser privacy permissions to Bitwarden to set it as the default password manager.", + "message": "Você deve conceder permissões de privacidade do navegador ao Bitwarden para defini-lo como o Gerenciador de Senhas padrão.", "description": "Description for the dialog that appears when the user has not granted the extension permission to set privacy settings" }, "makeDefault": { - "message": "Make default", + "message": "Tornar padrão", "description": "Button text for the setting that allows overriding the default browser autofill settings" }, "saveCipherAttemptSuccess": { - "message": "Credentials saved successfully!", + "message": "Credenciais salvas com sucesso!", "description": "Notification message for when saving credentials has succeeded." }, "updateCipherAttemptSuccess": { - "message": "Credentials updated successfully!", + "message": "Credenciais atualizadas com sucesso!", "description": "Notification message for when updating credentials has succeeded." }, "saveCipherAttemptFailed": { - "message": "Error saving credentials. Check console for details.", + "message": "Erro ao salvar credenciais. Verifique o console para detalhes.", "description": "Notification message for when saving credentials has failed." }, "success": { - "message": "Success" + "message": "Sucesso" }, "removePasskey": { - "message": "Remove passkey" + "message": "Remover senha" }, "passkeyRemoved": { - "message": "Passkey removed" + "message": "Chave de acesso removida" }, "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + "message": "Aviso: Itens da organização não atribuídos não estão mais visíveis na visualização Todos os Cofres e só são acessíveis por meio do painel de administração." }, "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." + "message": "Aviso: Em 16 de maio, 2024, itens da organização não serão mais visíveis na visualização Todos os Cofres e só serão acessíveis por meio do painel de administração." }, "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", + "message": "Atribua estes itens a uma coleção da", "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.", + "message": "para torná-los visíveis.", "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" + "message": "Painel de administração" }, "errorAssigningTargetCollection": { - "message": "Error assigning target collection." + "message": "Erro ao atribuir coleção de destino." }, "errorAssigningTargetFolder": { - "message": "Error assigning target folder." + "message": "Erro ao atribuir pasta de destino." } } diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index 5819546800..acac4d14c6 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -173,10 +173,10 @@ "message": "Промени главну лозинку" }, "continueToWebApp": { - "message": "Continue to web app?" + "message": "Ићи на веб апликацију?" }, "changeMasterPasswordOnWebConfirmation": { - "message": "You can change your master password on the Bitwarden web app." + "message": "Можете променити главну лозинку на Bitwarden веб апликацији." }, "fingerprintPhrase": { "message": "Сигурносна Фраза Сефа", @@ -3001,7 +3001,7 @@ "description": "Notification message for when saving credentials has failed." }, "success": { - "message": "Success" + "message": "Успех" }, "removePasskey": { "message": "Уклонити приступачни кључ" @@ -3010,10 +3010,10 @@ "message": "Приступачни кључ је уклоњен" }, "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + "message": "Напомена: Недодељене ставке организације више нису видљиве у приказу Сви сефови и доступне су само преко Админ конзоле." }, "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." + "message": "Напомена: од 16 Маја 2024м недодељене ставке организације више нису видљиве у приказу Сви сефови и доступне су само преко Админ конзоле." }, "unassignedItemsBannerCTAPartOne": { "message": "Assign these items to a collection from the", @@ -3024,12 +3024,12 @@ "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" + "message": "Администраторска конзола" }, "errorAssigningTargetCollection": { - "message": "Error assigning target collection." + "message": "Грешка при додељивању циљне колекције." }, "errorAssigningTargetFolder": { - "message": "Error assigning target folder." + "message": "Грешка при додељивању циљне фасцикле." } } diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index a2f856c31b..fa4dab6a8f 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden Password Manager", + "message": "Bitwarden 密码管理器", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", + "message": "无论是在家里、工作中还是在外出时,Bitwarden 都可以轻松地保护您的所有密码、通行密钥和敏感信息。", "description": "Extension description" }, "loginOrCreateNewAccount": { @@ -176,7 +176,7 @@ "message": "前往网页 App 吗?" }, "changeMasterPasswordOnWebConfirmation": { - "message": "You can change your master password on the Bitwarden web app." + "message": "您可以在 Bitwarden 网页应用上更改您的主密码。" }, "fingerprintPhrase": { "message": "指纹短语", @@ -3010,17 +3010,17 @@ "message": "通行密钥已移除" }, "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + "message": "注意:未分配的组织项目在「所有密码库」视图中不再可见,只能通过管理控制台访问。" }, "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." + "message": "注意:从 2024 年 5 月 16 日起,未分配的组织项目在「所有密码库」视图中将不再可见,只能通过管理控制台访问。" }, "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.", + "message": "以使其可见。", "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": { diff --git a/apps/browser/store/locales/de/copy.resx b/apps/browser/store/locales/de/copy.resx index 2267c6c85e..eb3ab2afd4 100644 --- a/apps/browser/store/locales/de/copy.resx +++ b/apps/browser/store/locales/de/copy.resx @@ -121,55 +121,55 @@ <value>Bitwarden Passwort-Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> + <value>Zu Hause, am Arbeitsplatz oder unterwegs schützt Bitwarden einfach alle deine Passwörter, Passkeys und vertraulichen Informationen.</value> </data> <data name="Description" xml:space="preserve"> - <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! + <value>Ausgezeichnet als bester Passwortmanager von PCMag, WIRED, The Verge, CNET, G2 und vielen anderen! -SECURE YOUR DIGITAL LIFE -Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. +SCHÜTZE DEIN DIGITALES LEBEN +Sicher dein digitales Leben und schütze dich vor Passwortdiebstählen, indem du individuelle, sichere Passwörter für jedes Konto erstellest und speicherst. Verwalte alles in einem Ende-zu-Ende verschlüsselten Passwort-Tresor, auf den nur du Zugriff hast. -ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE -Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. +ZUGRIFF AUF DEINE DATEN, ÜBERALL, JEDERZEIT UND AUF JEDEM GERÄT +Verwalte, speichere, sichere und teile einfach eine unbegrenzte Anzahl von Passwörtern auf einer unbegrenzten Anzahl von Geräten ohne Einschränkungen. -EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE -Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. +JEDER SOLLTE DIE MÖGLICHKEIT HABEN, ONLINE GESCHÜTZT ZU BLEIBEN +Verwende Bitwarden kostenlos, ohne Werbung oder Datenverkauf. Bitwarden glaubt, dass jeder die Möglichkeit haben sollte, online geschützt zu bleiben. Premium-Abos bieten Zugang zu erweiterten Funktionen. -EMPOWER YOUR TEAMS WITH BITWARDEN -Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. +STÄRKE DEINE TEAMS MIT BITWARDEN +Tarife für Teams und Enterprise enthalten professionelle Business-Funktionen. Einige Beispiele sind SSO-Integration, Selbst-Hosting, Directory-Integration und SCIM-Bereitstellung, globale Richtlinien, API-Zugang, Ereignisprotokolle und mehr. -Use Bitwarden to secure your workforce and share sensitive information with colleagues. +Nutze Bitwarden, um deine Mitarbeiter abzusichern und sensible Informationen mit Kollegen zu teilen. -More reasons to choose Bitwarden: +Weitere Gründe, Bitwarden zu wählen: -World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. +Weltklasse-Verschlüsselung +Passwörter werden mit fortschrittlicher Ende-zu-Ende-Verschlüsselung (AES-256 bit, salted hashtag und PBKDF2 SHA-256) geschützt, damit deine Daten sicher und geheim bleiben. -3rd-party Audits -Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. +3rd-Party-Prüfungen +Bitwarden führt regelmäßig umfassende Sicherheitsprüfungen durch Dritte von namhaften Sicherheitsfirmen durch. Diese jährlichen Prüfungen umfassen Quellcode-Bewertungen und Penetration-Tests für Bitwarden-IPs, Server und Webanwendungen. -Advanced 2FA -Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. +Erweiterte 2FA +Schütze deine Zugangsdaten mit einem Authentifikator eines Drittanbieters, per E-Mail verschickten Codes oder FIDO2 WebAuthn-Zugangsadaten wie einem Hardware-Sicherheitsschlüssel oder Passkey. Bitwarden Send -Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. +Übertrage Daten direkt an andere, während die Ende-zu-Ende-Verschlüsselung beibehalten wird und die Verbreitung begrenzt werden kann. -Built-in Generator -Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. +Eingebauter Generator +Erstelle lange, komplexe und eindeutige Passwörter und eindeutige Benutzernamen für jede Website, die du besuchst. Integriere E-Mail-Alias-Anbieter für zusätzlichen Datenschutz. -Global Translations -Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. +Globale Übersetzungen +Es gibt Bitwarden-Übersetzungen für mehr als 60 Sprachen, die von der weltweiten Community über Crowdin übersetzt werden. -Cross-Platform Applications -Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. +Plattformübergreifende Anwendungen +Schütze und teile sensible Daten in deinem Bitwarden Tresor von jedem Browser, mobilen Gerät oder Desktop-Betriebssystem und mehr. -Bitwarden secures more than just passwords -End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! +Bitwarden schützt mehr als nur Passwörter +Ende-zu-Ende verschlüsselte Zugangsverwaltungs-Lösungen von Bitwarden ermöglicht es Organisationen, alles zu sichern, einschließlich Entwicklergeheimnissen und Passkeys. Besuche Bitwarden.com, um mehr über den Bitwarden Secrets Manager und Bitwarden Passwordless.dev zu erfahren! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> + <value>Zu Hause, am Arbeitsplatz oder unterwegs schützt Bitwarden einfach alle deine Passwörter, Passkeys und vertraulichen Informationen.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Synchronisiere und greife auf deinen Tresor von unterschiedlichen Geräten aus zu</value> diff --git a/apps/browser/store/locales/ko/copy.resx b/apps/browser/store/locales/ko/copy.resx index fdfb93ad6a..595663b1ca 100644 --- a/apps/browser/store/locales/ko/copy.resx +++ b/apps/browser/store/locales/ko/copy.resx @@ -121,7 +121,7 @@ <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> + <value>집에서도, 직장에서도, 이동 중에도 Bitwarden은 비밀번호, 패스키, 민감 정보를 쉽게 보호합니다.</value> </data> <data name="Description" xml:space="preserve"> <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! @@ -169,7 +169,7 @@ End-to-end encrypted credential management solutions from Bitwarden empower orga </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> + <value>집에서도, 직장에서도, 이동 중에도 Bitwarden은 비밀번호, 패스키, 민감 정보를 쉽게 보호합니다.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>여러 기기에서 보관함에 접근하고 동기화할 수 있습니다.</value> diff --git a/apps/browser/store/locales/pl/copy.resx b/apps/browser/store/locales/pl/copy.resx index 60709c7d4d..5641c68c48 100644 --- a/apps/browser/store/locales/pl/copy.resx +++ b/apps/browser/store/locales/pl/copy.resx @@ -118,58 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden Password Manager</value> + <value>Menedżer Haseł Bitwarden</value> </data> <data name="Summary" xml:space="preserve"> - <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> + <value>W domu, w pracy, lub w ruchu, Bitwarden z łatwością zabezpiecza wszystkie Twoje hasła, passkeys i poufne informacje.</value> </data> <data name="Description" xml:space="preserve"> - <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! + <value>Uznany za najlepszego menedżera haseł przez PCMag, WIRED, The Verge, CNET, G2 i wielu innych! -SECURE YOUR DIGITAL LIFE -Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. +ZABEZPIECZ SWOJE CYFROWE ŻYCIE +Zabezpiecz swoje cyfrowe życie i chroń przed naruszeniami danych, generując i zapisując unikalne, silne hasła do każdego konta. Przechowuj wszystko w zaszyfrowanym end-to-end magazynie haseł, do którego tylko Ty masz dostęp. -ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE -Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. +DOSTĘP DO SWOICH DANYCH W KAŻDYM MIEJSCU, W DOWOLNYM CZASIE, NA KAŻDYM URZĄDZENIU +Z łatwością zarządzaj, przechowuj, zabezpieczaj i udostępniaj nieograniczoną liczbę haseł na nieograniczonej liczbie urządzeń. -EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE -Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. +KAŻDY POWINIEN POSIADAĆ NARZĘDZIA ABY ZACHOWAĆ BEZPIECZEŃSTWO W INTERNECIE +Korzystaj z Bitwarden za darmo, bez reklam i sprzedawania Twoich danych. Bitwarden wierzy, że każdy powinien mieć możliwość zachowania bezpieczeństwa w Internecie. Plany premium oferują dostęp do zaawansowanych funkcji. -EMPOWER YOUR TEAMS WITH BITWARDEN -Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. +WZMOCNIJ SWOJE ZESPOŁY DZIĘKI BITWARDEN +Plany dla Zespołów i Enterprise oferują profesjonalne funkcje biznesowe. Na przykład obejmują integrację z SSO, własny hosting, integrację katalogów i udostępnianie SCIM, zasady globalne, dostęp do API, dzienniki zdarzeń i inne. -Use Bitwarden to secure your workforce and share sensitive information with colleagues. +Użyj Bitwarden, aby zabezpieczyć swoich pracowników i udostępniać poufne informacje współpracownikom. -More reasons to choose Bitwarden: +Więcej powodów, aby wybrać Bitwarden: -World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. +Szyfrowanie na światowym poziomie +Hasła są chronione za pomocą zaawansowanego, kompleksowego szyfrowania (AES-256-bitowy, solony hashtag i PBKDF2 SHA-256), dzięki czemu Twoje dane pozostają bezpieczne i prywatne. -3rd-party Audits -Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. +Audyty stron trzecich +Bitwarden regularnie przeprowadza kompleksowe audyty bezpieczeństwa stron trzecich we współpracy ze znanymi firmami security. Te coroczne audyty obejmują ocenę kodu źródłowego i testy penetracyjne adresów IP Bitwarden, serwerów i aplikacji internetowych. -Advanced 2FA -Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. +Zaawansowane 2FA +Zabezpiecz swój login za pomocą zewnętrznego narzędzia uwierzytelniającego, kodów przesłanych pocztą elektroniczną lub poświadczeń FIDO2 WebAuthn, takich jak sprzętowy klucz bezpieczeństwa lub hasło. -Bitwarden Send -Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. +Bitwarden Wyślij +Przesyłaj dane bezpośrednio do innych, zachowując kompleksowe szyfrowane bezpieczeństwo i ograniczając ryzyko. -Built-in Generator -Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. +Wbudowany generator +Twórz długie, złożone i różne hasła oraz unikalne nazwy użytkowników dla każdej odwiedzanej witryny. Zintegruj się z dostawcami aliasów e-mail, aby uzyskać dodatkową prywatność. -Global Translations -Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. +Tłumaczenia globalne +Istnieją tłumaczenia Bitwarden na ponad 60 języków, tłumaczone przez globalną społeczność za pośrednictwem Crowdin. -Cross-Platform Applications -Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. +Aplikacje wieloplatformowe +Zabezpiecz i udostępniaj poufne dane w swoim Sejfie Bitwarden z dowolnej przeglądarki, urządzenia mobilnego lub systemu operacyjnego na komputerze stacjonarnym i nie tylko. -Bitwarden secures more than just passwords -End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! +Bitwarden zabezpiecza nie tylko hasła +Rozwiązania do zarządzania danymi zaszyfrownaymi end-to-end od firmy Bitwarden umożliwiają organizacjom zabezpieczanie wszystkiego, w tym tajemnic programistów i kluczy dostępu. Odwiedź Bitwarden.com, aby dowiedzieć się więcej o Mendżerze Sekretów Bitwarden i Bitwarden Passwordless.dev! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> + <value>W domu, w pracy, lub w ruchu, Bitwarden z łatwością zabezpiecza wszystkie Twoje hasła, passkeys i poufne informacje.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Synchronizacja i dostęp do sejfu z różnych urządzeń</value> diff --git a/apps/browser/store/locales/pt_BR/copy.resx b/apps/browser/store/locales/pt_BR/copy.resx index 8b99c436d0..067f9357b2 100644 --- a/apps/browser/store/locales/pt_BR/copy.resx +++ b/apps/browser/store/locales/pt_BR/copy.resx @@ -118,10 +118,10 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden Password Manager</value> + <value>Gerenciador de Senhas Bitwarden</value> </data> <data name="Summary" xml:space="preserve"> - <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> + <value>Em casa, no trabalho, ou em qualquer lugar, o Bitwarden protege facilmente todas as suas senhas, senhas e informações confidenciais.</value> </data> <data name="Description" xml:space="preserve"> <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! @@ -169,7 +169,7 @@ End-to-end encrypted credential management solutions from Bitwarden empower orga </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> + <value>Em casa, no trabalho, ou em qualquer lugar, o Bitwarden protege facilmente todas as suas senhas, senhas e informações confidenciais.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Sincronize e acesse o seu cofre através de múltiplos dispositivos</value> diff --git a/apps/browser/store/locales/zh_CN/copy.resx b/apps/browser/store/locales/zh_CN/copy.resx index 94543f8f6f..d010cb1a7b 100644 --- a/apps/browser/store/locales/zh_CN/copy.resx +++ b/apps/browser/store/locales/zh_CN/copy.resx @@ -118,58 +118,56 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden Password Manager</value> + <value>Bitwarden 密码管理器</value> </data> <data name="Summary" xml:space="preserve"> - <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> + <value>无论是在家里、工作中还是在外出时,Bitwarden 都可以轻松地保护您的所有密码、通行密钥和敏感信息。</value> </data> <data name="Description" xml:space="preserve"> - <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! + <value>被 PCMag、WIRED、The Verge、CNET、G2 等评为最佳密码管理器! -SECURE YOUR DIGITAL LIFE -Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. +保护您的数字生活 +通过为每个账户生成并保存独特而强大的密码,保护您的数字生活并防范数据泄露。所有内容保存在只有您可以访问的端对端加密的密码库中。 -ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE -Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. +随时随地在任何设备上访问您的数据 +不受任何限制跨无限数量的设备轻松管理、存储、保护和分享不限数量的密码。 -EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE -Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. +每个人都应该拥有的保持在线安全的工具 +使用 Bitwarden 是免费的,没有广告,不会出售数据。Bitwarden 相信每个人都应该拥有保持在线安全的能力。高级计划提供了堆高级功能的访问。 -EMPOWER YOUR TEAMS WITH BITWARDEN -Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. +通过 BITWARDEN 为您的团队提供支持 +团队和企业计划具有专业的商业功能。例如 SSO 集成、自托管、目录集成和 SCIM 配置、全局策略、API 访问、事件日志等。 -Use Bitwarden to secure your workforce and share sensitive information with colleagues. +使用 Bitwarden 保护您的团队,并与同事共享敏感信息。 +选择 Bitwarden 的更多理由: -More reasons to choose Bitwarden: +世界级加密 +密码受到先进的端对端加密(AES-256 位、加盐哈希标签和 PBKDF2 SHA-256)保护,使您的数据保持安全和私密。 -World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. +第三方审计 +Bitwarden 定期与知名的安全公司进行全面的第三方安全审计。这些年度审核包括对 Bitwarden IP、服务器和 Web 应用程序的源代码评估和渗透测试。 -3rd-party Audits -Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. - -Advanced 2FA -Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. +高级两步验证 +使用第三方身份验证器、通过电子邮件发送代码或 FIDO2 WebAuthn 凭据(如硬件安全钥匙或通行密钥)保护您的登录。 Bitwarden Send -Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. +直接传输数据给他人,同时保持端对端加密的安全性并防止曝露。 -Built-in Generator -Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. +内置生成器 +为您访问的每个网站创建长、复杂且独特的密码和用户名。与电子邮件别名提供商集成,增加隐私保护。 -Global Translations -Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. +全球翻译 +Bitwarden 的翻译涵盖 60 多种语言,由全球社区通过 Crowdin 翻译。 -Cross-Platform Applications -Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. +跨平台应用程序 +从任何浏览器、移动设备或桌面操作系统中安全地访问和共享 Bitwarden 密码库中的敏感数据。 -Bitwarden secures more than just passwords -End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! -</value> +Bitwarden 保护的不仅仅是密码 +Bitwarden 的端对端加密凭据管理解决方案使组织能够保护所有内容,包括开发人员机密和通行密钥体验。访问 Bitwarden.com 了解更多关于Bitwarden Secrets Manager 和 Bitwarden Passwordless.dev 的信息!</value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> + <value>无论是在家里、工作中还是在外出时,Bitwarden 都可以轻松地保护您的所有密码、通行密钥和敏感信息。</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>从多台设备同步和访问密码库</value> From 68839a80b7b36c785064fb45e9b7ddd05c502da0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 23 Apr 2024 15:30:01 +0000 Subject: [PATCH 261/351] Autosync the updated translations (#8880) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/web/src/locales/az/messages.json | 6 +- apps/web/src/locales/bg/messages.json | 6 +- apps/web/src/locales/cs/messages.json | 10 +- apps/web/src/locales/da/messages.json | 6 +- apps/web/src/locales/de/messages.json | 82 +++++------ apps/web/src/locales/fr/messages.json | 6 +- apps/web/src/locales/hu/messages.json | 6 +- apps/web/src/locales/ja/messages.json | 6 +- apps/web/src/locales/lv/messages.json | 128 ++++++++--------- apps/web/src/locales/pl/messages.json | 6 +- apps/web/src/locales/pt_BR/messages.json | 170 +++++++++++------------ apps/web/src/locales/ru/messages.json | 6 +- apps/web/src/locales/sk/messages.json | 6 +- apps/web/src/locales/sr/messages.json | 86 ++++++------ apps/web/src/locales/uk/messages.json | 6 +- apps/web/src/locales/zh_CN/messages.json | 58 ++++---- 16 files changed, 297 insertions(+), 297 deletions(-) diff --git a/apps/web/src/locales/az/messages.json b/apps/web/src/locales/az/messages.json index d53278e93a..c3ae7b07b6 100644 --- a/apps/web/src/locales/az/messages.json +++ b/apps/web/src/locales/az/messages.json @@ -8042,12 +8042,12 @@ "message": "Bu kolleksiya yalnız admin konsolundan əlçatandır" }, "organizationOptionsMenu": { - "message": "Toggle Organization Menu" + "message": "Təşkilat Menyusuna keç" }, "vaultItemSelect": { - "message": "Select vault item" + "message": "Anbar elementini seç" }, "collectionItemSelect": { - "message": "Select collection item" + "message": "Kolleksiya elementini seç" } } diff --git a/apps/web/src/locales/bg/messages.json b/apps/web/src/locales/bg/messages.json index f4ee30ba95..e0d0f00174 100644 --- a/apps/web/src/locales/bg/messages.json +++ b/apps/web/src/locales/bg/messages.json @@ -8042,12 +8042,12 @@ "message": "Тази колекция е достъпна само през административната конзола" }, "organizationOptionsMenu": { - "message": "Toggle Organization Menu" + "message": "Превключване на менюто на организацията" }, "vaultItemSelect": { - "message": "Select vault item" + "message": "Изберете елемент от трезора" }, "collectionItemSelect": { - "message": "Select collection item" + "message": "Изберете елемент от колекцията" } } diff --git a/apps/web/src/locales/cs/messages.json b/apps/web/src/locales/cs/messages.json index b6e62aef9a..72e621b1a5 100644 --- a/apps/web/src/locales/cs/messages.json +++ b/apps/web/src/locales/cs/messages.json @@ -8036,18 +8036,18 @@ "message": "Nový klient byl úspěšně vytvořen" }, "noAccess": { - "message": "No access" + "message": "Žádný přístup" }, "collectionAdminConsoleManaged": { - "message": "This collection is only accessible from the admin console" + "message": "Tato kolekce je přístupná pouze z konzole administrátora" }, "organizationOptionsMenu": { - "message": "Toggle Organization Menu" + "message": "Přepnout menu organizace" }, "vaultItemSelect": { - "message": "Select vault item" + "message": "Vybrat položku trezoru" }, "collectionItemSelect": { - "message": "Select collection item" + "message": "Vybrat položku kolekce" } } diff --git a/apps/web/src/locales/da/messages.json b/apps/web/src/locales/da/messages.json index e5042229fe..4c8cbf108a 100644 --- a/apps/web/src/locales/da/messages.json +++ b/apps/web/src/locales/da/messages.json @@ -8042,12 +8042,12 @@ "message": "Denne samling er kun tilgængelig via Admin-konsol" }, "organizationOptionsMenu": { - "message": "Toggle Organization Menu" + "message": "Skift Organisationsmenu" }, "vaultItemSelect": { - "message": "Select vault item" + "message": "Vælg boksemne" }, "collectionItemSelect": { - "message": "Select collection item" + "message": "Vælg samlingsemne" } } diff --git a/apps/web/src/locales/de/messages.json b/apps/web/src/locales/de/messages.json index bd2e0946f6..1c7dd34329 100644 --- a/apps/web/src/locales/de/messages.json +++ b/apps/web/src/locales/de/messages.json @@ -4951,7 +4951,7 @@ "message": "Erstelle eine neue Kunden-Organisation, die dir als Anbieter zugeordnet wird. Du kannst auf diese Organisation zugreifen und diese verwalten." }, "newClient": { - "message": "New client" + "message": "Neuer Kunde" }, "addExistingOrganization": { "message": "Bestehende Organisation hinzufügen" @@ -7607,7 +7607,7 @@ "message": "Anbieterportal" }, "success": { - "message": "Success" + "message": "Erfolg" }, "viewCollection": { "message": "Sammlung anzeigen" @@ -7907,30 +7907,30 @@ "message": "Du kannst dich nicht selbst zu einer Gruppe hinzufügen." }, "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "message": "Hinweis: Ab dem 2. Mai 2024 werden nicht zugewiesene Organisationseinträge nicht mehr geräteübergreifend 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." }, "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + "message": "Hinweis: Nicht zugewiesene Organisationseinträge sind nicht mehr geräteübergreifend in der Ansicht aller Tresore sichtbar und nun nur über die Administrator-Konsole zugänglich." }, "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + "message": "Hinweis: Ab dem 16. Mai 2024 werden nicht zugewiesene Organisationseinträge nicht mehr geräteübergreifend in der Ansicht aller Tresore sichtbar sein und nur über die Administrator-Konsole zugänglich." }, "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", + "message": "Weise diese Einträge einer Sammlung aus der", "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.", + "message": "zu, um sie sichtbar zu machen.", "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." }, "deleteProvider": { - "message": "Delete provider" + "message": "Anbieter löschen" }, "deleteProviderConfirmation": { - "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + "message": "Das Löschen eines Anbieters ist dauerhaft und unwiderruflich. Gib dein Master-Passwort ein, um die Löschung des Anbieters und aller zugehörigen Daten zu bestätigen." }, "deleteProviderName": { - "message": "Cannot delete $ID$", + "message": "Kann $ID$ nicht löschen", "placeholders": { "id": { "content": "$1", @@ -7939,7 +7939,7 @@ } }, "deleteProviderWarningDesc": { - "message": "You must unlink all clients before you can delete $ID$", + "message": "Du musst die Verknüpfung zu allen Kunden aufheben, bevor du $ID$ löschen kannst", "placeholders": { "id": { "content": "$1", @@ -7948,16 +7948,16 @@ } }, "providerDeleted": { - "message": "Provider deleted" + "message": "Anbieter gelöscht" }, "providerDeletedDesc": { - "message": "The Provider and all associated data has been deleted." + "message": "Der Anbieter und alle zugehörigen Daten wurden gelöscht." }, "deleteProviderRecoverConfirmDesc": { - "message": "You have requested to delete this Provider. Use the button below to confirm." + "message": "Du hast die Löschung dieses Anbieters angefragt. Verwende den Button unten, um dies zu bestätigen." }, "deleteProviderWarning": { - "message": "Deleting your provider is permanent. It cannot be undone." + "message": "Die Löschung deines Anbieters ist dauerhaft. Sie kann nicht widerrufen werden." }, "errorAssigningTargetCollection": { "message": "Fehler beim Zuweisen der Ziel-Sammlung." @@ -7966,88 +7966,88 @@ "message": "Fehler beim Zuweisen des Ziel-Ordners." }, "integrationsAndSdks": { - "message": "Integrations & SDKs", + "message": "Integrationen & SDKs", "description": "The title for the section that deals with integrations and SDKs." }, "integrations": { - "message": "Integrations" + "message": "Integrationen" }, "integrationsDesc": { - "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + "message": "Geheimnisse des Bitwarden Secrets Managers automatisch mit einem Drittanbieter-Dienst synchronisieren." }, "sdks": { "message": "SDKs" }, "sdksDesc": { - "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + "message": "Verwende das Bitwarden Secrets Manager SDK in den folgenden Programmiersprachen, um deine eigenen Anwendungen zu erstellen." }, "setUpGithubActions": { - "message": "Set up Github Actions" + "message": "GitHub Actions einrichten" }, "setUpGitlabCICD": { - "message": "Set up GitLab CI/CD" + "message": "GitLab CI/CD einrichten" }, "setUpAnsible": { - "message": "Set up Ansible" + "message": "Ansible einrichten" }, "cSharpSDKRepo": { - "message": "View C# repository" + "message": "C#-Repository anzeigen" }, "cPlusPlusSDKRepo": { - "message": "View C++ repository" + "message": "C++-Repository anzeigen" }, "jsWebAssemblySDKRepo": { - "message": "View JS WebAssembly repository" + "message": "JS WebAssembly-Repository anzeigen" }, "javaSDKRepo": { - "message": "View Java repository" + "message": "Java-Repository anzeigen" }, "pythonSDKRepo": { - "message": "View Python repository" + "message": "Python-Repository anzeigen" }, "phpSDKRepo": { - "message": "View php repository" + "message": "PHP-Repository anzeigen" }, "rubySDKRepo": { - "message": "View Ruby repository" + "message": "Ruby-Repository anzeigen" }, "goSDKRepo": { - "message": "View Go repository" + "message": "Go-Repository anzeigen" }, "createNewClientToManageAsProvider": { - "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + "message": "Erstelle eine neue Kunden-Organisation, um sie als Anbieter zu verwalten. Zusätzliche Benutzerplätze werden im nächsten Abrechnungszeitraum berücksichtigt." }, "selectAPlan": { - "message": "Select a plan" + "message": "Ein Abo auswählen" }, "thirtyFivePercentDiscount": { - "message": "35% Discount" + "message": "35% Rabatt" }, "monthPerMember": { "message": "month per member" }, "seats": { - "message": "Seats" + "message": "Benutzerplätze" }, "addOrganization": { - "message": "Add organization" + "message": "Organisation hinzufügen" }, "createdNewClient": { - "message": "Successfully created new client" + "message": "Neuer Kunde erfolgreich erstellt" }, "noAccess": { - "message": "No access" + "message": "Kein Zugriff" }, "collectionAdminConsoleManaged": { - "message": "This collection is only accessible from the admin console" + "message": "Diese Sammlung ist nur über die Administrator-Konsole zugänglich" }, "organizationOptionsMenu": { - "message": "Toggle Organization Menu" + "message": "Organisationsmenü umschalten" }, "vaultItemSelect": { - "message": "Select vault item" + "message": "Tresor-Eintrag auswählen" }, "collectionItemSelect": { - "message": "Select collection item" + "message": "Sammlungseintrag auswählen" } } diff --git a/apps/web/src/locales/fr/messages.json b/apps/web/src/locales/fr/messages.json index 1f1f343897..ffb1fe436b 100644 --- a/apps/web/src/locales/fr/messages.json +++ b/apps/web/src/locales/fr/messages.json @@ -8042,12 +8042,12 @@ "message": "Cette collection n'est accessible qu'à partir de la Console Admin" }, "organizationOptionsMenu": { - "message": "Toggle Organization Menu" + "message": "Afficher/masquer le Menu de l'Organisation" }, "vaultItemSelect": { - "message": "Select vault item" + "message": "Sélectionner un élément du coffre" }, "collectionItemSelect": { - "message": "Select collection item" + "message": "Sélectionner un élément de la collection" } } diff --git a/apps/web/src/locales/hu/messages.json b/apps/web/src/locales/hu/messages.json index b85733325b..008a20b4f0 100644 --- a/apps/web/src/locales/hu/messages.json +++ b/apps/web/src/locales/hu/messages.json @@ -8042,12 +8042,12 @@ "message": "Ez a gyűjtemény csak az adminisztrátori konzolról érhető el." }, "organizationOptionsMenu": { - "message": "Toggle Organization Menu" + "message": "Szervezeti menü váltás" }, "vaultItemSelect": { - "message": "Select vault item" + "message": "Széf elem választás" }, "collectionItemSelect": { - "message": "Select collection item" + "message": "Gyűjtemény elem választás" } } diff --git a/apps/web/src/locales/ja/messages.json b/apps/web/src/locales/ja/messages.json index a701dd6e8d..1861743320 100644 --- a/apps/web/src/locales/ja/messages.json +++ b/apps/web/src/locales/ja/messages.json @@ -8042,12 +8042,12 @@ "message": "このコレクションは管理コンソールからのみアクセス可能です" }, "organizationOptionsMenu": { - "message": "Toggle Organization Menu" + "message": "組織メニューの切り替え" }, "vaultItemSelect": { - "message": "Select vault item" + "message": "保管庫のアイテムを選択" }, "collectionItemSelect": { - "message": "Select collection item" + "message": "コレクションのアイテムを選択" } } diff --git a/apps/web/src/locales/lv/messages.json b/apps/web/src/locales/lv/messages.json index b20f350231..1ad03eeb82 100644 --- a/apps/web/src/locales/lv/messages.json +++ b/apps/web/src/locales/lv/messages.json @@ -7741,22 +7741,22 @@ "description": "The date header used when a subscription is cancelled." }, "machineAccountsCannotCreate": { - "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + "message": "Mašīnu kontus nav iespējams izveidot apturētās apvienībās. Lūgums vērsties pie savas apvienības īpašnieka pēc palīdzības." }, "machineAccount": { - "message": "Machine account", + "message": "Mašīnas konts", "description": "A machine user which can be used to automate processes and access secrets in the system." }, "machineAccounts": { - "message": "Machine accounts", + "message": "Mašīnu konti", "description": "The title for the section that deals with machine accounts." }, "newMachineAccount": { - "message": "New machine account", + "message": "Jauns mašīnas konts", "description": "Title for creating a new machine account." }, "machineAccountsNoItemsMessage": { - "message": "Create a new machine account to get started automating secret access.", + "message": "Jāizveido jauns mašīnas konts, lai sāktu automatizēt piekļuvi noslēpumiem.", "description": "Message to encourage the user to start creating machine accounts." }, "machineAccountsNoItemsTitle": { @@ -7764,19 +7764,19 @@ "description": "Title to indicate that there are no machine accounts to display." }, "deleteMachineAccounts": { - "message": "Delete machine accounts", + "message": "Izdzēst mašīnu kontus", "description": "Title for the action to delete one or multiple machine accounts." }, "deleteMachineAccount": { - "message": "Delete machine account", + "message": "Izdzēst mašīnas kontu", "description": "Title for the action to delete a single machine account." }, "viewMachineAccount": { - "message": "View machine account", + "message": "Skatīt mašīnas kontu", "description": "Action to view the details of a machine account." }, "deleteMachineAccountDialogMessage": { - "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "message": "Mašīnas konta $MACHINE_ACCOUNT$ izdzēšana ir paliekoša un neatgriezeniska.", "placeholders": { "machine_account": { "content": "$1", @@ -7785,10 +7785,10 @@ } }, "deleteMachineAccountsDialogMessage": { - "message": "Deleting machine accounts is permanent and irreversible." + "message": "Mašīnu kontu izdzēšana ir paliekoša un neatgriezeniska." }, "deleteMachineAccountsConfirmMessage": { - "message": "Delete $COUNT$ machine accounts", + "message": "Izdzēst $COUNT$ mašīnu kontu(s)", "placeholders": { "count": { "content": "$1", @@ -7797,60 +7797,60 @@ } }, "deleteMachineAccountToast": { - "message": "Machine account deleted" + "message": "Mašīnas konts ir izdzēsts" }, "deleteMachineAccountsToast": { - "message": "Machine accounts deleted" + "message": "Mašīnu konti ir izdzēsti" }, "searchMachineAccounts": { - "message": "Search machine accounts", + "message": "Meklēt mašīnu kontus", "description": "Placeholder text for searching machine accounts." }, "editMachineAccount": { - "message": "Edit machine account", + "message": "Labot mašīnas kontu", "description": "Title for editing a machine account." }, "machineAccountName": { - "message": "Machine account name", + "message": "Mašīnas konta nosaukums", "description": "Label for the name of a machine account" }, "machineAccountCreated": { - "message": "Machine account created", + "message": "Mašīnas konts ir izveidots", "description": "Notifies that a new machine account has been created" }, "machineAccountUpdated": { - "message": "Machine account updated", + "message": "Mašīnas konts ir atjaunināts", "description": "Notifies that a machine account has been updated" }, "projectMachineAccountsDescription": { - "message": "Grant machine accounts access to this project." + "message": "Piešķirt mašīnau kontiem piekļuvi šim projektam." }, "projectMachineAccountsSelectHint": { - "message": "Type or select machine accounts" + "message": "Ievadīt vai atlasīt mašīnu kontus" }, "projectEmptyMachineAccountAccessPolicies": { - "message": "Add machine accounts to grant access" + "message": "Jāpievieno mašīnu konti, lai piešķirtu piekļuvi" }, "machineAccountPeopleDescription": { - "message": "Grant groups or people access to this machine account." + "message": "Piešķirt kopām vai cilvēkiem piekļuvi šim mašīnas kontam." }, "machineAccountProjectsDescription": { - "message": "Assign projects to this machine account. " + "message": "Piešķirt projektus šim mašīnas kontam. " }, "createMachineAccount": { - "message": "Create a machine account" + "message": "Izveidot mašīnas kontu" }, "maPeopleWarningMessage": { - "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + "message": "Cilvēku noņemšana no mašīnas konta nenoņem to izveidotās piekļuves pilnvaras. Labākajai drošības pieejai ir ieteicams atsaukt piekļuves pilnvaras, kuras ir izveidojuši cilvēki, kuri ir noņemti no mašīnas konta." }, "smAccessRemovalWarningMaTitle": { - "message": "Remove access to this machine account" + "message": "Noņemt piekļuvi šim mašīnas kontam" }, "smAccessRemovalWarningMaMessage": { - "message": "This action will remove your access to the machine account." + "message": "Šī darbība noņems piekļuvi mašīnas kontam." }, "machineAccountsIncluded": { - "message": "$COUNT$ machine accounts included", + "message": "Iekļauti $COUNT$ mašīnu konti", "placeholders": { "count": { "content": "$1", @@ -7859,7 +7859,7 @@ } }, "additionalMachineAccountCost": { - "message": "$COST$ per month for additional machine accounts", + "message": "$COST$ mēnesī par papildu mašīnu kontiem", "placeholders": { "cost": { "content": "$1", @@ -7868,10 +7868,10 @@ } }, "additionalMachineAccounts": { - "message": "Additional machine accounts" + "message": "Papildu mašīnu konti" }, "includedMachineAccounts": { - "message": "Your plan comes with $COUNT$ machine accounts.", + "message": "Plānā ir iekļauti $COUNT$ mašīnu konti.", "placeholders": { "count": { "content": "$1", @@ -7880,7 +7880,7 @@ } }, "addAdditionalMachineAccounts": { - "message": "You can add additional machine accounts for $COST$ per month.", + "message": "Papildu mašīnu kontus var pievienot par $COST$ mēnesī.", "placeholders": { "cost": { "content": "$1", @@ -7889,25 +7889,25 @@ } }, "limitMachineAccounts": { - "message": "Limit machine accounts (optional)" + "message": "Ierobežot mašīnu kontus (izvēles)" }, "limitMachineAccountsDesc": { - "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + "message": "Uzstāda mašīnu kontu skaita ierobežojumu. Tiklīdz tas ir sasniegts, nebūs iespējams izveidot jaunus mašīnu kontus." }, "machineAccountLimit": { - "message": "Machine account limit (optional)" + "message": "Mašīnu kontu skaita ierobežojums (izvēles)" }, "maxMachineAccountCost": { - "message": "Max potential machine account cost" + "message": "Lielākās iespējamās mašīnas konta izmaksas" }, "machineAccountAccessUpdated": { - "message": "Machine account access updated" + "message": "Mašīnas konta piekļuve ir atjaunināta" }, "restrictedGroupAccessDesc": { "message": "Sevi nevar pievienot kopai." }, "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "message": "Jāņem vērā: no 2024. gada 2. maija nepiešķirti apvienības vienumi vairs nebūs redzami skatā \"Visas glabātavas\" dažādās ierīcēs un būs sasniedzami tikai no pārvaldības konsoles, kur šie vienumi jāpiešķir krājumam, lai padarītu tos redzamus." }, "unassignedItemsBannerNotice": { "message": "Jāņem vērā: nepiešķirti apvienības vienumi vairs nav redzami skatā \"Visas glabātavas\" dažādās ierīcēs un tagad ir pieejami tikai pārvaldības konsolē." @@ -7966,74 +7966,74 @@ "message": "Kļūda mērķa mapes piešķiršanā." }, "integrationsAndSdks": { - "message": "Integrations & SDKs", + "message": "Integrācijas un izstrādātāju rīkkopas", "description": "The title for the section that deals with integrations and SDKs." }, "integrations": { - "message": "Integrations" + "message": "Integrācijas" }, "integrationsDesc": { - "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + "message": "Automātiski sinhronizēt noslēpumus no Bitwarden Noslēpumu pārvaldnieka uz trešās puses pakalpojumu." }, "sdks": { - "message": "SDKs" + "message": "Izstrādātāju rīkkopas" }, "sdksDesc": { - "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + "message": "Bitwarden Noslēpumu pārvaldnieka izstrādātāju rīkkopa ir izmantojama ar zemāk esošajām programmēšanas valodām, lai veidotu pats savas lietotnes." }, "setUpGithubActions": { - "message": "Set up Github Actions" + "message": "Iestatīt GitHub darbības" }, "setUpGitlabCICD": { - "message": "Set up GitLab CI/CD" + "message": "Iestatīt GitLab CI/CD" }, "setUpAnsible": { - "message": "Set up Ansible" + "message": "Iestatīt Ansible" }, "cSharpSDKRepo": { - "message": "View C# repository" + "message": "Skatīt C# glabātavu" }, "cPlusPlusSDKRepo": { - "message": "View C++ repository" + "message": "Skatīt C++ glabātavu" }, "jsWebAssemblySDKRepo": { - "message": "View JS WebAssembly repository" + "message": "Skatīt JS WebAssembly glabātavu" }, "javaSDKRepo": { - "message": "View Java repository" + "message": "Skatīt Java glabātavu" }, "pythonSDKRepo": { - "message": "View Python repository" + "message": "Skatīt Python glabātavu" }, "phpSDKRepo": { - "message": "View php repository" + "message": "Skatīt PHP glabātavu" }, "rubySDKRepo": { - "message": "View Ruby repository" + "message": "Skatīt Ruby glabātavu" }, "goSDKRepo": { - "message": "View Go repository" + "message": "Skatīt Go glabātavu" }, "createNewClientToManageAsProvider": { - "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + "message": "Izveidot jaunu klienta apvienību, ko pārvaldīt kā nodrošinātājam. Papildu vietas tiks atspoguļotas nākamajā norēķinu posmā." }, "selectAPlan": { - "message": "Select a plan" + "message": "Atlasīt plānu" }, "thirtyFivePercentDiscount": { - "message": "35% Discount" + "message": "35% atlaide" }, "monthPerMember": { - "message": "month per member" + "message": "mēnesī par dalībnieku" }, "seats": { - "message": "Seats" + "message": "Vietas" }, "addOrganization": { - "message": "Add organization" + "message": "Pievienot apvienību" }, "createdNewClient": { - "message": "Successfully created new client" + "message": "Veiksmīgi izveidots jauns klients" }, "noAccess": { "message": "Nav piekļuves" @@ -8042,12 +8042,12 @@ "message": "Šis krājums ir pieejams tikai pārvaldības konsolē" }, "organizationOptionsMenu": { - "message": "Toggle Organization Menu" + "message": "Pārslēgt apvienību izvēlni" }, "vaultItemSelect": { - "message": "Select vault item" + "message": "Atlasīt glabātavas vienumu" }, "collectionItemSelect": { - "message": "Select collection item" + "message": "Atlasīt krājuma vienumu" } } diff --git a/apps/web/src/locales/pl/messages.json b/apps/web/src/locales/pl/messages.json index c54b72c21c..8fc603c1d5 100644 --- a/apps/web/src/locales/pl/messages.json +++ b/apps/web/src/locales/pl/messages.json @@ -4053,7 +4053,7 @@ "message": "Wszystkie funkcje zespołów oraz:" }, "includeAllTeamsStarterFeatures": { - "message": "All Teams Starter features, plus:" + "message": "Wszystkie funkcje z Teams Starter, plus:" }, "chooseMonthlyOrAnnualBilling": { "message": "Wybierz miesięczną lub roczną płatność" @@ -6737,7 +6737,7 @@ } }, "teamsStarterPlanInvLimitReachedManageBilling": { - "message": "Teams Starter plans may have up to $SEATCOUNT$ members. Upgrade to your plan to invite more members.", + "message": "Plan Teams Starter może mieć maksymalnie $SEATCOUNT$ członków. Przejdź na wyższy plan, aby zaprosić więcej członków.", "placeholders": { "seatcount": { "content": "$1", @@ -6746,7 +6746,7 @@ } }, "teamsStarterPlanInvLimitReachedNoManageBilling": { - "message": "Teams Starter plans may have up to $SEATCOUNT$ members. Contact your organization owner to upgrade your plan and invite more members.", + "message": "Plan Teams Starter może mieć maksymalnie $SEATCOUNT$ członków. Skontaktuj się z właścicielem organizacji, aby przejść na wyższy plan i zaprosić więcej członków.", "placeholders": { "seatcount": { "content": "$1", diff --git a/apps/web/src/locales/pt_BR/messages.json b/apps/web/src/locales/pt_BR/messages.json index 2b296694bb..06b117ed06 100644 --- a/apps/web/src/locales/pt_BR/messages.json +++ b/apps/web/src/locales/pt_BR/messages.json @@ -627,10 +627,10 @@ "message": "Iniciar sessão com a chave de acesso" }, "invalidPasskeyPleaseTryAgain": { - "message": "Invalid Passkey. Please try again." + "message": "Senha inválida. Por favor, tente novamente." }, "twoFactorForPasskeysNotSupportedOnClientUpdateToLogIn": { - "message": "2FA for passkeys is not supported. Update the app to log in." + "message": "2FA para senhas não é suportada. Atualize o aplicativo para iniciar a sessão." }, "loginWithPasskeyInfo": { "message": "Use uma senha gerada que fará o login automaticamente sem uma senha. Biometrias como reconhecimento facial ou impressão digital, ou outro método de segurança FIDO2 verificarão sua identidade." @@ -1359,11 +1359,11 @@ "description": "This will be part of a larger sentence, that will read like this: If you don't have any data to import, you can create a new item instead. (Optional second half: You may need to wait until your administrator confirms your organization membership.)" }, "onboardingImportDataDetailsPartTwoNoOrgs": { - "message": " instead.", + "message": " em vez disso.", "description": "This will be part of a larger sentence, that will read like this: If you don't have any data to import, you can create a new item instead." }, "onboardingImportDataDetailsPartTwoWithOrgs": { - "message": " instead. You may need to wait until your administrator confirms your organization membership.", + "message": " em vez disso, você pode precisar esperar até que o seu administrador confirme a sua associação à organização.", "description": "This will be part of a larger sentence, that will read like this: If you don't have any data to import, you can create a new item instead. You may need to wait until your administrator confirms your organization membership." }, "importError": { @@ -2802,10 +2802,10 @@ "message": "CLI" }, "bitWebVault": { - "message": "Bitwarden Web vault" + "message": "Cofre Web do Bitwarden" }, "bitSecretsManager": { - "message": "Bitwarden Secrets Manager" + "message": "Gerenciador de Segredos Bitwarden" }, "loggedIn": { "message": "Conectado(a)." @@ -3489,7 +3489,7 @@ "message": "Defina um limite de vagas para sua assinatura. Quando esse limite for atingido, você não poderá convidar novos usuários." }, "limitSmSubscriptionDesc": { - "message": "Set a seat limit for your Secrets Manager subscription. Once this limit is reached, you will not be able to invite new members." + "message": "Defina um limite de assento para sua assinatura do Gerenciador Secretos. Uma vez que este limite for atingido, você não poderá convidar novos membros." }, "maxSeatLimit": { "message": "Limite Máximo de Vaga (opcional)", @@ -3621,7 +3621,7 @@ "message": "Atualizar Chave de Criptografia" }, "updateEncryptionSchemeDesc": { - "message": "We've changed the encryption scheme to provide better security. Update your encryption key now by entering your master password below." + "message": "Alteramos o esquema de criptografia para fornecer melhor segurança. Atualize sua chave de criptografia agora digitando sua senha mestra abaixo." }, "updateEncryptionKeyWarning": { "message": "Depois de atualizar sua chave de criptografia, é necessário encerrar e iniciar a sessão em todos os aplicativos do Bitwarden que você está usando atualmente (como o aplicativo móvel ou as extensões do navegador). Não encerrar e iniciar sessão (que baixa sua nova chave de criptografia) pode resultar em corrupção de dados. Nós tentaremos desconectá-lo automaticamente, mas isso pode demorar um pouco." @@ -3678,7 +3678,7 @@ "message": "Escolha quando o tempo limite do seu cofre irá se esgotar e execute a ação selecionada." }, "vaultTimeoutLogoutDesc": { - "message": "Choose when your vault will be logged out." + "message": "Escolha quando seu cofre será desconectado." }, "oneMinute": { "message": "1 minuto" @@ -4083,7 +4083,7 @@ "message": "Identificador SSO" }, "ssoIdentifierHintPartOne": { - "message": "Provide this ID to your members to login with SSO. To bypass this step, set up ", + "message": "Forneça esse ID aos seus membros para logar com SSO. Para ignorar essa etapa, configure ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" }, "unlinkSso": { @@ -4695,7 +4695,7 @@ "message": "Inscrito na recuperação de conta" }, "withdrawAccountRecovery": { - "message": "Withdraw from account recovery" + "message": "Retirar da recuperação de conta" }, "enrollPasswordResetSuccess": { "message": "Inscrição com sucesso!" @@ -4704,7 +4704,7 @@ "message": "Retirada com sucesso!" }, "eventEnrollAccountRecovery": { - "message": "User $ID$ enrolled in account recovery.", + "message": "O usuário $ID$ se inscreveu na recuperação de conta.", "placeholders": { "id": { "content": "$1", @@ -4713,7 +4713,7 @@ } }, "eventWithdrawAccountRecovery": { - "message": "User $ID$ withdrew from account recovery.", + "message": "O usuário $ID$ retirou da recuperação de conta.", "placeholders": { "id": { "content": "$1", @@ -4887,7 +4887,7 @@ "message": "Erro" }, "accountRecoveryManageUsers": { - "message": "Manage users must also be granted with the manage account recovery permission" + "message": "Gerenciar usuários também devem ser concedidos com a permissão de gerenciar a recuperação de contas" }, "setupProvider": { "message": "Configuração do Provedor" @@ -5117,7 +5117,7 @@ "message": "Ativar preenchimento automático" }, "activateAutofillPolicyDesc": { - "message": "Activate the auto-fill on page load setting on the browser extension for all existing and new members." + "message": "Ative o autopreenchimento na configuração de carregamento de página na extensão do navegador para todos os membros existentes e novos." }, "experimentalFeature": { "message": "Sites comprometidos ou não confiáveis podem tomar vantagem do autopreenchimento ao carregar a página." @@ -5432,7 +5432,7 @@ "message": "Conector de Chave" }, "memberDecryptionKeyConnectorDescStart": { - "message": "Connect login with SSO to your self-hosted decryption key server. Using this option, members won’t need to use their master passwords to decrypt vault data. The", + "message": "Conecte o login com SSO ao seu servidor de chave de descriptografia auto-hospedado. Usando essa opção, os membros não precisarão usar suas senhas mestres para descriptografar os dados", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Connect login with SSO to your self-hosted decryption key server. Using this option, members won’t need to use their master passwords to decrypt vault data. The require SSO authentication and single organization policies are required to set up Key Connector decryption. Contact Bitwarden Support for set up assistance.'" }, "memberDecryptionKeyConnectorDescLink": { @@ -5440,7 +5440,7 @@ "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Connect login with SSO to your self-hosted decryption key server. Using this option, members won’t need to use their master passwords to decrypt vault data. The require SSO authentication and single organization policies are required to set up Key Connector decryption. Contact Bitwarden Support for set up assistance.'" }, "memberDecryptionKeyConnectorDescEnd": { - "message": "are required to set up Key Connector decryption. Contact Bitwarden Support for set up assistance.", + "message": "são necessários para configurar a descriptografia do conector chave. Contacte o suporte do Bitwarden para configurar a assistência.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Connect login with SSO to your self-hosted decryption key server. Using this option, members won’t need to use their master passwords to decrypt vault data. The require SSO authentication and single organization policies are required to set up Key Connector decryption. Contact Bitwarden Support for set up assistance.'" }, "keyConnectorPolicyRestriction": { @@ -5637,7 +5637,7 @@ } }, "exportingOrganizationVaultDesc": { - "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "message": "Apenas o cofre da organização associado com $ORGANIZATION$ será exportado. Itens do cofre pessoal e itens de outras organizações não serão incluídos.", "placeholders": { "organization": { "content": "$1", @@ -6707,10 +6707,10 @@ "message": "Convidar membro" }, "needsConfirmation": { - "message": "Needs confirmation" + "message": "Precisa de confirmação" }, "memberRole": { - "message": "Member role" + "message": "Função de membro" }, "moreFromBitwarden": { "message": "Mais do Bitwarden" @@ -6719,7 +6719,7 @@ "message": "Trocar Produtos" }, "freeOrgInvLimitReachedManageBilling": { - "message": "Free organizations may have up to $SEATCOUNT$ members. Upgrade to a paid plan to invite more members.", + "message": "Organizações gratuitas podem ter até $SEATCOUNT$ membros. Faça upgrade para um plano pago para convidar mais membros.", "placeholders": { "seatcount": { "content": "$1", @@ -6728,7 +6728,7 @@ } }, "freeOrgInvLimitReachedNoManageBilling": { - "message": "Free organizations may have up to $SEATCOUNT$ members. Contact your organization owner to upgrade.", + "message": "Organizações gratuitas podem ter até $SEATCOUNT$ membros. Entre em contato com o proprietário da sua organização para fazer upgrade.", "placeholders": { "seatcount": { "content": "$1", @@ -6737,7 +6737,7 @@ } }, "teamsStarterPlanInvLimitReachedManageBilling": { - "message": "Teams Starter plans may have up to $SEATCOUNT$ members. Upgrade to your plan to invite more members.", + "message": "Planos de Times Starter podem ter até $SEATCOUNT$ membros. Atualize para o seu plano para convidar mais membros.", "placeholders": { "seatcount": { "content": "$1", @@ -6755,7 +6755,7 @@ } }, "freeOrgMaxCollectionReachedManageBilling": { - "message": "Free organizations may have up to $COLLECTIONCOUNT$ collections. Upgrade to a paid plan to add more collections.", + "message": "Organizações gratuitas podem ter até $COLLECTIONCOUNT$ coleções. Faça o upgrade para um plano pago para adicionar mais coleções.", "placeholders": { "COLLECTIONCOUNT": { "content": "$1", @@ -6764,7 +6764,7 @@ } }, "freeOrgMaxCollectionReachedNoManageBilling": { - "message": "Free organizations may have up to $COLLECTIONCOUNT$ collections. Contact your organization owner to upgrade.", + "message": "Organizações gratuitas podem ter até $COLLECTIONCOUNT$ membros. Entre em contato com o proprietário da sua organização para fazer upgrade.", "placeholders": { "COLLECTIONCOUNT": { "content": "$1", @@ -6779,10 +6779,10 @@ "message": "Exportar dados" }, "exportingOrganizationSecretDataTitle": { - "message": "Exporting Organization Secret Data" + "message": "Exportando dados secretos da organização" }, "exportingOrganizationSecretDataDescription": { - "message": "Only the Secrets Manager data associated with $ORGANIZATION$ will be exported. Items in other products or from other organizations will not be included.", + "message": "Apenas o cofre da organização associado com $ORGANIZATION$ será exportado. Itens do cofre pessoal e itens de outras organizações não serão incluídos.", "placeholders": { "ORGANIZATION": { "content": "$1", @@ -6812,7 +6812,7 @@ "message": "Upload manual" }, "manualUploadDesc": { - "message": "If you do not want to opt into billing sync, manually upload your license here." + "message": "Se você não deseja optar pela sincronização de cobrança, carregue sua licença manualmente aqui." }, "syncLicense": { "message": "Sincronizar licença" @@ -6827,13 +6827,13 @@ "message": "Última sincronização de licença" }, "billingSyncHelp": { - "message": "Billing Sync help" + "message": "Ajuda da Sincronização de faturamento" }, "licensePaidFeaturesHelp": { - "message": "License paid features help" + "message": "Ajuda dos recursos de licença paga" }, "selfHostGracePeriodHelp": { - "message": "After your subscription expires, you have 60 days to apply an updated license file to your organization. Grace period ends $GRACE_PERIOD_END_DATE$.", + "message": "Após a expiração da assinatura, você tem 60 dias para aplicar um arquivo de licença atualizado à sua organização. Fim do período de carência $GRACE_PERIOD_END_DATE$.", "placeholders": { "GRACE_PERIOD_END_DATE": { "content": "$1", @@ -6845,28 +6845,28 @@ "message": "Enviar licença" }, "projectPeopleDescription": { - "message": "Grant groups or people access to this project." + "message": "Conceder acesso a este projeto ou grupos de pessoas." }, "projectPeopleSelectHint": { - "message": "Type or select people or groups" + "message": "Digite ou selecione pessoas ou grupos" }, "projectServiceAccountsDescription": { - "message": "Grant service accounts access to this project." + "message": "Conceder acesso a contas de serviço a este projeto." }, "projectServiceAccountsSelectHint": { - "message": "Type or select service accounts" + "message": "Digite ou selecione contas de serviço" }, "projectEmptyPeopleAccessPolicies": { - "message": "Add people or groups to start collaborating" + "message": "Adicione pessoas ou grupos para começar a colaborar" }, "projectEmptyServiceAccountAccessPolicies": { "message": "Adicione contas de serviço para conceder acesso" }, "serviceAccountPeopleDescription": { - "message": "Grant groups or people access to this service account." + "message": "Conceder acesso a esta conta de serviço a grupos ou pessoas." }, "serviceAccountProjectsDescription": { - "message": "Assign projects to this service account. " + "message": "Atribuir projetos para esta conta de serviço. " }, "serviceAccountEmptyProjectAccesspolicies": { "message": "Adicionar projetos para conceder acesso" @@ -6878,13 +6878,13 @@ "message": "Grupo/Usuário" }, "lowKdfIterations": { - "message": "Low KDF Iterations" + "message": "Iterações KDF baixas" }, "updateLowKdfIterationsDesc": { - "message": "Update your encryption settings to meet new security recommendations and improve account protection." + "message": "Atualize suas configurações de criptografia para atender às novas recomendações de segurança e melhorar a proteção da conta." }, "changeKdfLoggedOutWarning": { - "message": "Proceeding will log you out of all active sessions. You will need to log back in and complete two-step login setup. We recommend exporting your vault before changing your encryption settings to prevent data loss." + "message": "O processo desconectará você de todas as sessões ativas. Você precisará iniciar a sessão novamente e concluir a configuração de login em duas etapas. Recomendamos exportar seu cofre antes de alterar suas configurações de criptografia para evitar perda de dados." }, "secretsManager": { "message": "Gerenciador de Segredos" @@ -6924,7 +6924,7 @@ "message": "Ocorreu um erro ao tentar ler o arquivo de importação" }, "accessedSecret": { - "message": "Accessed secret $SECRET_ID$.", + "message": "$SECRET_ID$ secreto acessado.", "placeholders": { "secret_id": { "content": "$1", @@ -6996,10 +6996,10 @@ "message": "Seleção é necessária." }, "saPeopleWarningTitle": { - "message": "Access tokens still available" + "message": "Tokens de acesso ainda disponíveis" }, "saPeopleWarningMessage": { - "message": "Removing people from a service account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a service account." + "message": "Remover pessoas de uma conta de serviço não remove os tokens de acesso que criaram. Para a melhor prática de segurança, é recomendável revogar os tokens de acesso criados por pessoas removidas de uma conta de serviço." }, "smAccessRemovalWarningProjectTitle": { "message": "Remover acesso para esse projeto" @@ -7008,10 +7008,10 @@ "message": "Esta ação removerá seu acesso ao projeto." }, "smAccessRemovalWarningSaTitle": { - "message": "Remove access to this service account" + "message": "Remover acesso a essa conta de serviço" }, "smAccessRemovalWarningSaMessage": { - "message": "This action will remove your access to the service account." + "message": "Essa ação removerá seu acesso à conta de serviço." }, "removeAccess": { "message": "Remover acesso" @@ -7023,16 +7023,16 @@ "message": "Senha mestre exposta" }, "exposedMasterPasswordDesc": { - "message": "Password found in a data breach. Use a unique password to protect your account. Are you sure you want to use an exposed password?" + "message": "A senha foi encontrada em violação de dados. Use uma senha única para proteger sua conta. Tem certeza de que deseja usar uma senha exposta?" }, "weakAndExposedMasterPassword": { - "message": "Weak and Exposed Master Password" + "message": "Senha Mestra Fraca e Exposta" }, "weakAndBreachedMasterPasswordDesc": { - "message": "Weak password identified and found in a data breach. Use a strong and unique password to protect your account. Are you sure you want to use this password?" + "message": "Senha fraca identificada e encontrada em um vazamento de dados. Use uma senha forte e única para proteger a sua conta. Tem certeza de que deseja usar essa senha?" }, "characterMinimum": { - "message": "$LENGTH$ character minimum", + "message": "Mínimo de $LENGTH$ caracteres", "placeholders": { "length": { "content": "$1", @@ -7041,7 +7041,7 @@ } }, "masterPasswordMinimumlength": { - "message": "Master password must be at least $LENGTH$ characters long.", + "message": "A senha mestra deve ter pelo menos $LENGTH$ caracteres.", "placeholders": { "length": { "content": "$1", @@ -7057,14 +7057,14 @@ "message": "Descartar" }, "notAvailableForFreeOrganization": { - "message": "This feature is not available for free organizations. Contact your organization owner to upgrade." + "message": "Este recurso não está disponível para organizações gratuitas. Entre em contato com o seu proprietário para atualizar." }, "smProjectSecretsNoItemsNoAccess": { - "message": "Contact your organization's admin to manage secrets for this project.", + "message": "Entre em contato com o administrador da sua organização para gerenciar segredos para este projeto.", "description": "The message shown to the user under a project's secrets tab when the user only has read access to the project." }, "enforceOnLoginDesc": { - "message": "Require existing members to change their passwords" + "message": "Exigir que os membros existentes alterem suas senhas" }, "smProjectDeleteAccessRestricted": { "message": "Você não tem permissão para excluir este projeto", @@ -7081,16 +7081,16 @@ "message": "Sessão iniciada" }, "deviceApprovalRequired": { - "message": "Device approval required. Select an approval option below:" + "message": "Aprovação do dispositivo necessária. Selecione uma opção de aprovação abaixo:" }, "rememberThisDevice": { "message": "Lembrar deste dispositivo" }, "uncheckIfPublicDevice": { - "message": "Uncheck if using a public device" + "message": "Desmarque se estiver usando um dispositivo público" }, "approveFromYourOtherDevice": { - "message": "Approve from your other device" + "message": "Aprovar do seu outro dispositivo" }, "requestAdminApproval": { "message": "Solicitar aprovação do administrador" @@ -7099,17 +7099,17 @@ "message": "Aprovar com a senha mestre" }, "trustedDeviceEncryption": { - "message": "Trusted device encryption" + "message": "Criptografia de dispositivo confiável" }, "trustedDevices": { "message": "Dispositivos confiáveis" }, "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Once authenticated, members will decrypt vault data using a key stored on their device. The", + "message": "Uma vez autenticados, os membros descriptografarão os dados do cofre usando uma chave armazenada no seu dispositivo", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" }, "memberDecryptionOptionTdeDescriptionLinkOne": { - "message": "single organization", + "message": "organização única", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" }, "memberDecryptionOptionTdeDescriptionPartTwo": { @@ -7117,7 +7117,7 @@ "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" }, "memberDecryptionOptionTdeDescriptionLinkTwo": { - "message": "SSO required", + "message": "Necessário SSO", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" }, "memberDecryptionOptionTdeDescriptionPartThree": { @@ -7125,11 +7125,11 @@ "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" }, "memberDecryptionOptionTdeDescriptionLinkThree": { - "message": "account recovery administration", + "message": "gerenciar recuperação de conta", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" }, "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "policy with automatic enrollment will turn on when this option is used.", + "message": "política com inscrição automática será ativada quando esta opção for utilizada.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { @@ -7157,7 +7157,7 @@ "message": "Recuperar conta" }, "updatedTempPassword": { - "message": "User updated a password issued through account recovery." + "message": "O usuário atualizou uma senha emitida através da recuperação de conta." }, "activatedAccessToSecretsManager": { "message": "Acesso ativado ao Gerenciador de Segredos", @@ -7232,7 +7232,7 @@ "message": "Remover membros que não têm senhas mestres sem definir uma para eles pode restringir o acesso à sua conta completa." }, "approvedAuthRequest": { - "message": "Approved device for $ID$.", + "message": "Dispositivo aprovado para $ID$.", "placeholders": { "id": { "content": "$1", @@ -7241,7 +7241,7 @@ } }, "rejectedAuthRequest": { - "message": "Denied device for $ID$.", + "message": "Dispositivo negado para $ID$.", "placeholders": { "id": { "content": "$1", @@ -7250,7 +7250,7 @@ } }, "requestedDeviceApproval": { - "message": "Requested device approval." + "message": "Aprovação do dispositivo solicitada." }, "startYour7DayFreeTrialOfBitwardenFor": { "message": "Comece o seu período de teste gratuito de 7 dias do Bitwarden para $ORG$", @@ -7262,7 +7262,7 @@ } }, "startYour7DayFreeTrialOfBitwardenSecretsManagerFor": { - "message": "Start your 7-Day free trial of Bitwarden Secrets Manager for $ORG$", + "message": "Inicie o seu período de teste gratuito de 7 dias do Bitwarden Secrets Manager para $ORG$", "placeholders": { "org": { "content": "$1", @@ -7280,19 +7280,19 @@ "message": "Sinalização de região selecionada" }, "accountSuccessfullyCreated": { - "message": "Account successfully created!" + "message": "Conta criada com sucesso!" }, "adminApprovalRequested": { - "message": "Admin approval requested" + "message": "Aprovação do administrador necessária" }, "adminApprovalRequestSentToAdmins": { - "message": "Your request has been sent to your admin." + "message": "Seu pedido foi enviado para seu administrador." }, "youWillBeNotifiedOnceApproved": { - "message": "You will be notified once approved." + "message": "Será notificado assim que for aprovado." }, "troubleLoggingIn": { - "message": "Trouble logging in?" + "message": "Problemas em efetuar login?" }, "loginApproved": { "message": "Sessão aprovada" @@ -7324,10 +7324,10 @@ } }, "secretsManagerForPlanDesc": { - "message": "For engineering and DevOps teams to manage secrets throughout the software development lifecycle." + "message": "Para equipes de engenharia e DevOps gerenciar segredos durante todo o ciclo de vida do desenvolvimento de software." }, "free2PersonOrganization": { - "message": "Free 2-person Organizations" + "message": "Organizações gratuitas com 2 pessoas" }, "unlimitedSecrets": { "message": "Segredos ilimitados" @@ -7366,7 +7366,7 @@ "message": "Assine o Gerenciador de Segredos" }, "addSecretsManagerUpgradeDesc": { - "message": "Add Secrets Manager to your upgraded plan to maintain access to any secrets created with your previous plan." + "message": "Adicione o Gerenciador de Segredos ao seu plano atualizado para manter o acesso a todos os segredos criados com seu plano anterior." }, "additionalServiceAccounts": { "message": "Contas de serviço adicionais" @@ -7420,13 +7420,13 @@ "message": "Limitar contas de serviço (opcional)" }, "limitServiceAccountsDesc": { - "message": "Set a limit for your service accounts. Once this limit is reached, you will not be able to create new service accounts." + "message": "Defina um limite para suas contas de máquina. Quando este limite for atingido, você não poderá criar novas contas de máquina." }, "serviceAccountLimit": { "message": "Limite de contas de serviço (opcional)" }, "maxServiceAccountCost": { - "message": "Max potential service account cost" + "message": "Custo máximo da conta de serviço potencial" }, "loggedInExclamation": { "message": "Conectado!" @@ -7459,10 +7459,10 @@ "message": "Chave de acesso" }, "passkeyNotCopied": { - "message": "Passkey will not be copied" + "message": "A senha não será copiada" }, "passkeyNotCopiedAlert": { - "message": "The passkey will not be copied to the cloned item. Do you want to continue cloning this item?" + "message": "A senha não será copiada para o item clonado. Deseja continuar clonando este item?" }, "modifiedCollectionManagement": { "message": "Definição de gerenciamento da coleção $ID$ modificada.", @@ -7484,7 +7484,7 @@ "message": "Use a extensão para salvar rapidamente credenciais e formulários de autopreenchimento sem abrir o aplicativo web." }, "projectAccessUpdated": { - "message": "Project access updated" + "message": "Acesso ao projeto atualizado" }, "unexpectedErrorSend": { "message": "Ocorreu um erro inesperado ao carregar este Envio. Tente novamente mais tarde." @@ -7505,7 +7505,7 @@ "message": "Você não tem acesso para gerenciar esta coleção." }, "grantCollectionAccess": { - "message": "Grant groups or members access to this collection." + "message": "Conceder acesso de grupos ou membros a esta coleção." }, "grantCollectionAccessMembersOnly": { "message": "Conceder acesso a essa coleção." @@ -8042,12 +8042,12 @@ "message": "This collection is only accessible from the admin console" }, "organizationOptionsMenu": { - "message": "Toggle Organization Menu" + "message": "Alternar Menu da Organização" }, "vaultItemSelect": { - "message": "Select vault item" + "message": "Selecionar item do cofre" }, "collectionItemSelect": { - "message": "Select collection item" + "message": "Selecionar item da coleção" } } diff --git a/apps/web/src/locales/ru/messages.json b/apps/web/src/locales/ru/messages.json index 67b23b0e89..213a9d1965 100644 --- a/apps/web/src/locales/ru/messages.json +++ b/apps/web/src/locales/ru/messages.json @@ -8042,12 +8042,12 @@ "message": "Эта коллекция доступна только из консоли администратора" }, "organizationOptionsMenu": { - "message": "Toggle Organization Menu" + "message": "Переключить меню организации" }, "vaultItemSelect": { - "message": "Select vault item" + "message": "Выбрать элемент хранилища" }, "collectionItemSelect": { - "message": "Select collection item" + "message": "Выбрать элемент коллекции" } } diff --git a/apps/web/src/locales/sk/messages.json b/apps/web/src/locales/sk/messages.json index 756723129f..652b1b08fd 100644 --- a/apps/web/src/locales/sk/messages.json +++ b/apps/web/src/locales/sk/messages.json @@ -8042,12 +8042,12 @@ "message": "Táto zbierka je dostupná iba z administrátorskej konzoly" }, "organizationOptionsMenu": { - "message": "Toggle Organization Menu" + "message": "Prepnúť menu organizácie" }, "vaultItemSelect": { - "message": "Select vault item" + "message": "Vyberte položku z trezora" }, "collectionItemSelect": { - "message": "Select collection item" + "message": "Vyberte položku zo zbierky" } } diff --git a/apps/web/src/locales/sr/messages.json b/apps/web/src/locales/sr/messages.json index f927113fad..de563afe50 100644 --- a/apps/web/src/locales/sr/messages.json +++ b/apps/web/src/locales/sr/messages.json @@ -7607,7 +7607,7 @@ "message": "Портал провајдера" }, "success": { - "message": "Success" + "message": "Успех" }, "viewCollection": { "message": "Преглед колекције" @@ -7904,16 +7904,16 @@ "message": "Приступ налога машине ажуриран" }, "restrictedGroupAccessDesc": { - "message": "You cannot add yourself to a group." + "message": "Не можете да се додате у групу." }, "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "message": "Обавештење: 2. маја 2024. недодељене ставке организације више неће бити видљиве у вашем приказу Сви сефови на свим уређајима и биће им доступне само преко Админ конзоле. Доделите ове ставке колекцији са Админ конзолом да бисте их учинили видљивим." }, "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + "message": "Напомена: Недодељене ставке организације више нису видљиве у вашем приказу Сви сефови на свим уређајима и сада су доступне само преко Админ конзоле." }, "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + "message": "Обавештење: 16. маја 2024. недодељене ставке организације више неће бити видљиве у вашем приказу Сви сефови на свим уређајима и биће им доступне само преко Админ конзоле." }, "unassignedItemsBannerCTAPartOne": { "message": "Assign these items to a collection from the", @@ -7924,13 +7924,13 @@ "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." }, "deleteProvider": { - "message": "Delete provider" + "message": "Избриши провајдера" }, "deleteProviderConfirmation": { - "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + "message": "Брисање провајдера је трајно и неповратно. Унесите своју главну лозинку да бисте потврдили брисање провајдера и свих повезаних података." }, "deleteProviderName": { - "message": "Cannot delete $ID$", + "message": "Не може да се избрише $ID$", "placeholders": { "id": { "content": "$1", @@ -7939,7 +7939,7 @@ } }, "deleteProviderWarningDesc": { - "message": "You must unlink all clients before you can delete $ID$", + "message": "Морате прекинути везу са свим клијентима да бисте могли да избришете $ID$", "placeholders": { "id": { "content": "$1", @@ -7948,106 +7948,106 @@ } }, "providerDeleted": { - "message": "Provider deleted" + "message": "Провајдер је избрисан" }, "providerDeletedDesc": { - "message": "The Provider and all associated data has been deleted." + "message": "Провајдер и сви повезани подаци су избрисани." }, "deleteProviderRecoverConfirmDesc": { - "message": "You have requested to delete this Provider. Use the button below to confirm." + "message": "Захтевали сте брисање овог провајдера. Користите дугме испод да потврдите." }, "deleteProviderWarning": { - "message": "Deleting your provider is permanent. It cannot be undone." + "message": "Брисање провајдера је трајно. Не може се поништити." }, "errorAssigningTargetCollection": { - "message": "Error assigning target collection." + "message": "Грешка при додељивању циљне колекције." }, "errorAssigningTargetFolder": { - "message": "Error assigning target folder." + "message": "Грешка при додељивању циљне фасцикле." }, "integrationsAndSdks": { - "message": "Integrations & SDKs", + "message": "Интеграције & SDK", "description": "The title for the section that deals with integrations and SDKs." }, "integrations": { - "message": "Integrations" + "message": "Интеграције" }, "integrationsDesc": { - "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + "message": "Аутоматски синхронизујте тајне од Bitwarden Secrets Manager са сервисима треће стране." }, "sdks": { - "message": "SDKs" + "message": "SDK" }, "sdksDesc": { - "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + "message": "Употребите Bitwarden Secrets Manager SDK на следећим програмским језицима да направите сопствене апликације." }, "setUpGithubActions": { - "message": "Set up Github Actions" + "message": "Подесити акције GitHub-а" }, "setUpGitlabCICD": { - "message": "Set up GitLab CI/CD" + "message": "Подесити GitLab CI/CD" }, "setUpAnsible": { - "message": "Set up Ansible" + "message": "Подесити Ansible" }, "cSharpSDKRepo": { - "message": "View C# repository" + "message": "Преглед C# спремишта" }, "cPlusPlusSDKRepo": { - "message": "View C++ repository" + "message": "Преглед C++ спремишта" }, "jsWebAssemblySDKRepo": { - "message": "View JS WebAssembly repository" + "message": "Преглед JS WebAssembly спремишта" }, "javaSDKRepo": { - "message": "View Java repository" + "message": "Преглед Java спремишта" }, "pythonSDKRepo": { - "message": "View Python repository" + "message": "Преглед Python спремишта" }, "phpSDKRepo": { - "message": "View php repository" + "message": "Преглед php спремишта" }, "rubySDKRepo": { - "message": "View Ruby repository" + "message": "Преглед Ruby спремишта" }, "goSDKRepo": { - "message": "View Go repository" + "message": "Преглед Go спремишта" }, "createNewClientToManageAsProvider": { - "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + "message": "Креирајте нову клијентску организацију којом ћете управљати као добављач. Додатна места ће се одразити у следећем обрачунском циклусу." }, "selectAPlan": { - "message": "Select a plan" + "message": "Изаберите пакет" }, "thirtyFivePercentDiscount": { - "message": "35% Discount" + "message": "Попуст од 35%" }, "monthPerMember": { - "message": "month per member" + "message": "месечно по члану" }, "seats": { - "message": "Seats" + "message": "Места" }, "addOrganization": { - "message": "Add organization" + "message": "Додај организацију" }, "createdNewClient": { - "message": "Successfully created new client" + "message": "Нови клијент је успешно креиран" }, "noAccess": { - "message": "No access" + "message": "Немате приступ" }, "collectionAdminConsoleManaged": { - "message": "This collection is only accessible from the admin console" + "message": "Овој колекцији се може приступити само са администраторске конзоле" }, "organizationOptionsMenu": { - "message": "Toggle Organization Menu" + "message": "Укључи мени Организација" }, "vaultItemSelect": { - "message": "Select vault item" + "message": "Изаберите ставку сефа" }, "collectionItemSelect": { - "message": "Select collection item" + "message": "Изаберите ставку колекције" } } diff --git a/apps/web/src/locales/uk/messages.json b/apps/web/src/locales/uk/messages.json index c9434d72d6..e7b9722c9d 100644 --- a/apps/web/src/locales/uk/messages.json +++ b/apps/web/src/locales/uk/messages.json @@ -8042,12 +8042,12 @@ "message": "Ця збірка доступна тільки з консолі адміністратора" }, "organizationOptionsMenu": { - "message": "Toggle Organization Menu" + "message": "Перемкнути меню організації" }, "vaultItemSelect": { - "message": "Select vault item" + "message": "Вибрати елемент сховища" }, "collectionItemSelect": { - "message": "Select collection item" + "message": "Вибрати елемент збірки" } } diff --git a/apps/web/src/locales/zh_CN/messages.json b/apps/web/src/locales/zh_CN/messages.json index a9a0d85457..292041624c 100644 --- a/apps/web/src/locales/zh_CN/messages.json +++ b/apps/web/src/locales/zh_CN/messages.json @@ -7907,20 +7907,20 @@ "message": "您不能将自己添加到群组。" }, "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "message": "注意:从 2024 年 5 月 2 日起,未分配的组织项目在您所有设备的「所有密码库」视图中将不再可见,只能通过管理控制台访问。通过管理控制台将这些项目分配给集合以使其可见。" }, "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + "message": "注意:未分配的组织项目在您所有设备的「所有密码库」视图中不再可见,现在只能通过管理控制台访问。" }, "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + "message": "注意:从 2024 年 5 月 2 日起,未分配的组织项目在您所有设备的「所有密码库」视图中将不再可见,只能通过管理控制台访问。" }, "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.", + "message": "以使其可见。", "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." }, "deleteProvider": { @@ -7966,88 +7966,88 @@ "message": "分配目标文件夹时出错。" }, "integrationsAndSdks": { - "message": "Integrations & SDKs", + "message": "集成和 SDK", "description": "The title for the section that deals with integrations and SDKs." }, "integrations": { - "message": "Integrations" + "message": "集成" }, "integrationsDesc": { - "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + "message": "通过 Bitwarden 机密管理器将机密自动同步到第三方服务。" }, "sdks": { - "message": "SDKs" + "message": "SDK" }, "sdksDesc": { - "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + "message": "使用以下编程语言的 Bitwarden 机密管理器 SDK 来构建您自己的应用程序。" }, "setUpGithubActions": { - "message": "Set up Github Actions" + "message": "设置 Github Actions" }, "setUpGitlabCICD": { - "message": "Set up GitLab CI/CD" + "message": "设置 GitLab CI/CD" }, "setUpAnsible": { - "message": "Set up Ansible" + "message": "设置 Ansible" }, "cSharpSDKRepo": { - "message": "View C# repository" + "message": "查看 C# 存储库" }, "cPlusPlusSDKRepo": { - "message": "View C++ repository" + "message": "查看 C++ 存储库" }, "jsWebAssemblySDKRepo": { - "message": "View JS WebAssembly repository" + "message": "查看 JS WebAssembly 存储库" }, "javaSDKRepo": { - "message": "View Java repository" + "message": "查看 Java 存储库" }, "pythonSDKRepo": { - "message": "View Python repository" + "message": "查看 Python 存储库" }, "phpSDKRepo": { - "message": "View php repository" + "message": "查看 php 存储库" }, "rubySDKRepo": { - "message": "View Ruby repository" + "message": "查看 Ruby 存储库" }, "goSDKRepo": { - "message": "View Go repository" + "message": "查看 Go 存储库" }, "createNewClientToManageAsProvider": { - "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + "message": "创建一个新的客户组织作为提供商来管理。附加席位将反映在下一个计费周期中。" }, "selectAPlan": { "message": "选择套餐" }, "thirtyFivePercentDiscount": { - "message": "35% Discount" + "message": "35% 折扣" }, "monthPerMember": { "message": "month per member" }, "seats": { - "message": "Seats" + "message": "席位" }, "addOrganization": { - "message": "Add organization" + "message": "添加组织" }, "createdNewClient": { "message": "Successfully created new client" }, "noAccess": { - "message": "No access" + "message": "暂无权限" }, "collectionAdminConsoleManaged": { - "message": "This collection is only accessible from the admin console" + "message": "此集合只能从管理控制台访问" }, "organizationOptionsMenu": { - "message": "Toggle Organization Menu" + "message": "切换组织菜单" }, "vaultItemSelect": { - "message": "Select vault item" + "message": "选择密码库项目" }, "collectionItemSelect": { - "message": "Select collection item" + "message": "选择集合项目" } } From 7f207d25590e3363de5063d5f763977b98d09445 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 23 Apr 2024 15:30:11 +0000 Subject: [PATCH 262/351] Autosync the updated translations (#8879) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/desktop/src/locales/af/messages.json | 3 + apps/desktop/src/locales/ar/messages.json | 3 + apps/desktop/src/locales/az/messages.json | 3 + apps/desktop/src/locales/be/messages.json | 3 + apps/desktop/src/locales/bg/messages.json | 3 + apps/desktop/src/locales/bn/messages.json | 3 + apps/desktop/src/locales/bs/messages.json | 3 + apps/desktop/src/locales/ca/messages.json | 3 + apps/desktop/src/locales/cs/messages.json | 3 + apps/desktop/src/locales/cy/messages.json | 3 + apps/desktop/src/locales/da/messages.json | 3 + apps/desktop/src/locales/de/messages.json | 7 +- apps/desktop/src/locales/el/messages.json | 3 + apps/desktop/src/locales/en_GB/messages.json | 3 + apps/desktop/src/locales/en_IN/messages.json | 3 + apps/desktop/src/locales/eo/messages.json | 3 + apps/desktop/src/locales/es/messages.json | 3 + apps/desktop/src/locales/et/messages.json | 3 + apps/desktop/src/locales/eu/messages.json | 3 + apps/desktop/src/locales/fa/messages.json | 3 + apps/desktop/src/locales/fi/messages.json | 3 + apps/desktop/src/locales/fil/messages.json | 3 + apps/desktop/src/locales/fr/messages.json | 3 + apps/desktop/src/locales/gl/messages.json | 3 + apps/desktop/src/locales/he/messages.json | 3 + apps/desktop/src/locales/hi/messages.json | 3 + apps/desktop/src/locales/hr/messages.json | 3 + apps/desktop/src/locales/hu/messages.json | 3 + apps/desktop/src/locales/id/messages.json | 3 + apps/desktop/src/locales/it/messages.json | 3 + apps/desktop/src/locales/ja/messages.json | 3 + apps/desktop/src/locales/ka/messages.json | 3 + apps/desktop/src/locales/km/messages.json | 3 + apps/desktop/src/locales/kn/messages.json | 3 + apps/desktop/src/locales/ko/messages.json | 3 + apps/desktop/src/locales/lt/messages.json | 3 + apps/desktop/src/locales/lv/messages.json | 3 + apps/desktop/src/locales/me/messages.json | 3 + apps/desktop/src/locales/ml/messages.json | 3 + apps/desktop/src/locales/mr/messages.json | 3 + apps/desktop/src/locales/my/messages.json | 3 + apps/desktop/src/locales/nb/messages.json | 3 + apps/desktop/src/locales/ne/messages.json | 3 + apps/desktop/src/locales/nl/messages.json | 3 + apps/desktop/src/locales/nn/messages.json | 3 + apps/desktop/src/locales/or/messages.json | 3 + apps/desktop/src/locales/pl/messages.json | 3 + apps/desktop/src/locales/pt_BR/messages.json | 73 ++++++++++---------- apps/desktop/src/locales/pt_PT/messages.json | 3 + apps/desktop/src/locales/ro/messages.json | 3 + apps/desktop/src/locales/ru/messages.json | 3 + apps/desktop/src/locales/si/messages.json | 3 + apps/desktop/src/locales/sk/messages.json | 3 + apps/desktop/src/locales/sl/messages.json | 3 + apps/desktop/src/locales/sr/messages.json | 15 ++-- apps/desktop/src/locales/sv/messages.json | 3 + apps/desktop/src/locales/te/messages.json | 3 + apps/desktop/src/locales/th/messages.json | 3 + apps/desktop/src/locales/tr/messages.json | 3 + apps/desktop/src/locales/uk/messages.json | 3 + apps/desktop/src/locales/vi/messages.json | 3 + apps/desktop/src/locales/zh_CN/messages.json | 7 +- apps/desktop/src/locales/zh_TW/messages.json | 3 + 63 files changed, 234 insertions(+), 45 deletions(-) diff --git a/apps/desktop/src/locales/af/messages.json b/apps/desktop/src/locales/af/messages.json index afdfc90d76..97067b788a 100644 --- a/apps/desktop/src/locales/af/messages.json +++ b/apps/desktop/src/locales/af/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/ar/messages.json b/apps/desktop/src/locales/ar/messages.json index 7869b0894b..2d25269fff 100644 --- a/apps/desktop/src/locales/ar/messages.json +++ b/apps/desktop/src/locales/ar/messages.json @@ -2697,6 +2697,9 @@ "message": "تنسيقات مشتركة", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/az/messages.json b/apps/desktop/src/locales/az/messages.json index d4cea4f06e..cf664abf41 100644 --- a/apps/desktop/src/locales/az/messages.json +++ b/apps/desktop/src/locales/az/messages.json @@ -2697,6 +2697,9 @@ "message": "Ortaq formatlar", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Problemlərin aradan qaldırılması" }, diff --git a/apps/desktop/src/locales/be/messages.json b/apps/desktop/src/locales/be/messages.json index 53e3ec2d12..e0133e5a74 100644 --- a/apps/desktop/src/locales/be/messages.json +++ b/apps/desktop/src/locales/be/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/bg/messages.json b/apps/desktop/src/locales/bg/messages.json index f4886c420f..0c5dc25742 100644 --- a/apps/desktop/src/locales/bg/messages.json +++ b/apps/desktop/src/locales/bg/messages.json @@ -2697,6 +2697,9 @@ "message": "Често използвани формати", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Отстраняване на проблеми" }, diff --git a/apps/desktop/src/locales/bn/messages.json b/apps/desktop/src/locales/bn/messages.json index abd2c1cfae..626734ebff 100644 --- a/apps/desktop/src/locales/bn/messages.json +++ b/apps/desktop/src/locales/bn/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/bs/messages.json b/apps/desktop/src/locales/bs/messages.json index 825bd6344e..9d5685cca9 100644 --- a/apps/desktop/src/locales/bs/messages.json +++ b/apps/desktop/src/locales/bs/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/ca/messages.json b/apps/desktop/src/locales/ca/messages.json index 6c48d6cb0b..d8c0f32948 100644 --- a/apps/desktop/src/locales/ca/messages.json +++ b/apps/desktop/src/locales/ca/messages.json @@ -2697,6 +2697,9 @@ "message": "Formats comuns", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Resolució de problemes" }, diff --git a/apps/desktop/src/locales/cs/messages.json b/apps/desktop/src/locales/cs/messages.json index 550b10a31c..328ebe15ec 100644 --- a/apps/desktop/src/locales/cs/messages.json +++ b/apps/desktop/src/locales/cs/messages.json @@ -2697,6 +2697,9 @@ "message": "Společné formáty", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Řešení problémů" }, diff --git a/apps/desktop/src/locales/cy/messages.json b/apps/desktop/src/locales/cy/messages.json index b1cc9e63d3..62f2e608bb 100644 --- a/apps/desktop/src/locales/cy/messages.json +++ b/apps/desktop/src/locales/cy/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/da/messages.json b/apps/desktop/src/locales/da/messages.json index f2a84a3c29..95a054a7fb 100644 --- a/apps/desktop/src/locales/da/messages.json +++ b/apps/desktop/src/locales/da/messages.json @@ -2697,6 +2697,9 @@ "message": "Almindelige formater", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Fejlfinding" }, diff --git a/apps/desktop/src/locales/de/messages.json b/apps/desktop/src/locales/de/messages.json index e5e3945abc..6518a56b45 100644 --- a/apps/desktop/src/locales/de/messages.json +++ b/apps/desktop/src/locales/de/messages.json @@ -1633,10 +1633,10 @@ "message": "Browser-Integration wird nicht unterstützt" }, "browserIntegrationErrorTitle": { - "message": "Error enabling browser integration" + "message": "Fehler beim Aktivieren der Browser-Integration" }, "browserIntegrationErrorDesc": { - "message": "An error has occurred while enabling browser integration." + "message": "Beim Aktivieren der Browser-Integration ist ein Fehler aufgetreten." }, "browserIntegrationMasOnlyDesc": { "message": "Leider wird die Browser-Integration derzeit nur in der Mac App Store Version unterstützt." @@ -2697,6 +2697,9 @@ "message": "Gängigste Formate", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Problembehandlung" }, diff --git a/apps/desktop/src/locales/el/messages.json b/apps/desktop/src/locales/el/messages.json index 41e6a62a2f..87360c33ce 100644 --- a/apps/desktop/src/locales/el/messages.json +++ b/apps/desktop/src/locales/el/messages.json @@ -2697,6 +2697,9 @@ "message": "Κοινές μορφές", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Αντιμετώπιση Προβλημάτων" }, diff --git a/apps/desktop/src/locales/en_GB/messages.json b/apps/desktop/src/locales/en_GB/messages.json index 2658610df3..5c8c32b7c1 100644 --- a/apps/desktop/src/locales/en_GB/messages.json +++ b/apps/desktop/src/locales/en_GB/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/en_IN/messages.json b/apps/desktop/src/locales/en_IN/messages.json index 0542da9ddc..abfa0b1c0d 100644 --- a/apps/desktop/src/locales/en_IN/messages.json +++ b/apps/desktop/src/locales/en_IN/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/eo/messages.json b/apps/desktop/src/locales/eo/messages.json index 1c4cc4f0be..427f08f805 100644 --- a/apps/desktop/src/locales/eo/messages.json +++ b/apps/desktop/src/locales/eo/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/es/messages.json b/apps/desktop/src/locales/es/messages.json index ec5da44293..f7df93bdd7 100644 --- a/apps/desktop/src/locales/es/messages.json +++ b/apps/desktop/src/locales/es/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/et/messages.json b/apps/desktop/src/locales/et/messages.json index 3850cc1d85..02cd737baa 100644 --- a/apps/desktop/src/locales/et/messages.json +++ b/apps/desktop/src/locales/et/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/eu/messages.json b/apps/desktop/src/locales/eu/messages.json index b21108b6ad..2067b2dcc2 100644 --- a/apps/desktop/src/locales/eu/messages.json +++ b/apps/desktop/src/locales/eu/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/fa/messages.json b/apps/desktop/src/locales/fa/messages.json index 08356d410d..ef34f8222a 100644 --- a/apps/desktop/src/locales/fa/messages.json +++ b/apps/desktop/src/locales/fa/messages.json @@ -2697,6 +2697,9 @@ "message": "فرمت‌های رایج", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/fi/messages.json b/apps/desktop/src/locales/fi/messages.json index fca24c197a..33c593e3aa 100644 --- a/apps/desktop/src/locales/fi/messages.json +++ b/apps/desktop/src/locales/fi/messages.json @@ -2697,6 +2697,9 @@ "message": "Yleiset muodot", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Vianetsintä" }, diff --git a/apps/desktop/src/locales/fil/messages.json b/apps/desktop/src/locales/fil/messages.json index 6d5f85fca8..d28a4b568c 100644 --- a/apps/desktop/src/locales/fil/messages.json +++ b/apps/desktop/src/locales/fil/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/fr/messages.json b/apps/desktop/src/locales/fr/messages.json index 1097624b14..86550b736f 100644 --- a/apps/desktop/src/locales/fr/messages.json +++ b/apps/desktop/src/locales/fr/messages.json @@ -2697,6 +2697,9 @@ "message": "Formats communs", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Résolution de problèmes" }, diff --git a/apps/desktop/src/locales/gl/messages.json b/apps/desktop/src/locales/gl/messages.json index 90648699c0..889a2beeee 100644 --- a/apps/desktop/src/locales/gl/messages.json +++ b/apps/desktop/src/locales/gl/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/he/messages.json b/apps/desktop/src/locales/he/messages.json index 73599c012e..3b155ffdf3 100644 --- a/apps/desktop/src/locales/he/messages.json +++ b/apps/desktop/src/locales/he/messages.json @@ -2697,6 +2697,9 @@ "message": "תסדירים נפוצים", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/hi/messages.json b/apps/desktop/src/locales/hi/messages.json index 4f8bb9b4bb..af28c66681 100644 --- a/apps/desktop/src/locales/hi/messages.json +++ b/apps/desktop/src/locales/hi/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/hr/messages.json b/apps/desktop/src/locales/hr/messages.json index 220c8bfab2..01983d5891 100644 --- a/apps/desktop/src/locales/hr/messages.json +++ b/apps/desktop/src/locales/hr/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/hu/messages.json b/apps/desktop/src/locales/hu/messages.json index 149d48284e..5c91fb4b94 100644 --- a/apps/desktop/src/locales/hu/messages.json +++ b/apps/desktop/src/locales/hu/messages.json @@ -2697,6 +2697,9 @@ "message": "Általános formátumok", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Hibaelhárítás" }, diff --git a/apps/desktop/src/locales/id/messages.json b/apps/desktop/src/locales/id/messages.json index 3194b0f7d3..2173224f54 100644 --- a/apps/desktop/src/locales/id/messages.json +++ b/apps/desktop/src/locales/id/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/it/messages.json b/apps/desktop/src/locales/it/messages.json index 08ae2d9da8..93882cf698 100644 --- a/apps/desktop/src/locales/it/messages.json +++ b/apps/desktop/src/locales/it/messages.json @@ -2697,6 +2697,9 @@ "message": "Formati comuni", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Successo" + }, "troubleshooting": { "message": "Risoluzione problemi" }, diff --git a/apps/desktop/src/locales/ja/messages.json b/apps/desktop/src/locales/ja/messages.json index b07eab20cc..ab6c0be95f 100644 --- a/apps/desktop/src/locales/ja/messages.json +++ b/apps/desktop/src/locales/ja/messages.json @@ -2697,6 +2697,9 @@ "message": "一般的な形式", "description": "Label indicating the most common import formats" }, + "success": { + "message": "成功" + }, "troubleshooting": { "message": "トラブルシューティング" }, diff --git a/apps/desktop/src/locales/ka/messages.json b/apps/desktop/src/locales/ka/messages.json index 90648699c0..889a2beeee 100644 --- a/apps/desktop/src/locales/ka/messages.json +++ b/apps/desktop/src/locales/ka/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/km/messages.json b/apps/desktop/src/locales/km/messages.json index 90648699c0..889a2beeee 100644 --- a/apps/desktop/src/locales/km/messages.json +++ b/apps/desktop/src/locales/km/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/kn/messages.json b/apps/desktop/src/locales/kn/messages.json index 162cba3a75..eb0cbcf6be 100644 --- a/apps/desktop/src/locales/kn/messages.json +++ b/apps/desktop/src/locales/kn/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/ko/messages.json b/apps/desktop/src/locales/ko/messages.json index 09b1767af0..8e50ade96c 100644 --- a/apps/desktop/src/locales/ko/messages.json +++ b/apps/desktop/src/locales/ko/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/lt/messages.json b/apps/desktop/src/locales/lt/messages.json index de77aa8fbf..e9de697005 100644 --- a/apps/desktop/src/locales/lt/messages.json +++ b/apps/desktop/src/locales/lt/messages.json @@ -2697,6 +2697,9 @@ "message": "Dažni formatai", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/lv/messages.json b/apps/desktop/src/locales/lv/messages.json index 521a0afcf2..aa057f54ab 100644 --- a/apps/desktop/src/locales/lv/messages.json +++ b/apps/desktop/src/locales/lv/messages.json @@ -2697,6 +2697,9 @@ "message": "Izplatīti veidoli", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Sarežģījumu novēršana" }, diff --git a/apps/desktop/src/locales/me/messages.json b/apps/desktop/src/locales/me/messages.json index ed458379b8..1f49961b46 100644 --- a/apps/desktop/src/locales/me/messages.json +++ b/apps/desktop/src/locales/me/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/ml/messages.json b/apps/desktop/src/locales/ml/messages.json index b94b9d1b79..96811b9dba 100644 --- a/apps/desktop/src/locales/ml/messages.json +++ b/apps/desktop/src/locales/ml/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/mr/messages.json b/apps/desktop/src/locales/mr/messages.json index 90648699c0..889a2beeee 100644 --- a/apps/desktop/src/locales/mr/messages.json +++ b/apps/desktop/src/locales/mr/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/my/messages.json b/apps/desktop/src/locales/my/messages.json index 5142c8e61f..0ee0db69ef 100644 --- a/apps/desktop/src/locales/my/messages.json +++ b/apps/desktop/src/locales/my/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/nb/messages.json b/apps/desktop/src/locales/nb/messages.json index e190cfc236..7bf132bdac 100644 --- a/apps/desktop/src/locales/nb/messages.json +++ b/apps/desktop/src/locales/nb/messages.json @@ -2697,6 +2697,9 @@ "message": "Vanlige formater", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/ne/messages.json b/apps/desktop/src/locales/ne/messages.json index bd58a18b0d..13e1466805 100644 --- a/apps/desktop/src/locales/ne/messages.json +++ b/apps/desktop/src/locales/ne/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/nl/messages.json b/apps/desktop/src/locales/nl/messages.json index 3ca0730710..b5f2a413d6 100644 --- a/apps/desktop/src/locales/nl/messages.json +++ b/apps/desktop/src/locales/nl/messages.json @@ -2697,6 +2697,9 @@ "message": "Veelvoorkomende formaten", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Probleemoplossing" }, diff --git a/apps/desktop/src/locales/nn/messages.json b/apps/desktop/src/locales/nn/messages.json index 12e11b32c1..35e7173d74 100644 --- a/apps/desktop/src/locales/nn/messages.json +++ b/apps/desktop/src/locales/nn/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/or/messages.json b/apps/desktop/src/locales/or/messages.json index 7363558551..cd83d2ea69 100644 --- a/apps/desktop/src/locales/or/messages.json +++ b/apps/desktop/src/locales/or/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/pl/messages.json b/apps/desktop/src/locales/pl/messages.json index df7a158a3a..250c557309 100644 --- a/apps/desktop/src/locales/pl/messages.json +++ b/apps/desktop/src/locales/pl/messages.json @@ -2697,6 +2697,9 @@ "message": "Popularne formaty", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Sukces" + }, "troubleshooting": { "message": "Rozwiązywanie problemów" }, diff --git a/apps/desktop/src/locales/pt_BR/messages.json b/apps/desktop/src/locales/pt_BR/messages.json index f651e0b060..12db01d8cd 100644 --- a/apps/desktop/src/locales/pt_BR/messages.json +++ b/apps/desktop/src/locales/pt_BR/messages.json @@ -561,10 +561,10 @@ "message": "A sua nova conta foi criada! Agora você pode iniciar a sessão." }, "youSuccessfullyLoggedIn": { - "message": "You successfully logged in" + "message": "Você logou na sua conta com sucesso" }, "youMayCloseThisWindow": { - "message": "You may close this window" + "message": "Você pode fechar esta janela" }, "masterPassSent": { "message": "Enviamos um e-mail com a dica da sua senha mestra." @@ -801,10 +801,10 @@ "message": "Alterar Senha Mestra" }, "continueToWebApp": { - "message": "Continue to web app?" + "message": "Continuar no aplicativo web?" }, "changeMasterPasswordOnWebConfirmation": { - "message": "You can change your master password on the Bitwarden web app." + "message": "Você pode alterar a sua senha mestra no aplicativo web Bitwarden." }, "fingerprintPhrase": { "message": "Frase biométrica", @@ -1402,7 +1402,7 @@ "message": "Código PIN inválido." }, "tooManyInvalidPinEntryAttemptsLoggingOut": { - "message": "Too many invalid PIN entry attempts. Logging out." + "message": "Muitas tentativas de entrada de PIN inválidas. Desconectando." }, "unlockWithWindowsHello": { "message": "Desbloquear com o Windows Hello" @@ -1557,7 +1557,7 @@ "description": "Used as a card title description on the set password page to explain why the user is there" }, "verificationRequired": { - "message": "Verification required", + "message": "Verificação necessária", "description": "Default title for the user verification dialog." }, "currentMasterPass": { @@ -1633,10 +1633,10 @@ "message": "Integração com o navegador não suportado" }, "browserIntegrationErrorTitle": { - "message": "Error enabling browser integration" + "message": "Erro ao ativar a integração do navegador" }, "browserIntegrationErrorDesc": { - "message": "An error has occurred while enabling browser integration." + "message": "Ocorreu um erro ao permitir a integração do navegador." }, "browserIntegrationMasOnlyDesc": { "message": "Infelizmente, por ora, a integração do navegador só é suportada na versão da Mac App Store." @@ -1654,10 +1654,10 @@ "message": "Ative uma camada adicional de segurança, exigindo validação de frase de impressão digital ao estabelecer uma ligação entre o computador e o navegador. Quando ativado, isto requer intervenção do usuário e verificação cada vez que uma conexão é estabelecida." }, "enableHardwareAcceleration": { - "message": "Use hardware acceleration" + "message": "Utilizar aceleração de hardware" }, "enableHardwareAccelerationDesc": { - "message": "By default this setting is ON. Turn OFF only if you experience graphical issues. Restart is required." + "message": "Por padrão esta configuração está ativada. Desligar apenas se tiver problemas gráficos. Reiniciar é necessário." }, "approve": { "message": "Aprovar" @@ -1898,40 +1898,40 @@ "message": "A sua senha mestra não atende a uma ou mais das políticas da sua organização. Para acessar o cofre, você deve atualizar a sua senha mestra agora. O processo desconectará você da sessão atual, exigindo que você inicie a sessão novamente. Sessões ativas em outros dispositivos podem continuar ativas por até uma hora." }, "tryAgain": { - "message": "Try again" + "message": "Tentar novamente" }, "verificationRequiredForActionSetPinToContinue": { - "message": "Verification required for this action. Set a PIN to continue." + "message": "Verificação necessária para esta ação. Defina um PIN para continuar." }, "setPin": { - "message": "Set PIN" + "message": "Definir PIN" }, "verifyWithBiometrics": { - "message": "Verify with biometrics" + "message": "Verificiar com biometria" }, "awaitingConfirmation": { - "message": "Awaiting confirmation" + "message": "Aguardando confirmação" }, "couldNotCompleteBiometrics": { - "message": "Could not complete biometrics." + "message": "Não foi possível completar a biometria." }, "needADifferentMethod": { - "message": "Need a different method?" + "message": "Precisa de um método diferente?" }, "useMasterPassword": { - "message": "Use master password" + "message": "Usar a senha mestra" }, "usePin": { - "message": "Use PIN" + "message": "Usar PIN" }, "useBiometrics": { - "message": "Use biometrics" + "message": "Usar biometria" }, "enterVerificationCodeSentToEmail": { - "message": "Enter the verification code that was sent to your email." + "message": "Digite o código de verificação que foi enviado para o seu e-mail." }, "resendCode": { - "message": "Resend code" + "message": "Reenviar código" }, "hours": { "message": "Horas" @@ -2541,13 +2541,13 @@ } }, "launchDuoAndFollowStepsToFinishLoggingIn": { - "message": "Launch Duo and follow the steps to finish logging in." + "message": "Inicie o Duo e siga os passos para finalizar o login." }, "duoRequiredByOrgForAccount": { - "message": "Duo two-step login is required for your account." + "message": "A autenticação em duas etapas do Duo é necessária para sua conta." }, "launchDuo": { - "message": "Launch Duo in Browser" + "message": "Iniciar o Duo no navegador" }, "importFormatError": { "message": "Os dados não estão formatados corretamente. Por favor, verifique o seu arquivo de importação e tente novamente." @@ -2630,13 +2630,13 @@ "message": "Nome de usuário ou senha incorretos" }, "incorrectPassword": { - "message": "Incorrect password" + "message": "Senha incorreta" }, "incorrectCode": { - "message": "Incorrect code" + "message": "Código incorreto" }, "incorrectPin": { - "message": "Incorrect PIN" + "message": "PIN incorreto" }, "multifactorAuthenticationFailed": { "message": "Falha na autenticação de múltiplos fatores" @@ -2697,25 +2697,28 @@ "message": "Formatos comuns", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { - "message": "Troubleshooting" + "message": "Solução de problemas" }, "disableHardwareAccelerationRestart": { - "message": "Disable hardware acceleration and restart" + "message": "Desativar aceleração de hardware e reiniciar" }, "enableHardwareAccelerationRestart": { - "message": "Enable hardware acceleration and restart" + "message": "Ativar aceleração de hardware e reiniciar" }, "removePasskey": { - "message": "Remove passkey" + "message": "Remover senha" }, "passkeyRemoved": { - "message": "Passkey removed" + "message": "Chave de acesso removida" }, "errorAssigningTargetCollection": { - "message": "Error assigning target collection." + "message": "Erro ao atribuir coleção de destino." }, "errorAssigningTargetFolder": { - "message": "Error assigning target folder." + "message": "Erro ao atribuir pasta de destino." } } diff --git a/apps/desktop/src/locales/pt_PT/messages.json b/apps/desktop/src/locales/pt_PT/messages.json index 4e58dad2cf..97ce9a8b88 100644 --- a/apps/desktop/src/locales/pt_PT/messages.json +++ b/apps/desktop/src/locales/pt_PT/messages.json @@ -2697,6 +2697,9 @@ "message": "Formatos comuns", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Resolução de problemas" }, diff --git a/apps/desktop/src/locales/ro/messages.json b/apps/desktop/src/locales/ro/messages.json index 3fe73f28a7..978f57eb9b 100644 --- a/apps/desktop/src/locales/ro/messages.json +++ b/apps/desktop/src/locales/ro/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/ru/messages.json b/apps/desktop/src/locales/ru/messages.json index cc182812a6..c9b3b95b39 100644 --- a/apps/desktop/src/locales/ru/messages.json +++ b/apps/desktop/src/locales/ru/messages.json @@ -2697,6 +2697,9 @@ "message": "Основные форматы", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Успешно" + }, "troubleshooting": { "message": "Устранение проблем" }, diff --git a/apps/desktop/src/locales/si/messages.json b/apps/desktop/src/locales/si/messages.json index 261ae1c9b8..3d43997144 100644 --- a/apps/desktop/src/locales/si/messages.json +++ b/apps/desktop/src/locales/si/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/sk/messages.json b/apps/desktop/src/locales/sk/messages.json index 6ef52a83ee..13d720dbfb 100644 --- a/apps/desktop/src/locales/sk/messages.json +++ b/apps/desktop/src/locales/sk/messages.json @@ -2697,6 +2697,9 @@ "message": "Bežné formáty", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Riešenie problémov" }, diff --git a/apps/desktop/src/locales/sl/messages.json b/apps/desktop/src/locales/sl/messages.json index 8c9c158c87..8cb06dcf0c 100644 --- a/apps/desktop/src/locales/sl/messages.json +++ b/apps/desktop/src/locales/sl/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/sr/messages.json b/apps/desktop/src/locales/sr/messages.json index edafdb55a2..04b7e4cf29 100644 --- a/apps/desktop/src/locales/sr/messages.json +++ b/apps/desktop/src/locales/sr/messages.json @@ -801,10 +801,10 @@ "message": "Промени главну лозинку" }, "continueToWebApp": { - "message": "Continue to web app?" + "message": "Ићи на веб апликацију?" }, "changeMasterPasswordOnWebConfirmation": { - "message": "You can change your master password on the Bitwarden web app." + "message": "Можете променити главну лозинку на Bitwarden веб апликацији." }, "fingerprintPhrase": { "message": "Сигурносна Фраза Сефа", @@ -1633,10 +1633,10 @@ "message": "Интеграција са претраживачем није подржана" }, "browserIntegrationErrorTitle": { - "message": "Error enabling browser integration" + "message": "Грешка при омогућавању интеграције прегледача" }, "browserIntegrationErrorDesc": { - "message": "An error has occurred while enabling browser integration." + "message": "Дошло је до грешке при омогућавању интеграције прегледача." }, "browserIntegrationMasOnlyDesc": { "message": "Нажалост, интеграција прегледача за сада је подржана само у верзији Mac App Store." @@ -2697,6 +2697,9 @@ "message": "Уобичајени формати", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Решавање проблема" }, @@ -2713,9 +2716,9 @@ "message": "Приступачни кључ је уклоњен" }, "errorAssigningTargetCollection": { - "message": "Error assigning target collection." + "message": "Грешка при додељивању циљне колекције." }, "errorAssigningTargetFolder": { - "message": "Error assigning target folder." + "message": "Грешка при додељивању циљне фасцикле." } } diff --git a/apps/desktop/src/locales/sv/messages.json b/apps/desktop/src/locales/sv/messages.json index 6342424fd4..bd21c0f328 100644 --- a/apps/desktop/src/locales/sv/messages.json +++ b/apps/desktop/src/locales/sv/messages.json @@ -2697,6 +2697,9 @@ "message": "Vanliga format", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Felsökning" }, diff --git a/apps/desktop/src/locales/te/messages.json b/apps/desktop/src/locales/te/messages.json index 90648699c0..889a2beeee 100644 --- a/apps/desktop/src/locales/te/messages.json +++ b/apps/desktop/src/locales/te/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/th/messages.json b/apps/desktop/src/locales/th/messages.json index 5b6a1b9d0b..f1cd5351f7 100644 --- a/apps/desktop/src/locales/th/messages.json +++ b/apps/desktop/src/locales/th/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/tr/messages.json b/apps/desktop/src/locales/tr/messages.json index 42a8f207c7..3e7229c41b 100644 --- a/apps/desktop/src/locales/tr/messages.json +++ b/apps/desktop/src/locales/tr/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Sorun giderme" }, diff --git a/apps/desktop/src/locales/uk/messages.json b/apps/desktop/src/locales/uk/messages.json index ac7b7c3243..9ee7652093 100644 --- a/apps/desktop/src/locales/uk/messages.json +++ b/apps/desktop/src/locales/uk/messages.json @@ -2697,6 +2697,9 @@ "message": "Поширені формати", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Успішно" + }, "troubleshooting": { "message": "Усунення проблем" }, diff --git a/apps/desktop/src/locales/vi/messages.json b/apps/desktop/src/locales/vi/messages.json index aac9995db1..0c0e6f6df7 100644 --- a/apps/desktop/src/locales/vi/messages.json +++ b/apps/desktop/src/locales/vi/messages.json @@ -2697,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/zh_CN/messages.json b/apps/desktop/src/locales/zh_CN/messages.json index 0560466cf8..9837be29e3 100644 --- a/apps/desktop/src/locales/zh_CN/messages.json +++ b/apps/desktop/src/locales/zh_CN/messages.json @@ -801,10 +801,10 @@ "message": "修改主密码" }, "continueToWebApp": { - "message": "Continue to web app?" + "message": "前往网页 App 吗?" }, "changeMasterPasswordOnWebConfirmation": { - "message": "You can change your master password on the Bitwarden web app." + "message": "您可以在 Bitwarden 网页应用上更改您的主密码。" }, "fingerprintPhrase": { "message": "指纹短语", @@ -2697,6 +2697,9 @@ "message": "常规格式", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "故障排除" }, diff --git a/apps/desktop/src/locales/zh_TW/messages.json b/apps/desktop/src/locales/zh_TW/messages.json index 9eb12e23cf..5f768b0a43 100644 --- a/apps/desktop/src/locales/zh_TW/messages.json +++ b/apps/desktop/src/locales/zh_TW/messages.json @@ -2697,6 +2697,9 @@ "message": "常見格式", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "疑難排解" }, From 242ee306cf32674d968ba2b1639126d5d2c45486 Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Tue, 23 Apr 2024 20:34:02 +0200 Subject: [PATCH 263/351] Shorten extension description to 112 characters as that is a limit setup by Apple (#8884) Safari extension description is limited to 112 chars Add that restriction within the description Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com> --- apps/browser/src/_locales/en/messages.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 7e6e333689..1c0b178895 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." From bc43f3f78f41c1b171522d54665cfbfaedbb13e4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 23 Apr 2024 19:22:48 +0000 Subject: [PATCH 264/351] Autosync the updated translations (#8886) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/browser/src/_locales/ar/messages.json | 4 ++-- apps/browser/src/_locales/az/messages.json | 4 ++-- apps/browser/src/_locales/be/messages.json | 4 ++-- apps/browser/src/_locales/bg/messages.json | 4 ++-- apps/browser/src/_locales/bn/messages.json | 4 ++-- apps/browser/src/_locales/bs/messages.json | 4 ++-- apps/browser/src/_locales/ca/messages.json | 4 ++-- apps/browser/src/_locales/cs/messages.json | 4 ++-- apps/browser/src/_locales/cy/messages.json | 4 ++-- apps/browser/src/_locales/da/messages.json | 4 ++-- apps/browser/src/_locales/de/messages.json | 4 ++-- apps/browser/src/_locales/el/messages.json | 4 ++-- apps/browser/src/_locales/en_GB/messages.json | 4 ++-- apps/browser/src/_locales/en_IN/messages.json | 4 ++-- apps/browser/src/_locales/es/messages.json | 4 ++-- apps/browser/src/_locales/et/messages.json | 4 ++-- apps/browser/src/_locales/eu/messages.json | 4 ++-- apps/browser/src/_locales/fa/messages.json | 4 ++-- apps/browser/src/_locales/fi/messages.json | 4 ++-- apps/browser/src/_locales/fil/messages.json | 4 ++-- apps/browser/src/_locales/fr/messages.json | 4 ++-- apps/browser/src/_locales/gl/messages.json | 4 ++-- apps/browser/src/_locales/he/messages.json | 4 ++-- apps/browser/src/_locales/hi/messages.json | 4 ++-- apps/browser/src/_locales/hr/messages.json | 4 ++-- apps/browser/src/_locales/hu/messages.json | 4 ++-- apps/browser/src/_locales/id/messages.json | 4 ++-- apps/browser/src/_locales/it/messages.json | 4 ++-- apps/browser/src/_locales/ja/messages.json | 4 ++-- apps/browser/src/_locales/ka/messages.json | 4 ++-- apps/browser/src/_locales/km/messages.json | 4 ++-- apps/browser/src/_locales/kn/messages.json | 4 ++-- apps/browser/src/_locales/ko/messages.json | 2 +- apps/browser/src/_locales/lt/messages.json | 4 ++-- apps/browser/src/_locales/lv/messages.json | 4 ++-- apps/browser/src/_locales/ml/messages.json | 4 ++-- apps/browser/src/_locales/mr/messages.json | 4 ++-- apps/browser/src/_locales/my/messages.json | 4 ++-- apps/browser/src/_locales/nb/messages.json | 4 ++-- apps/browser/src/_locales/ne/messages.json | 4 ++-- apps/browser/src/_locales/nl/messages.json | 4 ++-- apps/browser/src/_locales/nn/messages.json | 4 ++-- apps/browser/src/_locales/or/messages.json | 4 ++-- apps/browser/src/_locales/pl/messages.json | 4 ++-- apps/browser/src/_locales/pt_BR/messages.json | 4 ++-- apps/browser/src/_locales/pt_PT/messages.json | 4 ++-- apps/browser/src/_locales/ro/messages.json | 4 ++-- apps/browser/src/_locales/ru/messages.json | 4 ++-- apps/browser/src/_locales/si/messages.json | 4 ++-- apps/browser/src/_locales/sk/messages.json | 4 ++-- apps/browser/src/_locales/sl/messages.json | 4 ++-- apps/browser/src/_locales/sr/messages.json | 4 ++-- apps/browser/src/_locales/sv/messages.json | 4 ++-- apps/browser/src/_locales/te/messages.json | 4 ++-- apps/browser/src/_locales/th/messages.json | 4 ++-- apps/browser/src/_locales/tr/messages.json | 4 ++-- apps/browser/src/_locales/uk/messages.json | 4 ++-- apps/browser/src/_locales/vi/messages.json | 4 ++-- apps/browser/src/_locales/zh_CN/messages.json | 2 +- apps/browser/src/_locales/zh_TW/messages.json | 4 ++-- 60 files changed, 118 insertions(+), 118 deletions(-) diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index e08894be0b..996142b5ad 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "قم بالتسجيل أو إنشاء حساب جديد للوصول إلى خزنتك الآمنة." diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index 1e5062d8c6..a58ada8eb1 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Güvənli anbarınıza müraciət etmək üçün giriş edin və ya yeni bir hesab yaradın." diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index 91ff397b3a..82fd4fa5d4 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Увайдзіце або стварыце новы ўліковы запіс для доступу да бяспечнага сховішча." diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index 33be2608b4..b6d41cb622 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Впишете се или създайте нов абонамент, за да достъпите защитен трезор." diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index a12308648a..dec1bc6cfa 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "আপনার সুরক্ষিত ভল্টে প্রবেশ করতে লগ ইন করুন অথবা একটি নতুন অ্যাকাউন্ট তৈরি করুন।" diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index 7f406fabee..9d3113e3f6 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Prijavite se ili napravite novi račun da biste pristupili svom sigurnom trezoru." diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index 7c8bd63aea..8063ba79d8 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Inicieu sessió o creeu un compte nou per accedir a la caixa forta." diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index bd3c6882df..ee58f3d263 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Pro přístup do Vašeho bezpečného trezoru se přihlaste nebo si vytvořte nový účet." diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index 2be868872c..a80dca5f92 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Mewngofnodwch neu crëwch gyfrif newydd i gael mynediad i'ch cell ddiogel." diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index 777c3b484f..215d79eb21 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Log ind eller opret en ny konto for at få adgang til din sikre boks." diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index d55d499b3c..fbc193dbae 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Zu Hause, am Arbeitsplatz oder unterwegs schützt Bitwarden einfach alle deine Passwörter, Passkeys und vertraulichen Informationen.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Melde dich an oder erstelle ein neues Konto, um auf deinen Tresor zuzugreifen." diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index 8c65e61e53..5c85aeff58 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Συνδεθείτε ή δημιουργήστε ένα νέο λογαριασμό για να αποκτήσετε πρόσβαση στο ασφαλές vault σας." diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index e4d90adf1a..087cd3faa8 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index 7cc17240d2..f370af7f36 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index 3e488bce4c..9e89f453df 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Identifícate o crea una nueva cuenta para acceder a tu caja fuerte." diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index 785a3e4986..5705a5a0d2 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Logi oma olemasolevasse kontosse sisse või loo uus konto." diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index 9a07b9d9ae..ee3b5f1329 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Saioa hasi edo sortu kontu berri bat zure kutxa gotorrera sartzeko." diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index c68dc43ef4..e2f0e96c86 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "وارد شوید یا یک حساب کاربری بسازید تا به گاوصندوق امن‌تان دسترسی یابید." diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index 2cdb6a2379..746f4f45be 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Kotona, töissä tai reissussa, Bitwarden suojaa helposti kaikki salasanasi, avainkoodisi ja arkaluonteiset tietosi.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Käytä salattua holviasi kirjautumalla sisään tai luo uusi tili." diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index 0dfb4a39c9..abb999d032 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Maglog-in o gumawa ng bagong account para ma-access ang iyong ligtas na kahadeyero." diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index 742e31ee58..de35f71832 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Identifiez-vous ou créez un nouveau compte pour accéder à votre coffre sécurisé." diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index b4c151eeb0..3dd737f0a8 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index 61482da54a..5d343ae807 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "צור חשבון חדש או התחבר כדי לגשת לכספת המאובטחת שלך." diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index b76405eed8..fa4051d3e9 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "अपनी सुरक्षित तिजोरी में प्रवेश करने के लिए नया खाता बनाएं या लॉग इन करें।" diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index 2dc500bc1e..c9b8741509 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Prijavi se ili stvori novi račun za pristup svojem sigurnom trezoru." diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index e47f2cda1f..5d5b174435 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Bejelentkezés vagy új fiók létrehozása a biztonsági széf eléréséhez." diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index d4399d8e15..b54e854d27 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Masuk atau buat akun baru untuk mengakses brankas Anda." diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index 93ae682190..91d10253a0 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Accedi o crea un nuovo account per accedere alla tua cassaforte." diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index 52ff21727a..967dc222e5 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "安全なデータ保管庫へアクセスするためにログインまたはアカウントを作成してください。" diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index 2c18502eca..c73c366195 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index 5d1b024c60..b6384bb840 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index 047270808e..178cd7c45f 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "ನಿಮ್ಮ ಸುರಕ್ಷಿತ ವಾಲ್ಟ್ ಅನ್ನು ಪ್ರವೇಶಿಸಲು ಲಾಗ್ ಇನ್ ಮಾಡಿ ಅಥವಾ ಹೊಸ ಖಾತೆಯನ್ನು ರಚಿಸಿ." diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index 1724225b0e..95a7727b83 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -8,7 +8,7 @@ }, "extDesc": { "message": "집에서도, 직장에서도, 이동 중에도 Bitwarden은 비밀번호, 패스키, 민감 정보를 쉽게 보호합니다.", - "description": "Extension description" + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "안전 보관함에 접근하려면 로그인하거나 새 계정을 만드세요." diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index b1a2c857e0..a01c5069e8 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Prisijunkite arba sukurkite naują paskyrą, kad galėtumėte pasiekti saugyklą." diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index 4055693486..f24f0a93fc 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Jāpiesakās vai jāizveido jauns konts, lai piekļūtu drošajai glabātavai." diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index d9703137fe..334027b407 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "നിങ്ങളുടെ സുരക്ഷിത വാൾട്ടിലേക്കു പ്രവേശിക്കാൻ ലോഗിൻ ചെയ്യുക അല്ലെങ്കിൽ ഒരു പുതിയ അക്കൗണ്ട് സൃഷ്ടിക്കുക." diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index f67f617d3b..b0e9f8abc1 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "तुमच्या सुरक्षित तिजोरीत पोहचण्यासाठी लॉग इन करा किंवा नवीन खाते उघडा." diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index 5d1b024c60..b6384bb840 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index 649163a8dc..163154b2f2 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Logg på eller opprett en ny konto for å få tilgang til ditt sikre hvelv." diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index 5d1b024c60..b6384bb840 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index f1424df0b9..cd76fc9684 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Log in of maak een nieuw account aan om toegang te krijgen tot je beveiligde kluis." diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index 5d1b024c60..b6384bb840 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index 5d1b024c60..b6384bb840 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index 9b3e8f20fc..d3d9106c15 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "W domu, w pracy, lub w ruchu, Bitwarden z łatwością zabezpiecza wszystkie Twoje hasła, passkeys i poufne informacje.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Zaloguj się lub utwórz nowe konto, aby uzyskać dostęp do Twojego bezpiecznego sejfu." diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index ef2b6f2dca..417bc977eb 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Em casa, no trabalho, ou em qualquer lugar, o Bitwarden protege facilmente todas as suas senhas, senhas e informações confidenciais.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Inicie a sessão ou crie uma nova conta para acessar seu cofre seguro." diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index 06ba8eed26..6d6fd70276 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Inicie sessão ou crie uma nova conta para aceder ao seu cofre seguro." diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index b3e0a2066f..780bf69b93 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Autentificați-vă sau creați un cont nou pentru a accesa seiful dvs. securizat." diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index e594dbdce2..927095a3f6 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Войдите или создайте новый аккаунт для доступа к вашему защищенному хранилищу." diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index 05e2dc3edd..33b03f574b 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "ඔබගේ ආරක්ෂිත සුරක්ෂිතාගාරය වෙත පිවිසීමට හෝ නව ගිණුමක් නිර්මාණය කරන්න." diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index eab1d105eb..c84cfbb778 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Prihláste sa, alebo vytvorte nový účet pre prístup k vášmu bezpečnému trezoru." diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index 935678efc8..4a6b7cd214 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Prijavite se ali ustvarite nov račun za dostop do svojega varnega trezorja." diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index acac4d14c6..a04a7ecd70 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Пријавите се или креирајте нови налог за приступ сефу." diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index d96e86b8d3..2b9ec59ec2 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Logga in eller skapa ett nytt konto för att komma åt ditt säkra valv." diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index 5d1b024c60..b6384bb840 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index 794d0e6c22..7e1dda99be 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "ล็อกอิน หรือ สร้างบัญชีใหม่ เพื่อใช้งานตู้นิรภัยของคุณ" diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index 8408253b86..8a8bb6ea60 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Güvenli kasanıza ulaşmak için giriş yapın veya yeni bir hesap oluşturun." diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index b590b92041..27293fc992 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Для доступу до сховища увійдіть в обліковий запис, або створіть новий." diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index ab1d0d515b..4eba4ffaea 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Đăng nhập hoặc tạo tài khoản mới để truy cập kho lưu trữ của bạn." diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index fa4dab6a8f..3cf2f96da1 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -8,7 +8,7 @@ }, "extDesc": { "message": "无论是在家里、工作中还是在外出时,Bitwarden 都可以轻松地保护您的所有密码、通行密钥和敏感信息。", - "description": "Extension description" + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "登录或者创建一个账户来访问您的安全密码库。" diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index 1ecfdfc50e..eb35cd08c7 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "登入或建立帳戶以存取您的安全密碼庫。" From 6b0628b81e33ae158231ccbd3d5efc3d09794167 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 23 Apr 2024 19:27:46 +0000 Subject: [PATCH 265/351] Autosync the updated translations (#8885) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/desktop/src/locales/az/messages.json | 2 +- apps/desktop/src/locales/bg/messages.json | 2 +- apps/desktop/src/locales/cs/messages.json | 2 +- apps/desktop/src/locales/da/messages.json | 2 +- apps/desktop/src/locales/de/messages.json | 2 +- apps/desktop/src/locales/fi/messages.json | 2 +- apps/desktop/src/locales/pt_PT/messages.json | 2 +- apps/desktop/src/locales/sk/messages.json | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/desktop/src/locales/az/messages.json b/apps/desktop/src/locales/az/messages.json index cf664abf41..1ecd18eee7 100644 --- a/apps/desktop/src/locales/az/messages.json +++ b/apps/desktop/src/locales/az/messages.json @@ -2698,7 +2698,7 @@ "description": "Label indicating the most common import formats" }, "success": { - "message": "Success" + "message": "Uğurlu" }, "troubleshooting": { "message": "Problemlərin aradan qaldırılması" diff --git a/apps/desktop/src/locales/bg/messages.json b/apps/desktop/src/locales/bg/messages.json index 0c5dc25742..d53034d61c 100644 --- a/apps/desktop/src/locales/bg/messages.json +++ b/apps/desktop/src/locales/bg/messages.json @@ -2698,7 +2698,7 @@ "description": "Label indicating the most common import formats" }, "success": { - "message": "Success" + "message": "Успех" }, "troubleshooting": { "message": "Отстраняване на проблеми" diff --git a/apps/desktop/src/locales/cs/messages.json b/apps/desktop/src/locales/cs/messages.json index 328ebe15ec..e68fe8fffc 100644 --- a/apps/desktop/src/locales/cs/messages.json +++ b/apps/desktop/src/locales/cs/messages.json @@ -2698,7 +2698,7 @@ "description": "Label indicating the most common import formats" }, "success": { - "message": "Success" + "message": "Úspěch" }, "troubleshooting": { "message": "Řešení problémů" diff --git a/apps/desktop/src/locales/da/messages.json b/apps/desktop/src/locales/da/messages.json index 95a054a7fb..0e578a6f66 100644 --- a/apps/desktop/src/locales/da/messages.json +++ b/apps/desktop/src/locales/da/messages.json @@ -2698,7 +2698,7 @@ "description": "Label indicating the most common import formats" }, "success": { - "message": "Success" + "message": "Gennemført" }, "troubleshooting": { "message": "Fejlfinding" diff --git a/apps/desktop/src/locales/de/messages.json b/apps/desktop/src/locales/de/messages.json index 6518a56b45..d04c2795f3 100644 --- a/apps/desktop/src/locales/de/messages.json +++ b/apps/desktop/src/locales/de/messages.json @@ -2698,7 +2698,7 @@ "description": "Label indicating the most common import formats" }, "success": { - "message": "Success" + "message": "Erfolg" }, "troubleshooting": { "message": "Problembehandlung" diff --git a/apps/desktop/src/locales/fi/messages.json b/apps/desktop/src/locales/fi/messages.json index 33c593e3aa..517b437d03 100644 --- a/apps/desktop/src/locales/fi/messages.json +++ b/apps/desktop/src/locales/fi/messages.json @@ -2698,7 +2698,7 @@ "description": "Label indicating the most common import formats" }, "success": { - "message": "Success" + "message": "Onnistui" }, "troubleshooting": { "message": "Vianetsintä" diff --git a/apps/desktop/src/locales/pt_PT/messages.json b/apps/desktop/src/locales/pt_PT/messages.json index 97ce9a8b88..14f0ec5d2f 100644 --- a/apps/desktop/src/locales/pt_PT/messages.json +++ b/apps/desktop/src/locales/pt_PT/messages.json @@ -2698,7 +2698,7 @@ "description": "Label indicating the most common import formats" }, "success": { - "message": "Success" + "message": "Com sucesso" }, "troubleshooting": { "message": "Resolução de problemas" diff --git a/apps/desktop/src/locales/sk/messages.json b/apps/desktop/src/locales/sk/messages.json index 13d720dbfb..6499486b9d 100644 --- a/apps/desktop/src/locales/sk/messages.json +++ b/apps/desktop/src/locales/sk/messages.json @@ -2698,7 +2698,7 @@ "description": "Label indicating the most common import formats" }, "success": { - "message": "Success" + "message": "Úspech" }, "troubleshooting": { "message": "Riešenie problémov" From 790c9a614141e91debfadfeff1607bfcd50ff8f0 Mon Sep 17 00:00:00 2001 From: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com> Date: Tue, 23 Apr 2024 16:00:47 -0400 Subject: [PATCH 266/351] Fixed race condition where this.canAccessPremium would be undefined before the sync could complete (#8887) --- .../angular/src/vault/components/view.component.ts | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/libs/angular/src/vault/components/view.component.ts b/libs/angular/src/vault/components/view.component.ts index 42349737f0..27d6e14b11 100644 --- a/libs/angular/src/vault/components/view.component.ts +++ b/libs/angular/src/vault/components/view.component.ts @@ -9,7 +9,7 @@ import { OnInit, Output, } from "@angular/core"; -import { firstValueFrom, Subject, takeUntil } from "rxjs"; +import { firstValueFrom } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; @@ -69,7 +69,6 @@ export class ViewComponent implements OnDestroy, OnInit { private totpInterval: any; private previousCipherId: string; private passwordReprompted = false; - private directiveIsDestroyed$ = new Subject<boolean>(); get fido2CredentialCreationDateValue(): string { const dateCreated = this.i18nService.t("dateCreated"); @@ -119,19 +118,11 @@ export class ViewComponent implements OnDestroy, OnInit { } }); }); - - this.billingAccountProfileStateService.hasPremiumFromAnySource$ - .pipe(takeUntil(this.directiveIsDestroyed$)) - .subscribe((canAccessPremium: boolean) => { - this.canAccessPremium = canAccessPremium; - }); } ngOnDestroy() { this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); this.cleanUp(); - this.directiveIsDestroyed$.next(true); - this.directiveIsDestroyed$.complete(); } async load() { @@ -141,6 +132,9 @@ export class ViewComponent implements OnDestroy, OnInit { this.cipher = await cipher.decrypt( await this.cipherService.getKeyForCipherKeyDecryption(cipher), ); + this.canAccessPremium = await firstValueFrom( + this.billingAccountProfileStateService.hasPremiumFromAnySource$, + ); this.showPremiumRequiredTotp = this.cipher.login.totp && !this.canAccessPremium && !this.cipher.organizationUseTotp; From 8ef5340635ac641d702f104f001baf40cd960b0c Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Tue, 23 Apr 2024 16:57:19 -0400 Subject: [PATCH 267/351] Trust our own copy of authenticatedAccounts until all accounts are initialized (#8888) --- .../src/platform/services/state.service.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index f660cd7a34..412176e235 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -115,14 +115,19 @@ export class StateService< return; } + // Get all likely authenticated accounts + const authenticatedAccounts = ( + (await this.storageService.get<string[]>(keys.authenticatedAccounts)) ?? [] + ).filter((account) => account != null); + await this.updateState(async (state) => { - state.authenticatedAccounts = - (await this.storageService.get<string[]>(keys.authenticatedAccounts)) ?? []; - for (const i in state.authenticatedAccounts) { - if (i != null) { - state = await this.syncAccountFromDisk(state.authenticatedAccounts[i]); - } + for (const i in authenticatedAccounts) { + state = await this.syncAccountFromDisk(authenticatedAccounts[i]); } + + // After all individual accounts have been added + state.authenticatedAccounts = authenticatedAccounts; + const storedActiveUser = await this.storageService.get<string>(keys.activeUserId); if (storedActiveUser != null) { state.activeUserId = storedActiveUser; From 1520d95bbc862d875b75764695262a45a85c300e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 23 Apr 2024 15:21:25 -0700 Subject: [PATCH 268/351] [deps] Auth: Update @types/node-ipc to v9.2.3 (#7248) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .../native-messaging-test-runner/package-lock.json | 10 ++++++---- apps/desktop/native-messaging-test-runner/package.json | 2 +- package-lock.json | 8 ++++---- package.json | 2 +- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/apps/desktop/native-messaging-test-runner/package-lock.json b/apps/desktop/native-messaging-test-runner/package-lock.json index e2961eb9ee..747d8ec981 100644 --- a/apps/desktop/native-messaging-test-runner/package-lock.json +++ b/apps/desktop/native-messaging-test-runner/package-lock.json @@ -20,15 +20,17 @@ "devDependencies": { "@tsconfig/node16": "1.0.4", "@types/node": "18.19.29", - "@types/node-ipc": "9.2.0", + "@types/node-ipc": "9.2.3", "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": { @@ -105,9 +107,9 @@ } }, "node_modules/@types/node-ipc": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/@types/node-ipc/-/node-ipc-9.2.0.tgz", - "integrity": "sha512-0v1oucUgINvWPhknecSBE5xkz74sVgeZgiL/LkWXNTSzFaGspEToA4oR56hjza0Jkk6DsS2EiNU3M2R2KQza9A==", + "version": "9.2.3", + "resolved": "https://registry.npmjs.org/@types/node-ipc/-/node-ipc-9.2.3.tgz", + "integrity": "sha512-/MvSiF71fYf3+zwqkh/zkVkZj1hl1Uobre9EMFy08mqfJNAmpR0vmPgOUdEIDVgifxHj6G1vYMPLSBLLxoDACQ==", "dev": true, "dependencies": { "@types/node": "*" diff --git a/apps/desktop/native-messaging-test-runner/package.json b/apps/desktop/native-messaging-test-runner/package.json index c572613119..72b2587a4a 100644 --- a/apps/desktop/native-messaging-test-runner/package.json +++ b/apps/desktop/native-messaging-test-runner/package.json @@ -25,7 +25,7 @@ "devDependencies": { "@tsconfig/node16": "1.0.4", "@types/node": "18.19.29", - "@types/node-ipc": "9.2.0", + "@types/node-ipc": "9.2.3", "typescript": "4.7.4" }, "_moduleAliases": { diff --git a/package-lock.json b/package-lock.json index f5932a25b8..ad27ada66b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -111,7 +111,7 @@ "@types/node": "18.19.29", "@types/node-fetch": "2.6.4", "@types/node-forge": "1.3.11", - "@types/node-ipc": "9.2.0", + "@types/node-ipc": "9.2.3", "@types/papaparse": "5.3.14", "@types/proper-lockfile": "4.1.4", "@types/react": "16.14.57", @@ -10544,9 +10544,9 @@ } }, "node_modules/@types/node-ipc": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/@types/node-ipc/-/node-ipc-9.2.0.tgz", - "integrity": "sha512-0v1oucUgINvWPhknecSBE5xkz74sVgeZgiL/LkWXNTSzFaGspEToA4oR56hjza0Jkk6DsS2EiNU3M2R2KQza9A==", + "version": "9.2.3", + "resolved": "https://registry.npmjs.org/@types/node-ipc/-/node-ipc-9.2.3.tgz", + "integrity": "sha512-/MvSiF71fYf3+zwqkh/zkVkZj1hl1Uobre9EMFy08mqfJNAmpR0vmPgOUdEIDVgifxHj6G1vYMPLSBLLxoDACQ==", "dev": true, "dependencies": { "@types/node": "*" diff --git a/package.json b/package.json index 057e737903..09065b234e 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "@types/node": "18.19.29", "@types/node-fetch": "2.6.4", "@types/node-forge": "1.3.11", - "@types/node-ipc": "9.2.0", + "@types/node-ipc": "9.2.3", "@types/papaparse": "5.3.14", "@types/proper-lockfile": "4.1.4", "@types/react": "16.14.57", From 423d8c71b520a578169a49577779f2cc79ac710d Mon Sep 17 00:00:00 2001 From: watsondm <129207532+watsondm@users.noreply.github.com> Date: Wed, 24 Apr 2024 11:01:51 -0400 Subject: [PATCH 269/351] CLOUDOPS-1592 Remove artifacts R2 steps from desktop release workflows (#8897) * CLOUDOPS-1592 Remove artifacts R2 steps from desktop release workflows * CLOUDOPS-1592 Remove artifacts R2 steps from staged rollout workflow --- .github/workflows/release-desktop-beta.yml | 20 +---------- .github/workflows/release-desktop.yml | 21 +---------- .github/workflows/staged-rollout-desktop.yml | 37 ++++---------------- 3 files changed, 8 insertions(+), 70 deletions(-) diff --git a/.github/workflows/release-desktop-beta.yml b/.github/workflows/release-desktop-beta.yml index b9e2d7a8c8..46f4ffad57 100644 --- a/.github/workflows/release-desktop-beta.yml +++ b/.github/workflows/release-desktop-beta.yml @@ -955,11 +955,7 @@ jobs: keyvault: "bitwarden-ci" secrets: "aws-electron-access-id, aws-electron-access-key, - aws-electron-bucket-name, - r2-electron-access-id, - r2-electron-access-key, - r2-electron-bucket-name, - cf-prod-account" + aws-electron-bucket-name" - name: Download all artifacts uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4 @@ -985,20 +981,6 @@ jobs: --recursive \ --quiet - - name: Publish artifacts to R2 - env: - AWS_ACCESS_KEY_ID: ${{ steps.retrieve-secrets.outputs.r2-electron-access-id }} - AWS_SECRET_ACCESS_KEY: ${{ steps.retrieve-secrets.outputs.r2-electron-access-key }} - AWS_DEFAULT_REGION: 'us-east-1' - AWS_S3_BUCKET_NAME: ${{ steps.retrieve-secrets.outputs.r2-electron-bucket-name }} - CF_ACCOUNT: ${{ steps.retrieve-secrets.outputs.cf-prod-account }} - working-directory: apps/desktop/artifacts - run: | - aws s3 cp ./ $AWS_S3_BUCKET_NAME/desktop/ \ - --recursive \ - --quiet \ - --endpoint-url https://${CF_ACCOUNT}.r2.cloudflarestorage.com - - name: Update deployment status to Success if: ${{ success() }} uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3 diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml index cf857d7177..dc6957d00d 100644 --- a/.github/workflows/release-desktop.yml +++ b/.github/workflows/release-desktop.yml @@ -115,11 +115,7 @@ jobs: keyvault: "bitwarden-ci" secrets: "aws-electron-access-id, aws-electron-access-key, - aws-electron-bucket-name, - r2-electron-access-id, - r2-electron-access-key, - r2-electron-bucket-name, - cf-prod-account" + aws-electron-bucket-name" - name: Download all artifacts if: ${{ github.event.inputs.release_type != 'Dry Run' }} @@ -169,21 +165,6 @@ jobs: --recursive \ --quiet - - name: Publish artifacts to R2 - if: ${{ github.event.inputs.release_type != 'Dry Run' && github.event.inputs.electron_publish == 'true' }} - env: - AWS_ACCESS_KEY_ID: ${{ steps.retrieve-secrets.outputs.r2-electron-access-id }} - AWS_SECRET_ACCESS_KEY: ${{ steps.retrieve-secrets.outputs.r2-electron-access-key }} - AWS_DEFAULT_REGION: 'us-east-1' - AWS_S3_BUCKET_NAME: ${{ steps.retrieve-secrets.outputs.r2-electron-bucket-name }} - CF_ACCOUNT: ${{ steps.retrieve-secrets.outputs.cf-prod-account }} - working-directory: apps/desktop/artifacts - run: | - aws s3 cp ./ $AWS_S3_BUCKET_NAME/desktop/ \ - --recursive \ - --quiet \ - --endpoint-url https://${CF_ACCOUNT}.r2.cloudflarestorage.com - - name: Get checksum files uses: bitwarden/gh-actions/get-checksum@main with: diff --git a/.github/workflows/staged-rollout-desktop.yml b/.github/workflows/staged-rollout-desktop.yml index a5b5fc69b1..a6ca2f1e31 100644 --- a/.github/workflows/staged-rollout-desktop.yml +++ b/.github/workflows/staged-rollout-desktop.yml @@ -31,29 +31,21 @@ jobs: keyvault: "bitwarden-ci" secrets: "aws-electron-access-id, aws-electron-access-key, - aws-electron-bucket-name, - r2-electron-access-id, - r2-electron-access-key, - r2-electron-bucket-name, - cf-prod-account" + aws-electron-bucket-name" - - name: Download channel update info files from R2 + - name: Download channel update info files from S3 env: - AWS_ACCESS_KEY_ID: ${{ steps.retrieve-secrets.outputs.r2-electron-access-id }} - AWS_SECRET_ACCESS_KEY: ${{ steps.retrieve-secrets.outputs.r2-electron-access-key }} - AWS_DEFAULT_REGION: 'us-east-1' - AWS_S3_BUCKET_NAME: ${{ steps.retrieve-secrets.outputs.r2-electron-bucket-name }} - CF_ACCOUNT: ${{ steps.retrieve-secrets.outputs.cf-prod-account }} + AWS_ACCESS_KEY_ID: ${{ steps.retrieve-secrets.outputs.aws-electron-access-id }} + AWS_SECRET_ACCESS_KEY: ${{ steps.retrieve-secrets.outputs.aws-electron-access-key }} + AWS_DEFAULT_REGION: 'us-west-2' + AWS_S3_BUCKET_NAME: ${{ steps.retrieve-secrets.outputs.aws-electron-bucket-name }} run: | aws s3 cp $AWS_S3_BUCKET_NAME/desktop/latest.yml . \ --quiet \ - --endpoint-url https://${CF_ACCOUNT}.r2.cloudflarestorage.com aws s3 cp $AWS_S3_BUCKET_NAME/desktop/latest-linux.yml . \ --quiet \ - --endpoint-url https://${CF_ACCOUNT}.r2.cloudflarestorage.com aws s3 cp $AWS_S3_BUCKET_NAME/desktop/latest-mac.yml . \ --quiet \ - --endpoint-url https://${CF_ACCOUNT}.r2.cloudflarestorage.com - name: Check new rollout percentage env: @@ -95,20 +87,3 @@ jobs: aws s3 cp latest-mac.yml $AWS_S3_BUCKET_NAME/desktop/ \ --acl "public-read" - - - name: Publish channel update info files to R2 - env: - AWS_ACCESS_KEY_ID: ${{ steps.retrieve-secrets.outputs.r2-electron-access-id }} - AWS_SECRET_ACCESS_KEY: ${{ steps.retrieve-secrets.outputs.r2-electron-access-key }} - AWS_DEFAULT_REGION: 'us-east-1' - AWS_S3_BUCKET_NAME: ${{ steps.retrieve-secrets.outputs.r2-electron-bucket-name }} - CF_ACCOUNT: ${{ steps.retrieve-secrets.outputs.cf-prod-account }} - run: | - aws s3 cp latest.yml $AWS_S3_BUCKET_NAME/desktop/ \ - --endpoint-url https://${CF_ACCOUNT}.r2.cloudflarestorage.com - - aws s3 cp latest-linux.yml $AWS_S3_BUCKET_NAME/desktop/ \ - --endpoint-url https://${CF_ACCOUNT}.r2.cloudflarestorage.com - - aws s3 cp latest-mac.yml $AWS_S3_BUCKET_NAME/desktop/ \ - --endpoint-url https://${CF_ACCOUNT}.r2.cloudflarestorage.com From 493b79b8881b44b34c2a19ded009308960b16fac Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Wed, 24 Apr 2024 11:14:53 -0400 Subject: [PATCH 270/351] Only Run Migrations in True Background (#8548) * Only Run Migrations in True Background * Use `isPrivateMode` * Use `popupOnlyContext` --- apps/browser/src/background/main.background.ts | 3 ++- apps/browser/src/popup/services/init.service.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index a64ee2b8a0..6069e9b5e9 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1095,7 +1095,8 @@ export default class MainBackground { async bootstrap() { this.containerService.attachToGlobal(self); - await this.stateService.init({ runMigrations: !this.isPrivateMode }); + // Only the "true" background should run migrations + await this.stateService.init({ runMigrations: !this.popupOnlyContext }); // This is here instead of in in the InitService b/c we don't plan for // side effects to run in the Browser InitService. diff --git a/apps/browser/src/popup/services/init.service.ts b/apps/browser/src/popup/services/init.service.ts index c9e6d66c2a..ee842565d7 100644 --- a/apps/browser/src/popup/services/init.service.ts +++ b/apps/browser/src/popup/services/init.service.ts @@ -22,7 +22,7 @@ export class InitService { init() { return async () => { - await this.stateService.init(); + await this.stateService.init({ runMigrations: false }); // Browser background is responsible for migrations await this.i18nService.init(); if (!BrowserPopupUtils.inPopup(window)) { From b7957d6e28f7826b042e41063c2d4642aa09ece5 Mon Sep 17 00:00:00 2001 From: Jake Fink <jfink@bitwarden.com> Date: Wed, 24 Apr 2024 11:19:10 -0400 Subject: [PATCH 271/351] set keypair before creating hub connection for admin requests (#8898) --- .../src/auth/components/login-via-auth-request.component.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 5a1180cd38..3b827669a5 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 @@ -221,7 +221,8 @@ export class LoginViaAuthRequestComponent } // Request still pending response from admin - // So, create hub connection so that any approvals will be received via push notification + // set keypair and create hub connection so that any approvals will be received via push notification + this.authRequestKeyPair = { privateKey: adminAuthReqStorable.privateKey, publicKey: null }; await this.anonymousHubService.createHubConnection(adminAuthReqStorable.id); } From 94fe9bd053b24211032430258416b4d7116b93f4 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Wed, 24 Apr 2024 11:20:13 -0400 Subject: [PATCH 272/351] Remove `StateService` `useAccountCache` (#8882) * Remove Account Cache from StateService * Remove Extra Change * Fix Desktop Build --- .../state-service.factory.ts | 2 - .../services/browser-state.service.spec.ts | 4 -- .../services/default-browser-state.service.ts | 40 ------------------- .../src/app/services/services.module.ts | 2 - apps/desktop/src/main.ts | 1 - apps/web/src/app/core/core.module.ts | 5 --- apps/web/src/app/core/state/state.service.ts | 3 -- libs/angular/src/services/injection-tokens.ts | 1 - .../src/services/jslib-services.module.ts | 6 --- .../src/platform/services/state.service.ts | 31 -------------- 10 files changed, 95 deletions(-) diff --git a/apps/browser/src/platform/background/service-factories/state-service.factory.ts b/apps/browser/src/platform/background/service-factories/state-service.factory.ts index 5567e00990..026a29668e 100644 --- a/apps/browser/src/platform/background/service-factories/state-service.factory.ts +++ b/apps/browser/src/platform/background/service-factories/state-service.factory.ts @@ -30,7 +30,6 @@ import { type StateServiceFactoryOptions = FactoryOptions & { stateServiceOptions: { - useAccountCache?: boolean; stateFactory: StateFactory<GlobalState, Account>; }; }; @@ -64,7 +63,6 @@ export async function stateServiceFactory( await environmentServiceFactory(cache, opts), await tokenServiceFactory(cache, opts), await migrationRunnerFactory(cache, opts), - opts.stateServiceOptions.useAccountCache, ), ); // TODO: If we run migration through a chrome installed/updated event we can turn off running migrations diff --git a/apps/browser/src/platform/services/browser-state.service.spec.ts b/apps/browser/src/platform/services/browser-state.service.spec.ts index 8f43998321..f06126dcf5 100644 --- a/apps/browser/src/platform/services/browser-state.service.spec.ts +++ b/apps/browser/src/platform/services/browser-state.service.spec.ts @@ -27,7 +27,6 @@ describe("Browser State Service", () => { let diskStorageService: MockProxy<AbstractStorageService>; let logService: MockProxy<LogService>; let stateFactory: MockProxy<StateFactory<GlobalState, Account>>; - let useAccountCache: boolean; let environmentService: MockProxy<EnvironmentService>; let tokenService: MockProxy<TokenService>; let migrationRunner: MockProxy<MigrationRunner>; @@ -46,8 +45,6 @@ describe("Browser State Service", () => { environmentService = mock(); tokenService = mock(); migrationRunner = mock(); - // turn off account cache for tests - useAccountCache = false; state = new State(new GlobalState()); state.accounts[userId] = new Account({ @@ -78,7 +75,6 @@ describe("Browser State Service", () => { environmentService, tokenService, migrationRunner, - useAccountCache, ); }); diff --git a/apps/browser/src/platform/services/default-browser-state.service.ts b/apps/browser/src/platform/services/default-browser-state.service.ts index f1f306dbc0..b9cd219076 100644 --- a/apps/browser/src/platform/services/default-browser-state.service.ts +++ b/apps/browser/src/platform/services/default-browser-state.service.ts @@ -15,7 +15,6 @@ import { MigrationRunner } from "@bitwarden/common/platform/services/migration-r import { StateService as BaseStateService } from "@bitwarden/common/platform/services/state.service"; import { Account } from "../../models/account"; -import { BrowserApi } from "../browser/browser-api"; import { browserSession, sessionSync } from "../decorators/session-sync-observable"; import { BrowserStateService } from "./abstractions/browser-state.service"; @@ -45,7 +44,6 @@ export class DefaultBrowserStateService environmentService: EnvironmentService, tokenService: TokenService, migrationRunner: MigrationRunner, - useAccountCache = true, ) { super( storageService, @@ -57,45 +55,7 @@ export class DefaultBrowserStateService environmentService, tokenService, migrationRunner, - useAccountCache, ); - - // TODO: This is a hack to fix having a disk cache on both the popup and - // the background page that can get out of sync. We need to work out the - // best way to handle caching with multiple instances of the state service. - if (useAccountCache) { - BrowserApi.storageChangeListener((changes, namespace) => { - if (namespace === "local") { - for (const key of Object.keys(changes)) { - if (key !== "accountActivity" && this.accountDiskCache.value[key]) { - this.deleteDiskCache(key); - } - } - } - }); - - BrowserApi.addListener( - chrome.runtime.onMessage, - (message: { command: string }, _, respond) => { - if (message.command === "initializeDiskCache") { - respond(JSON.stringify(this.accountDiskCache.value)); - } - }, - ); - } - } - - override async initAccountState(): Promise<void> { - if (this.isRecoveredSession && this.useAccountCache) { - // request cache initialization - - const response = await BrowserApi.sendMessageWithResponse<string>("initializeDiskCache"); - this.accountDiskCache.next(JSON.parse(response)); - - return; - } - - await super.initAccountState(); } async addAccount(account: Account) { diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index d1d51c0f1c..c15743ba5c 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -4,7 +4,6 @@ import { Subject, merge } from "rxjs"; import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider"; import { SECURE_STORAGE, - STATE_SERVICE_USE_CACHE, LOCALES_DIRECTORY, SYSTEM_LANGUAGE, MEMORY_STORAGE, @@ -205,7 +204,6 @@ const safeProviders: SafeProvider[] = [ EnvironmentService, TokenService, MigrationRunner, - STATE_SERVICE_USE_CACHE, ], }), safeProvider({ diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index bffd2002ff..da4c14b4aa 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -205,7 +205,6 @@ export class Main { this.environmentService, this.tokenService, this.migrationRunner, - false, // Do not use disk caching because this will get out of sync with the renderer service ); this.desktopSettingsService = new DesktopSettingsService(stateProvider); diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index a274764756..7a95650039 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -5,7 +5,6 @@ import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/sa import { SECURE_STORAGE, STATE_FACTORY, - STATE_SERVICE_USE_CACHE, LOCALES_DIRECTORY, SYSTEM_LANGUAGE, MEMORY_STORAGE, @@ -78,10 +77,6 @@ const safeProviders: SafeProvider[] = [ provide: STATE_FACTORY, useValue: new StateFactory(GlobalState, Account), }), - safeProvider({ - provide: STATE_SERVICE_USE_CACHE, - useValue: false, - }), safeProvider({ provide: I18nServiceAbstraction, useClass: I18nService, diff --git a/apps/web/src/app/core/state/state.service.ts b/apps/web/src/app/core/state/state.service.ts index 1ae62d8591..185509e150 100644 --- a/apps/web/src/app/core/state/state.service.ts +++ b/apps/web/src/app/core/state/state.service.ts @@ -4,7 +4,6 @@ import { MEMORY_STORAGE, SECURE_STORAGE, STATE_FACTORY, - STATE_SERVICE_USE_CACHE, } from "@bitwarden/angular/services/injection-tokens"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; @@ -34,7 +33,6 @@ export class StateService extends BaseStateService<GlobalState, Account> { environmentService: EnvironmentService, tokenService: TokenService, migrationRunner: MigrationRunner, - @Inject(STATE_SERVICE_USE_CACHE) useAccountCache = true, ) { super( storageService, @@ -46,7 +44,6 @@ export class StateService extends BaseStateService<GlobalState, Account> { environmentService, tokenService, migrationRunner, - useAccountCache, ); } diff --git a/libs/angular/src/services/injection-tokens.ts b/libs/angular/src/services/injection-tokens.ts index 6fffe722fb..413fc5b530 100644 --- a/libs/angular/src/services/injection-tokens.ts +++ b/libs/angular/src/services/injection-tokens.ts @@ -36,7 +36,6 @@ export const MEMORY_STORAGE = new SafeInjectionToken<AbstractMemoryStorageServic ); export const SECURE_STORAGE = new SafeInjectionToken<AbstractStorageService>("SECURE_STORAGE"); export const STATE_FACTORY = new SafeInjectionToken<StateFactory>("STATE_FACTORY"); -export const STATE_SERVICE_USE_CACHE = new SafeInjectionToken<boolean>("STATE_SERVICE_USE_CACHE"); export const LOGOUT_CALLBACK = new SafeInjectionToken< (expired: boolean, userId?: string) => Promise<void> >("LOGOUT_CALLBACK"); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index f31bcb1c51..a63f4f0f7d 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -269,7 +269,6 @@ import { SafeInjectionToken, SECURE_STORAGE, STATE_FACTORY, - STATE_SERVICE_USE_CACHE, SUPPORTS_SECURE_STORAGE, SYSTEM_LANGUAGE, SYSTEM_THEME_OBSERVABLE, @@ -313,10 +312,6 @@ const safeProviders: SafeProvider[] = [ provide: STATE_FACTORY, useValue: new StateFactory(GlobalState, Account), }), - safeProvider({ - provide: STATE_SERVICE_USE_CACHE, - useValue: true, - }), safeProvider({ provide: LOGOUT_CALLBACK, useFactory: @@ -690,7 +685,6 @@ const safeProviders: SafeProvider[] = [ EnvironmentService, TokenServiceAbstraction, MigrationRunner, - STATE_SERVICE_USE_CACHE, ], }), safeProvider({ diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index 412176e235..8758f6d200 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -65,8 +65,6 @@ export class StateService< private hasBeenInited = false; protected isRecoveredSession = false; - protected accountDiskCache = new BehaviorSubject<Record<string, TAccount>>({}); - // default account serializer, must be overridden by child class protected accountDeserializer = Account.fromJSON as (json: Jsonify<TAccount>) => TAccount; @@ -80,7 +78,6 @@ export class StateService< protected environmentService: EnvironmentService, protected tokenService: TokenService, private migrationRunner: MigrationRunner, - protected useAccountCache: boolean = true, ) {} async init(initOptions: InitOptions = {}): Promise<void> { @@ -995,13 +992,6 @@ export class StateService< return null; } - if (this.useAccountCache) { - const cachedAccount = this.accountDiskCache.value[options.userId]; - if (cachedAccount != null) { - return cachedAccount; - } - } - const account = options?.useSecureStorage ? (await this.secureStorageService.get<TAccount>(options.userId, options)) ?? (await this.storageService.get<TAccount>( @@ -1009,8 +999,6 @@ export class StateService< this.reconcileOptions(options, { htmlStorageLocation: HtmlStorageLocation.Local }), )) : await this.storageService.get<TAccount>(options.userId, options); - - this.setDiskCache(options.userId, account); return account; } @@ -1040,8 +1028,6 @@ export class StateService< : this.storageService; await storageLocation.save(`${options.userId}`, account, options); - - this.deleteDiskCache(options.userId); } protected async saveAccountToMemory(account: TAccount): Promise<void> { @@ -1241,9 +1227,6 @@ export class StateService< await this.updateState(async (state) => { userId = userId ?? state.activeUserId; delete state.accounts[userId]; - - this.deleteDiskCache(userId); - return state; }); } @@ -1357,20 +1340,6 @@ export class StateService< return await this.setState(updatedState); }); } - - private setDiskCache(key: string, value: TAccount, options?: StorageOptions) { - if (this.useAccountCache) { - this.accountDiskCache.value[key] = value; - this.accountDiskCache.next(this.accountDiskCache.value); - } - } - - protected deleteDiskCache(key: string) { - if (this.useAccountCache) { - delete this.accountDiskCache.value[key]; - this.accountDiskCache.next(this.accountDiskCache.value); - } - } } function withPrototypeForArrayMembers<T>( From a12c140792c708073623356b45f0dfef58566cb0 Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Wed, 24 Apr 2024 12:37:19 -0400 Subject: [PATCH 273/351] =?UTF-8?q?Revert=20"Revert=20"Auth/PM-6689=20-=20?= =?UTF-8?q?Migrate=20Security=20Stamp=20to=20Token=20Service=20and=20St?= =?UTF-8?q?=E2=80=A6"=20(#8889)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 100b43dd8f7ac23cb888b0f031353aa68beffb82. --- .../browser/src/background/main.background.ts | 1 + apps/cli/src/bw.ts | 1 + .../src/services/jslib-services.module.ts | 1 + .../login-strategies/login.strategy.spec.ts | 4 - .../common/login-strategies/login.strategy.ts | 9 +-- .../src/auth/abstractions/token.service.ts | 6 ++ .../src/auth/services/token.service.spec.ts | 79 +++++++++++++++++++ .../common/src/auth/services/token.service.ts | 25 ++++++ .../src/auth/services/token.state.spec.ts | 2 + libs/common/src/auth/services/token.state.ts | 5 ++ .../platform/abstractions/state.service.ts | 2 - .../models/domain/account-tokens.spec.ts | 9 --- .../platform/models/domain/account.spec.ts | 4 +- .../src/platform/models/domain/account.ts | 18 ----- .../src/platform/services/state.service.ts | 17 ---- .../src/vault/services/sync/sync.service.ts | 6 +- 16 files changed, 126 insertions(+), 63 deletions(-) delete mode 100644 libs/common/src/platform/models/domain/account-tokens.spec.ts diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 6069e9b5e9..bee102be46 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -813,6 +813,7 @@ export default class MainBackground { this.avatarService, logoutCallback, this.billingAccountProfileStateService, + this.tokenService, ); this.eventUploadService = new EventUploadService( this.apiService, diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index 58329128b8..437f807bc6 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -631,6 +631,7 @@ export class Main { this.avatarService, async (expired: boolean) => await this.logout(), this.billingAccountProfileStateService, + this.tokenService, ); this.totpService = new TotpService(this.cryptoFunctionService, this.logService); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index a63f4f0f7d..45f11befa6 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -623,6 +623,7 @@ const safeProviders: SafeProvider[] = [ AvatarServiceAbstraction, LOGOUT_CALLBACK, BillingAccountProfileStateService, + TokenServiceAbstraction, ], }), safeProvider({ 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 431f736e94..e0833342ce 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.spec.ts @@ -27,7 +27,6 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Account, AccountProfile, - AccountTokens, AccountKeys, } from "@bitwarden/common/platform/models/domain/account"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; @@ -213,9 +212,6 @@ describe("LoginStrategy", () => { kdfType: kdf, }, }, - tokens: { - ...new AccountTokens(), - }, keys: new AccountKeys(), }), ); diff --git a/libs/auth/src/common/login-strategies/login.strategy.ts b/libs/auth/src/common/login-strategies/login.strategy.ts index a6dc193183..a73c32e120 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.ts @@ -27,11 +27,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 { - Account, - AccountProfile, - AccountTokens, -} from "@bitwarden/common/platform/models/domain/account"; +import { Account, AccountProfile } from "@bitwarden/common/platform/models/domain/account"; import { UserId } from "@bitwarden/common/types/guid"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; @@ -192,9 +188,6 @@ export abstract class LoginStrategy { kdfType: tokenResponse.kdf, }, }, - tokens: { - ...new AccountTokens(), - }, }), ); diff --git a/libs/common/src/auth/abstractions/token.service.ts b/libs/common/src/auth/abstractions/token.service.ts index 75bb383882..fc3bd317f4 100644 --- a/libs/common/src/auth/abstractions/token.service.ts +++ b/libs/common/src/auth/abstractions/token.service.ts @@ -213,4 +213,10 @@ export abstract class TokenService { * @returns A promise that resolves with a boolean representing the user's external authN status. */ getIsExternal: () => Promise<boolean>; + + /** Gets the active or passed in user's security stamp */ + getSecurityStamp: (userId?: UserId) => Promise<string | null>; + + /** Sets the security stamp for the active or passed in user */ + setSecurityStamp: (securityStamp: string, userId?: UserId) => Promise<void>; } diff --git a/libs/common/src/auth/services/token.service.spec.ts b/libs/common/src/auth/services/token.service.spec.ts index d32c4d8e1c..3e92053d2f 100644 --- a/libs/common/src/auth/services/token.service.spec.ts +++ b/libs/common/src/auth/services/token.service.spec.ts @@ -23,6 +23,7 @@ import { EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL, REFRESH_TOKEN_DISK, REFRESH_TOKEN_MEMORY, + SECURITY_STAMP_MEMORY, } from "./token.state"; describe("TokenService", () => { @@ -2191,6 +2192,84 @@ describe("TokenService", () => { }); }); + describe("Security Stamp methods", () => { + const mockSecurityStamp = "securityStamp"; + + describe("setSecurityStamp", () => { + it("should throw an error if no user id is provided and there is no active user in global state", async () => { + // Act + // note: don't await here because we want to test the error + const result = tokenService.setSecurityStamp(mockSecurityStamp); + // Assert + await expect(result).rejects.toThrow("User id not found. Cannot set security stamp."); + }); + + it("should set the security stamp in memory when there is an active user in global state", async () => { + // Arrange + globalStateProvider + .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) + .stateSubject.next(userIdFromAccessToken); + + // Act + await tokenService.setSecurityStamp(mockSecurityStamp); + + // Assert + expect( + singleUserStateProvider.getFake(userIdFromAccessToken, SECURITY_STAMP_MEMORY).nextMock, + ).toHaveBeenCalledWith(mockSecurityStamp); + }); + + it("should set the security stamp in memory for the specified user id", async () => { + // Act + await tokenService.setSecurityStamp(mockSecurityStamp, userIdFromAccessToken); + + // Assert + expect( + singleUserStateProvider.getFake(userIdFromAccessToken, SECURITY_STAMP_MEMORY).nextMock, + ).toHaveBeenCalledWith(mockSecurityStamp); + }); + }); + + describe("getSecurityStamp", () => { + it("should throw an error if no user id is provided and there is no active user in global state", async () => { + // Act + // note: don't await here because we want to test the error + const result = tokenService.getSecurityStamp(); + // Assert + await expect(result).rejects.toThrow("User id not found. Cannot get security stamp."); + }); + + it("should return the security stamp from memory with no user id specified (uses global active user)", async () => { + // Arrange + globalStateProvider + .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) + .stateSubject.next(userIdFromAccessToken); + + singleUserStateProvider + .getFake(userIdFromAccessToken, SECURITY_STAMP_MEMORY) + .stateSubject.next([userIdFromAccessToken, mockSecurityStamp]); + + // Act + const result = await tokenService.getSecurityStamp(); + + // Assert + expect(result).toEqual(mockSecurityStamp); + }); + + it("should return the security stamp from memory for the specified user id", async () => { + // Arrange + singleUserStateProvider + .getFake(userIdFromAccessToken, SECURITY_STAMP_MEMORY) + .stateSubject.next([userIdFromAccessToken, mockSecurityStamp]); + + // Act + const result = await tokenService.getSecurityStamp(userIdFromAccessToken); + // Assert + expect(result).toEqual(mockSecurityStamp); + }); + }); + }); + // Helpers function createTokenService(supportsSecureStorage: boolean) { return new TokenService( diff --git a/libs/common/src/auth/services/token.service.ts b/libs/common/src/auth/services/token.service.ts index c24a2c186b..40036a8453 100644 --- a/libs/common/src/auth/services/token.service.ts +++ b/libs/common/src/auth/services/token.service.ts @@ -32,6 +32,7 @@ import { EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL, REFRESH_TOKEN_DISK, REFRESH_TOKEN_MEMORY, + SECURITY_STAMP_MEMORY, } from "./token.state"; export enum TokenStorageLocation { @@ -850,6 +851,30 @@ export class TokenService implements TokenServiceAbstraction { return Array.isArray(decoded.amr) && decoded.amr.includes("external"); } + async getSecurityStamp(userId?: UserId): Promise<string | null> { + userId ??= await firstValueFrom(this.activeUserIdGlobalState.state$); + + if (!userId) { + throw new Error("User id not found. Cannot get security stamp."); + } + + const securityStamp = await this.getStateValueByUserIdAndKeyDef(userId, SECURITY_STAMP_MEMORY); + + return securityStamp; + } + + async setSecurityStamp(securityStamp: string, userId?: UserId): Promise<void> { + userId ??= await firstValueFrom(this.activeUserIdGlobalState.state$); + + if (!userId) { + throw new Error("User id not found. Cannot set security stamp."); + } + + await this.singleUserStateProvider + .get(userId, SECURITY_STAMP_MEMORY) + .update((_) => securityStamp); + } + private async getStateValueByUserIdAndKeyDef( userId: UserId, storageLocation: UserKeyDefinition<string>, diff --git a/libs/common/src/auth/services/token.state.spec.ts b/libs/common/src/auth/services/token.state.spec.ts index dc00fec383..bb82410fac 100644 --- a/libs/common/src/auth/services/token.state.spec.ts +++ b/libs/common/src/auth/services/token.state.spec.ts @@ -10,6 +10,7 @@ import { EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL, REFRESH_TOKEN_DISK, REFRESH_TOKEN_MEMORY, + SECURITY_STAMP_MEMORY, } from "./token.state"; describe.each([ @@ -22,6 +23,7 @@ describe.each([ [API_KEY_CLIENT_ID_MEMORY, "apiKeyClientIdMemory"], [API_KEY_CLIENT_SECRET_DISK, "apiKeyClientSecretDisk"], [API_KEY_CLIENT_SECRET_MEMORY, "apiKeyClientSecretMemory"], + [SECURITY_STAMP_MEMORY, "securityStamp"], ])( "deserializes state key definitions", ( diff --git a/libs/common/src/auth/services/token.state.ts b/libs/common/src/auth/services/token.state.ts index 458d6846c1..57d85f2a55 100644 --- a/libs/common/src/auth/services/token.state.ts +++ b/libs/common/src/auth/services/token.state.ts @@ -69,3 +69,8 @@ export const API_KEY_CLIENT_SECRET_MEMORY = new UserKeyDefinition<string>( clearOn: [], // Manually handled }, ); + +export const SECURITY_STAMP_MEMORY = new UserKeyDefinition<string>(TOKEN_MEMORY, "securityStamp", { + deserializer: (securityStamp) => securityStamp, + clearOn: ["logout"], +}); diff --git a/libs/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index 051604f0ae..f1d4b3848e 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -181,8 +181,6 @@ export abstract class StateService<T extends Account = Account> { * Sets the user's Pin, encrypted by the user key */ setProtectedPin: (value: string, options?: StorageOptions) => Promise<void>; - getSecurityStamp: (options?: StorageOptions) => Promise<string>; - setSecurityStamp: (value: string, options?: StorageOptions) => Promise<void>; getUserId: (options?: StorageOptions) => Promise<string>; getVaultTimeout: (options?: StorageOptions) => Promise<number>; setVaultTimeout: (value: number, options?: StorageOptions) => Promise<void>; diff --git a/libs/common/src/platform/models/domain/account-tokens.spec.ts b/libs/common/src/platform/models/domain/account-tokens.spec.ts deleted file mode 100644 index 733b3908e9..0000000000 --- a/libs/common/src/platform/models/domain/account-tokens.spec.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { AccountTokens } from "./account"; - -describe("AccountTokens", () => { - describe("fromJSON", () => { - it("should deserialize to an instance of itself", () => { - expect(AccountTokens.fromJSON({})).toBeInstanceOf(AccountTokens); - }); - }); -}); diff --git a/libs/common/src/platform/models/domain/account.spec.ts b/libs/common/src/platform/models/domain/account.spec.ts index 0c76c16cc2..77c242b6ff 100644 --- a/libs/common/src/platform/models/domain/account.spec.ts +++ b/libs/common/src/platform/models/domain/account.spec.ts @@ -1,4 +1,4 @@ -import { Account, AccountKeys, AccountProfile, AccountSettings, AccountTokens } from "./account"; +import { Account, AccountKeys, AccountProfile, AccountSettings } from "./account"; describe("Account", () => { describe("fromJSON", () => { @@ -10,14 +10,12 @@ describe("Account", () => { const keysSpy = jest.spyOn(AccountKeys, "fromJSON"); const profileSpy = jest.spyOn(AccountProfile, "fromJSON"); const settingsSpy = jest.spyOn(AccountSettings, "fromJSON"); - const tokensSpy = jest.spyOn(AccountTokens, "fromJSON"); Account.fromJSON({}); expect(keysSpy).toHaveBeenCalled(); expect(profileSpy).toHaveBeenCalled(); expect(settingsSpy).toHaveBeenCalled(); - expect(tokensSpy).toHaveBeenCalled(); }); }); }); diff --git a/libs/common/src/platform/models/domain/account.ts b/libs/common/src/platform/models/domain/account.ts index 5a9a764696..cd416ec1f9 100644 --- a/libs/common/src/platform/models/domain/account.ts +++ b/libs/common/src/platform/models/domain/account.ts @@ -171,24 +171,11 @@ export class AccountSettings { } } -export class AccountTokens { - securityStamp?: string; - - static fromJSON(obj: Jsonify<AccountTokens>): AccountTokens { - if (obj == null) { - return null; - } - - return Object.assign(new AccountTokens(), obj); - } -} - export class Account { data?: AccountData = new AccountData(); keys?: AccountKeys = new AccountKeys(); profile?: AccountProfile = new AccountProfile(); settings?: AccountSettings = new AccountSettings(); - tokens?: AccountTokens = new AccountTokens(); constructor(init: Partial<Account>) { Object.assign(this, { @@ -208,10 +195,6 @@ export class Account { ...new AccountSettings(), ...init?.settings, }, - tokens: { - ...new AccountTokens(), - ...init?.tokens, - }, }); } @@ -225,7 +208,6 @@ export class Account { data: AccountData.fromJSON(json?.data), profile: AccountProfile.fromJSON(json?.profile), settings: AccountSettings.fromJSON(json?.settings), - tokens: AccountTokens.fromJSON(json?.tokens), }); } } diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index 8758f6d200..0c7cdd22d2 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -841,23 +841,6 @@ export class StateService< ); } - async getSecurityStamp(options?: StorageOptions): Promise<string> { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) - )?.tokens?.securityStamp; - } - - async setSecurityStamp(value: string, options?: StorageOptions): Promise<void> { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - account.tokens.securityStamp = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - } - async getUserId(options?: StorageOptions): Promise<string> { return ( await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) diff --git a/libs/common/src/vault/services/sync/sync.service.ts b/libs/common/src/vault/services/sync/sync.service.ts index ff8e9f1f4f..73869ff488 100644 --- a/libs/common/src/vault/services/sync/sync.service.ts +++ b/libs/common/src/vault/services/sync/sync.service.ts @@ -15,6 +15,7 @@ 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 { TokenService } from "../../../auth/abstractions/token.service"; 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"; @@ -73,6 +74,7 @@ export class SyncService implements SyncServiceAbstraction { private avatarService: AvatarService, private logoutCallback: (expired: boolean) => Promise<void>, private billingAccountProfileStateService: BillingAccountProfileStateService, + private tokenService: TokenService, ) {} async getLastSync(): Promise<Date> { @@ -309,7 +311,7 @@ export class SyncService implements SyncServiceAbstraction { } private async syncProfile(response: ProfileResponse) { - const stamp = await this.stateService.getSecurityStamp(); + const stamp = await this.tokenService.getSecurityStamp(response.id as UserId); if (stamp != null && stamp !== response.securityStamp) { if (this.logoutCallback != null) { await this.logoutCallback(true); @@ -323,7 +325,7 @@ export class SyncService implements SyncServiceAbstraction { await this.cryptoService.setProviderKeys(response.providers); await this.cryptoService.setOrgKeys(response.organizations, response.providerOrganizations); await this.avatarService.setSyncAvatarColor(response.id as UserId, response.avatarColor); - await this.stateService.setSecurityStamp(response.securityStamp); + await this.tokenService.setSecurityStamp(response.securityStamp, response.id as UserId); await this.stateService.setEmailVerified(response.emailVerified); await this.billingAccountProfileStateService.setHasPremium( From 5dc83cd34c9a5e93f1fce7a614af6af11b3e383e Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Wed, 24 Apr 2024 12:54:54 -0400 Subject: [PATCH 274/351] PM-6787 - Rename DeviceTrustCryptoService to DeviceTrustService (#8819) --- ...ory.ts => device-trust-service.factory.ts} | 20 ++-- .../login-strategy-service.factory.ts | 10 +- apps/browser/src/auth/popup/lock.component.ts | 6 +- .../popup/login-via-auth-request.component.ts | 6 +- .../browser/src/background/main.background.ts | 10 +- .../src/popup/services/services.module.ts | 6 +- apps/cli/src/bw.ts | 10 +- apps/desktop/src/auth/lock.component.spec.ts | 6 +- apps/desktop/src/auth/lock.component.ts | 6 +- .../login/login-via-auth-request.component.ts | 6 +- .../user-key-rotation.service.spec.ts | 8 +- .../key-rotation/user-key-rotation.service.ts | 6 +- ...base-login-decryption-options.component.ts | 12 +- .../src/auth/components/lock.component.ts | 6 +- .../login-via-auth-request.component.ts | 6 +- libs/angular/src/auth/guards/lock.guard.ts | 6 +- .../angular/src/auth/guards/redirect.guard.ts | 6 +- .../guards/tde-decryption-required.guard.ts | 6 +- .../src/services/jslib-services.module.ts | 10 +- .../auth-request-login.strategy.spec.ts | 12 +- .../auth-request-login.strategy.ts | 6 +- .../sso-login.strategy.spec.ts | 38 +++--- .../login-strategies/sso-login.strategy.ts | 10 +- .../login-strategy.service.spec.ts | 8 +- .../login-strategy.service.ts | 8 +- ...ts => device-trust.service.abstraction.ts} | 5 +- ...=> device-trust.service.implementation.ts} | 4 +- ...e.spec.ts => device-trust.service.spec.ts} | 111 ++++++++---------- libs/common/src/state-migrations/migrate.ts | 4 +- ...vice-trust-svc-to-state-providers.spec.ts} | 12 +- ...te-device-trust-svc-to-state-providers.ts} | 4 +- ...resh-token-migrated-state-provider-flag.ts | 2 +- 32 files changed, 182 insertions(+), 194 deletions(-) rename apps/browser/src/auth/background/service-factories/{device-trust-crypto-service.factory.ts => device-trust-service.factory.ts} (79%) rename libs/common/src/auth/abstractions/{device-trust-crypto.service.abstraction.ts => device-trust.service.abstraction.ts} (89%) rename libs/common/src/auth/services/{device-trust-crypto.service.implementation.ts => device-trust.service.implementation.ts} (98%) rename libs/common/src/auth/services/{device-trust-crypto.service.spec.ts => device-trust.service.spec.ts} (86%) rename libs/common/src/state-migrations/migrations/{53-migrate-device-trust-crypto-svc-to-state-providers.spec.ts => 53-migrate-device-trust-svc-to-state-providers.spec.ts} (92%) rename libs/common/src/state-migrations/migrations/{53-migrate-device-trust-crypto-svc-to-state-providers.ts => 53-migrate-device-trust-svc-to-state-providers.ts} (94%) 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-service.factory.ts similarity index 79% rename from apps/browser/src/auth/background/service-factories/device-trust-crypto-service.factory.ts rename to apps/browser/src/auth/background/service-factories/device-trust-service.factory.ts index cac6f9bbe8..106bcbcf72 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-service.factory.ts @@ -1,5 +1,5 @@ -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; -import { DeviceTrustCryptoService } from "@bitwarden/common/auth/services/device-trust-crypto.service.implementation"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; +import { DeviceTrustService } from "@bitwarden/common/auth/services/device-trust.service.implementation"; import { DevicesApiServiceInitOptions, @@ -52,9 +52,9 @@ import { userDecryptionOptionsServiceFactory, } from "./user-decryption-options-service.factory"; -type DeviceTrustCryptoServiceFactoryOptions = FactoryOptions; +type DeviceTrustServiceFactoryOptions = FactoryOptions; -export type DeviceTrustCryptoServiceInitOptions = DeviceTrustCryptoServiceFactoryOptions & +export type DeviceTrustServiceInitOptions = DeviceTrustServiceFactoryOptions & KeyGenerationServiceInitOptions & CryptoFunctionServiceInitOptions & CryptoServiceInitOptions & @@ -67,16 +67,16 @@ export type DeviceTrustCryptoServiceInitOptions = DeviceTrustCryptoServiceFactor SecureStorageServiceInitOptions & UserDecryptionOptionsServiceInitOptions; -export function deviceTrustCryptoServiceFactory( - cache: { deviceTrustCryptoService?: DeviceTrustCryptoServiceAbstraction } & CachedServices, - opts: DeviceTrustCryptoServiceInitOptions, -): Promise<DeviceTrustCryptoServiceAbstraction> { +export function deviceTrustServiceFactory( + cache: { deviceTrustService?: DeviceTrustServiceAbstraction } & CachedServices, + opts: DeviceTrustServiceInitOptions, +): Promise<DeviceTrustServiceAbstraction> { return factory( cache, - "deviceTrustCryptoService", + "deviceTrustService", opts, async () => - new DeviceTrustCryptoService( + new DeviceTrustService( await keyGenerationServiceFactory(cache, opts), await cryptoFunctionServiceFactory(cache, opts), await cryptoServiceFactory(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 f184072cce..075ba614b7 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 @@ -65,9 +65,9 @@ import { AuthRequestServiceInitOptions, } from "./auth-request-service.factory"; import { - deviceTrustCryptoServiceFactory, - DeviceTrustCryptoServiceInitOptions, -} from "./device-trust-crypto-service.factory"; + deviceTrustServiceFactory, + DeviceTrustServiceInitOptions, +} from "./device-trust-service.factory"; import { keyConnectorServiceFactory, KeyConnectorServiceInitOptions, @@ -102,7 +102,7 @@ export type LoginStrategyServiceInitOptions = LoginStrategyServiceFactoryOptions EncryptServiceInitOptions & PolicyServiceInitOptions & PasswordStrengthServiceInitOptions & - DeviceTrustCryptoServiceInitOptions & + DeviceTrustServiceInitOptions & AuthRequestServiceInitOptions & UserDecryptionOptionsServiceInitOptions & GlobalStateProviderInitOptions & @@ -135,7 +135,7 @@ export function loginStrategyServiceFactory( await encryptServiceFactory(cache, opts), await passwordStrengthServiceFactory(cache, opts), await policyServiceFactory(cache, opts), - await deviceTrustCryptoServiceFactory(cache, opts), + await deviceTrustServiceFactory(cache, opts), await authRequestServiceFactory(cache, opts), await internalUserDecryptionOptionServiceFactory(cache, opts), await globalStateProviderFactory(cache, opts), diff --git a/apps/browser/src/auth/popup/lock.component.ts b/apps/browser/src/auth/popup/lock.component.ts index 16c32337cf..78039d793f 100644 --- a/apps/browser/src/auth/popup/lock.component.ts +++ b/apps/browser/src/auth/popup/lock.component.ts @@ -11,7 +11,7 @@ import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abs 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 { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.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"; @@ -60,7 +60,7 @@ export class LockComponent extends BaseLockComponent { passwordStrengthService: PasswordStrengthServiceAbstraction, private authService: AuthService, dialogService: DialogService, - deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, + deviceTrustService: DeviceTrustServiceAbstraction, userVerificationService: UserVerificationService, pinCryptoService: PinCryptoServiceAbstraction, private routerService: BrowserRouterService, @@ -85,7 +85,7 @@ export class LockComponent extends BaseLockComponent { policyService, passwordStrengthService, dialogService, - deviceTrustCryptoService, + deviceTrustService, userVerificationService, pinCryptoService, biometricStateService, 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 52f311ce7b..158296058e 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 @@ -12,7 +12,7 @@ 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 { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; 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"; @@ -47,7 +47,7 @@ export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent { stateService: StateService, loginEmailService: LoginEmailServiceAbstraction, syncService: SyncService, - deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, + deviceTrustService: DeviceTrustServiceAbstraction, authRequestService: AuthRequestServiceAbstraction, loginStrategyService: LoginStrategyServiceAbstraction, accountService: AccountService, @@ -69,7 +69,7 @@ export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent { validationService, stateService, loginEmailService, - deviceTrustCryptoService, + deviceTrustService, authRequestService, loginStrategyService, accountService, diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index bee102be46..dc93de7803 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -30,7 +30,7 @@ import { ProviderService } from "@bitwarden/common/admin-console/services/provid import { AccountService as AccountServiceAbstraction } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.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 { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; 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"; @@ -45,7 +45,7 @@ import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/for 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 { DeviceTrustService } from "@bitwarden/common/auth/services/device-trust.service.implementation"; 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"; @@ -318,7 +318,7 @@ export default class MainBackground { configApiService: ConfigApiServiceAbstraction; devicesApiService: DevicesApiServiceAbstraction; devicesService: DevicesServiceAbstraction; - deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction; + deviceTrustService: DeviceTrustServiceAbstraction; authRequestService: AuthRequestServiceAbstraction; accountService: AccountServiceAbstraction; globalStateProvider: GlobalStateProvider; @@ -612,7 +612,7 @@ export default class MainBackground { this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider); this.devicesApiService = new DevicesApiServiceImplementation(this.apiService); - this.deviceTrustCryptoService = new DeviceTrustCryptoService( + this.deviceTrustService = new DeviceTrustService( this.keyGenerationService, this.cryptoFunctionService, this.cryptoService, @@ -670,7 +670,7 @@ export default class MainBackground { this.encryptService, this.passwordStrengthService, this.policyService, - this.deviceTrustCryptoService, + this.deviceTrustService, this.authRequestService, this.userDecryptionOptionsService, this.globalStateProvider, diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index a7da6b7612..38068d1849 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -28,7 +28,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { PolicyService } 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 { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; @@ -250,8 +250,8 @@ const safeProviders: SafeProvider[] = [ deps: [], }), safeProvider({ - provide: DeviceTrustCryptoServiceAbstraction, - useFactory: getBgService<DeviceTrustCryptoServiceAbstraction>("deviceTrustCryptoService"), + provide: DeviceTrustServiceAbstraction, + useFactory: getBgService<DeviceTrustServiceAbstraction>("deviceTrustService"), deps: [], }), safeProvider({ diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index 437f807bc6..8163aa2945 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -28,13 +28,13 @@ import { ProviderApiService } from "@bitwarden/common/admin-console/services/pro 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 { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.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 { DeviceTrustService } from "@bitwarden/common/auth/services/device-trust.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"; @@ -217,7 +217,7 @@ export class Main { syncNotifierService: SyncNotifierService; sendApiService: SendApiService; devicesApiService: DevicesApiServiceAbstraction; - deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction; + deviceTrustService: DeviceTrustServiceAbstraction; authRequestService: AuthRequestService; configApiService: ConfigApiServiceAbstraction; configService: ConfigService; @@ -460,7 +460,7 @@ export class Main { this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider); this.devicesApiService = new DevicesApiServiceImplementation(this.apiService); - this.deviceTrustCryptoService = new DeviceTrustCryptoService( + this.deviceTrustService = new DeviceTrustService( this.keyGenerationService, this.cryptoFunctionService, this.cryptoService, @@ -505,7 +505,7 @@ export class Main { this.encryptService, this.passwordStrengthService, this.policyService, - this.deviceTrustCryptoService, + this.deviceTrustService, this.authRequestService, this.userDecryptionOptionsService, this.globalStateProvider, diff --git a/apps/desktop/src/auth/lock.component.spec.ts b/apps/desktop/src/auth/lock.component.spec.ts index c125eba022..480e443eab 100644 --- a/apps/desktop/src/auth/lock.component.spec.ts +++ b/apps/desktop/src/auth/lock.component.spec.ts @@ -13,7 +13,7 @@ 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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.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 { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; @@ -145,8 +145,8 @@ describe("LockComponent", () => { useValue: mock<DialogService>(), }, { - provide: DeviceTrustCryptoServiceAbstraction, - useValue: mock<DeviceTrustCryptoServiceAbstraction>(), + provide: DeviceTrustServiceAbstraction, + useValue: mock<DeviceTrustServiceAbstraction>(), }, { provide: UserVerificationService, diff --git a/apps/desktop/src/auth/lock.component.ts b/apps/desktop/src/auth/lock.component.ts index 16b58c5bbe..b8feef4ab5 100644 --- a/apps/desktop/src/auth/lock.component.ts +++ b/apps/desktop/src/auth/lock.component.ts @@ -10,7 +10,7 @@ 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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.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"; @@ -58,7 +58,7 @@ export class LockComponent extends BaseLockComponent { passwordStrengthService: PasswordStrengthServiceAbstraction, logService: LogService, dialogService: DialogService, - deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, + deviceTrustService: DeviceTrustServiceAbstraction, userVerificationService: UserVerificationService, pinCryptoService: PinCryptoServiceAbstraction, biometricStateService: BiometricStateService, @@ -82,7 +82,7 @@ export class LockComponent extends BaseLockComponent { policyService, passwordStrengthService, dialogService, - deviceTrustCryptoService, + deviceTrustService, userVerificationService, pinCryptoService, biometricStateService, 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 0a339030ba..2d0f560205 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 @@ -13,7 +13,7 @@ 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 { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; 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"; @@ -55,7 +55,7 @@ export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent { syncService: SyncService, stateService: StateService, loginEmailService: LoginEmailServiceAbstraction, - deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, + deviceTrustService: DeviceTrustServiceAbstraction, authRequestService: AuthRequestServiceAbstraction, loginStrategyService: LoginStrategyServiceAbstraction, accountService: AccountService, @@ -77,7 +77,7 @@ export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent { validationService, stateService, loginEmailService, - deviceTrustCryptoService, + deviceTrustService, authRequestService, loginStrategyService, accountService, 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 0997f18864..ed665fe773 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 @@ -1,7 +1,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject } from "rxjs"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.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"; @@ -42,7 +42,7 @@ describe("KeyRotationService", () => { let mockSendService: MockProxy<SendService>; let mockEmergencyAccessService: MockProxy<EmergencyAccessService>; let mockResetPasswordService: MockProxy<OrganizationUserResetPasswordService>; - let mockDeviceTrustCryptoService: MockProxy<DeviceTrustCryptoServiceAbstraction>; + let mockDeviceTrustService: MockProxy<DeviceTrustServiceAbstraction>; let mockCryptoService: MockProxy<CryptoService>; let mockEncryptService: MockProxy<EncryptService>; let mockStateService: MockProxy<StateService>; @@ -60,7 +60,7 @@ describe("KeyRotationService", () => { mockSendService = mock<SendService>(); mockEmergencyAccessService = mock<EmergencyAccessService>(); mockResetPasswordService = mock<OrganizationUserResetPasswordService>(); - mockDeviceTrustCryptoService = mock<DeviceTrustCryptoServiceAbstraction>(); + mockDeviceTrustService = mock<DeviceTrustServiceAbstraction>(); mockCryptoService = mock<CryptoService>(); mockEncryptService = mock<EncryptService>(); mockStateService = mock<StateService>(); @@ -74,7 +74,7 @@ describe("KeyRotationService", () => { mockSendService, mockEmergencyAccessService, mockResetPasswordService, - mockDeviceTrustCryptoService, + mockDeviceTrustService, mockCryptoService, mockEncryptService, mockStateService, 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 f5812d341a..2ff48809a0 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 @@ -2,7 +2,7 @@ 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 { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -33,7 +33,7 @@ export class UserKeyRotationService { private sendService: SendService, private emergencyAccessService: EmergencyAccessService, private resetPasswordService: OrganizationUserResetPasswordService, - private deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, + private deviceTrustService: DeviceTrustServiceAbstraction, private cryptoService: CryptoService, private encryptService: EncryptService, private stateService: StateService, @@ -96,7 +96,7 @@ export class UserKeyRotationService { } const activeAccount = await firstValueFrom(this.accountService.activeAccount$); - await this.deviceTrustCryptoService.rotateDevicesTrust( + await this.deviceTrustService.rotateDevicesTrust( activeAccount.id, newUserKey, masterPasswordHash, 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 8345bb9939..0e58c03a54 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 @@ -23,7 +23,7 @@ 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 { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.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"; @@ -93,7 +93,7 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy { protected apiService: ApiService, protected i18nService: I18nService, protected validationService: ValidationService, - protected deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, + protected deviceTrustService: DeviceTrustServiceAbstraction, protected platformUtilsService: PlatformUtilsService, protected userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, protected passwordResetEnrollmentService: PasswordResetEnrollmentServiceAbstraction, @@ -156,7 +156,7 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy { } private async setRememberDeviceDefaultValue() { - const rememberDeviceFromState = await this.deviceTrustCryptoService.getShouldTrustDevice( + const rememberDeviceFromState = await this.deviceTrustService.getShouldTrustDevice( this.activeAccountId, ); @@ -169,9 +169,7 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy { this.rememberDevice.valueChanges .pipe( switchMap((value) => - defer(() => - this.deviceTrustCryptoService.setShouldTrustDevice(this.activeAccountId, value), - ), + defer(() => this.deviceTrustService.setShouldTrustDevice(this.activeAccountId, value)), ), takeUntil(this.destroy$), ) @@ -288,7 +286,7 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy { await this.passwordResetEnrollmentService.enroll(this.data.organizationId); if (this.rememberDeviceForm.value.rememberDevice) { - await this.deviceTrustCryptoService.trustDevice(this.activeAccountId); + await this.deviceTrustService.trustDevice(this.activeAccountId); } } catch (error) { this.validationService.showError(error); diff --git a/libs/angular/src/auth/components/lock.component.ts b/libs/angular/src/auth/components/lock.component.ts index 9c2ed55357..927fbb27b1 100644 --- a/libs/angular/src/auth/components/lock.component.ts +++ b/libs/angular/src/auth/components/lock.component.ts @@ -11,7 +11,7 @@ import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abs 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 { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.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"; @@ -74,7 +74,7 @@ export class LockComponent implements OnInit, OnDestroy { protected policyService: InternalPolicyService, protected passwordStrengthService: PasswordStrengthServiceAbstraction, protected dialogService: DialogService, - protected deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, + protected deviceTrustService: DeviceTrustServiceAbstraction, protected userVerificationService: UserVerificationService, protected pinCryptoService: PinCryptoServiceAbstraction, protected biometricStateService: BiometricStateService, @@ -277,7 +277,7 @@ 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 const activeAccount = await firstValueFrom(this.accountService.activeAccount$); - await this.deviceTrustCryptoService.trustDeviceIfRequired(activeAccount.id); + await this.deviceTrustService.trustDeviceIfRequired(activeAccount.id); await this.doContinue(evaluatePasswordAfterUnlock); } 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 3b827669a5..a60468e244 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 @@ -12,7 +12,7 @@ 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 { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; 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"; @@ -86,7 +86,7 @@ export class LoginViaAuthRequestComponent private validationService: ValidationService, private stateService: StateService, private loginEmailService: LoginEmailServiceAbstraction, - private deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, + private deviceTrustService: DeviceTrustServiceAbstraction, private authRequestService: AuthRequestServiceAbstraction, private loginStrategyService: LoginStrategyServiceAbstraction, private accountService: AccountService, @@ -402,7 +402,7 @@ export class LoginViaAuthRequestComponent // Now that we have a decrypted user key in memory, we can check if we // need to establish trust on the current device const activeAccount = await firstValueFrom(this.accountService.activeAccount$); - await this.deviceTrustCryptoService.trustDeviceIfRequired(activeAccount.id); + await this.deviceTrustService.trustDeviceIfRequired(activeAccount.id); // TODO: don't forget to use auto enrollment service everywhere we trust device diff --git a/libs/angular/src/auth/guards/lock.guard.ts b/libs/angular/src/auth/guards/lock.guard.ts index 6f71d77a63..8cd5290ebc 100644 --- a/libs/angular/src/auth/guards/lock.guard.ts +++ b/libs/angular/src/auth/guards/lock.guard.ts @@ -8,7 +8,7 @@ import { import { firstValueFrom } from "rxjs"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.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 { ClientType } from "@bitwarden/common/enums"; @@ -30,7 +30,7 @@ export function lockGuard(): CanActivateFn { ) => { const authService = inject(AuthService); const cryptoService = inject(CryptoService); - const deviceTrustCryptoService = inject(DeviceTrustCryptoServiceAbstraction); + const deviceTrustService = inject(DeviceTrustServiceAbstraction); const platformUtilService = inject(PlatformUtilsService); const messagingService = inject(MessagingService); const router = inject(Router); @@ -53,7 +53,7 @@ export function lockGuard(): CanActivateFn { // User is authN and in locked state. - const tdeEnabled = await firstValueFrom(deviceTrustCryptoService.supportsDeviceTrust$); + const tdeEnabled = await firstValueFrom(deviceTrustService.supportsDeviceTrust$); // Create special exception which allows users to go from the login-initiated page to the lock page for the approve w/ MP flow // The MP check is necessary to prevent direct manual navigation from other locked state pages for users who don't have a MP diff --git a/libs/angular/src/auth/guards/redirect.guard.ts b/libs/angular/src/auth/guards/redirect.guard.ts index ca9152186d..0c43673c34 100644 --- a/libs/angular/src/auth/guards/redirect.guard.ts +++ b/libs/angular/src/auth/guards/redirect.guard.ts @@ -3,7 +3,7 @@ import { CanActivateFn, Router } from "@angular/router"; import { firstValueFrom } from "rxjs"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -31,7 +31,7 @@ export function redirectGuard(overrides: Partial<RedirectRoutes> = {}): CanActiv return async (route) => { const authService = inject(AuthService); const cryptoService = inject(CryptoService); - const deviceTrustCryptoService = inject(DeviceTrustCryptoServiceAbstraction); + const deviceTrustService = inject(DeviceTrustServiceAbstraction); const router = inject(Router); const authStatus = await authService.getAuthStatus(); @@ -46,7 +46,7 @@ export function redirectGuard(overrides: Partial<RedirectRoutes> = {}): CanActiv // If locked, TDE is enabled, and the user hasn't decrypted yet, then redirect to the // login decryption options component. - const tdeEnabled = await firstValueFrom(deviceTrustCryptoService.supportsDeviceTrust$); + const tdeEnabled = await firstValueFrom(deviceTrustService.supportsDeviceTrust$); const everHadUserKey = await firstValueFrom(cryptoService.everHadUserKey$); if (authStatus === AuthenticationStatus.Locked && tdeEnabled && !everHadUserKey) { return router.createUrlTree([routes.notDecrypted], { queryParams: route.queryParams }); diff --git a/libs/angular/src/auth/guards/tde-decryption-required.guard.ts b/libs/angular/src/auth/guards/tde-decryption-required.guard.ts index 146c6e19a2..524ce7dce5 100644 --- a/libs/angular/src/auth/guards/tde-decryption-required.guard.ts +++ b/libs/angular/src/auth/guards/tde-decryption-required.guard.ts @@ -8,7 +8,7 @@ import { import { firstValueFrom } from "rxjs"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -22,11 +22,11 @@ export function tdeDecryptionRequiredGuard(): CanActivateFn { return async (_: ActivatedRouteSnapshot, state: RouterStateSnapshot) => { const authService = inject(AuthService); const cryptoService = inject(CryptoService); - const deviceTrustCryptoService = inject(DeviceTrustCryptoServiceAbstraction); + const deviceTrustService = inject(DeviceTrustServiceAbstraction); const router = inject(Router); const authStatus = await authService.getAuthStatus(); - const tdeEnabled = await firstValueFrom(deviceTrustCryptoService.supportsDeviceTrust$); + const tdeEnabled = await firstValueFrom(deviceTrustService.supportsDeviceTrust$); const everHadUserKey = await firstValueFrom(cryptoService.everHadUserKey$); if (authStatus !== AuthenticationStatus.Locked || !tdeEnabled || everHadUserKey) { return router.createUrlTree(["/"]); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 45f11befa6..aabf823c0b 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -60,7 +60,7 @@ import { import { AnonymousHubService as AnonymousHubServiceAbstraction } from "@bitwarden/common/auth/abstractions/anonymous-hub.service"; import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.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 { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; 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"; @@ -82,7 +82,7 @@ import { AccountServiceImplementation } from "@bitwarden/common/auth/services/ac import { AnonymousHubService } from "@bitwarden/common/auth/services/anonymous-hub.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 { DeviceTrustService } from "@bitwarden/common/auth/services/device-trust.service.implementation"; 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"; @@ -385,7 +385,7 @@ const safeProviders: SafeProvider[] = [ EncryptService, PasswordStrengthServiceAbstraction, PolicyServiceAbstraction, - DeviceTrustCryptoServiceAbstraction, + DeviceTrustServiceAbstraction, AuthRequestServiceAbstraction, InternalUserDecryptionOptionsServiceAbstraction, GlobalStateProvider, @@ -949,8 +949,8 @@ const safeProviders: SafeProvider[] = [ deps: [DevicesApiServiceAbstraction], }), safeProvider({ - provide: DeviceTrustCryptoServiceAbstraction, - useClass: DeviceTrustCryptoService, + provide: DeviceTrustServiceAbstraction, + useClass: DeviceTrustService, deps: [ KeyGenerationServiceAbstraction, CryptoFunctionServiceAbstraction, 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 0ce6c9fed7..4e0b1ac3ac 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 @@ -1,7 +1,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; 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"; @@ -42,7 +42,7 @@ describe("AuthRequestLoginStrategy", () => { let stateService: MockProxy<StateService>; let twoFactorService: MockProxy<TwoFactorService>; let userDecryptionOptions: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>; - let deviceTrustCryptoService: MockProxy<DeviceTrustCryptoServiceAbstraction>; + let deviceTrustService: MockProxy<DeviceTrustServiceAbstraction>; let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>; const mockUserId = Utils.newGuid() as UserId; @@ -75,7 +75,7 @@ describe("AuthRequestLoginStrategy", () => { stateService = mock<StateService>(); twoFactorService = mock<TwoFactorService>(); userDecryptionOptions = mock<InternalUserDecryptionOptionsServiceAbstraction>(); - deviceTrustCryptoService = mock<DeviceTrustCryptoServiceAbstraction>(); + deviceTrustService = mock<DeviceTrustServiceAbstraction>(); billingAccountProfileStateService = mock<BillingAccountProfileStateService>(); accountService = mockAccountServiceWith(mockUserId); @@ -99,7 +99,7 @@ describe("AuthRequestLoginStrategy", () => { stateService, twoFactorService, userDecryptionOptions, - deviceTrustCryptoService, + deviceTrustService, billingAccountProfileStateService, ); @@ -132,7 +132,7 @@ describe("AuthRequestLoginStrategy", () => { ); expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key); expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey); - expect(deviceTrustCryptoService.trustDeviceIfRequired).toHaveBeenCalled(); + expect(deviceTrustService.trustDeviceIfRequired).toHaveBeenCalled(); expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey); }); @@ -160,6 +160,6 @@ describe("AuthRequestLoginStrategy", () => { expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey); // trustDeviceIfRequired should be called - expect(deviceTrustCryptoService.trustDeviceIfRequired).not.toHaveBeenCalled(); + expect(deviceTrustService.trustDeviceIfRequired).not.toHaveBeenCalled(); }); }); 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 4035a7be58..5220e432de 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 @@ -3,7 +3,6 @@ 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"; @@ -18,6 +17,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 { DeviceTrustServiceAbstraction } from "@bitwarden/common/src/auth/abstractions/device-trust.service.abstraction"; import { UserId } from "@bitwarden/common/types/guid"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; @@ -61,7 +61,7 @@ export class AuthRequestLoginStrategy extends LoginStrategy { stateService: StateService, twoFactorService: TwoFactorService, userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, - private deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, + private deviceTrustService: DeviceTrustServiceAbstraction, billingAccountProfileStateService: BillingAccountProfileStateService, ) { super( @@ -147,7 +147,7 @@ export class AuthRequestLoginStrategy extends LoginStrategy { await this.trySetUserKeyWithMasterKey(); // Establish trust if required after setting user key - await this.deviceTrustCryptoService.trustDeviceIfRequired(userId); + await this.deviceTrustService.trustDeviceIfRequired(userId); } } 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 b78ad6dea6..df33415247 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 @@ -1,7 +1,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; 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"; @@ -50,7 +50,7 @@ describe("SsoLoginStrategy", () => { let twoFactorService: MockProxy<TwoFactorService>; let userDecryptionOptionsService: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>; let keyConnectorService: MockProxy<KeyConnectorService>; - let deviceTrustCryptoService: MockProxy<DeviceTrustCryptoServiceAbstraction>; + let deviceTrustService: MockProxy<DeviceTrustServiceAbstraction>; let authRequestService: MockProxy<AuthRequestServiceAbstraction>; let i18nService: MockProxy<I18nService>; let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>; @@ -82,7 +82,7 @@ describe("SsoLoginStrategy", () => { twoFactorService = mock<TwoFactorService>(); userDecryptionOptionsService = mock<InternalUserDecryptionOptionsServiceAbstraction>(); keyConnectorService = mock<KeyConnectorService>(); - deviceTrustCryptoService = mock<DeviceTrustCryptoServiceAbstraction>(); + deviceTrustService = mock<DeviceTrustServiceAbstraction>(); authRequestService = mock<AuthRequestServiceAbstraction>(); i18nService = mock<I18nService>(); billingAccountProfileStateService = mock<BillingAccountProfileStateService>(); @@ -106,7 +106,7 @@ describe("SsoLoginStrategy", () => { twoFactorService, userDecryptionOptionsService, keyConnectorService, - deviceTrustCryptoService, + deviceTrustService, authRequestService, i18nService, billingAccountProfileStateService, @@ -209,8 +209,8 @@ describe("SsoLoginStrategy", () => { ); apiService.postIdentityToken.mockResolvedValue(idTokenResponse); - deviceTrustCryptoService.getDeviceKey.mockResolvedValue(mockDeviceKey); - deviceTrustCryptoService.decryptUserKeyWithDeviceKey.mockResolvedValue(mockUserKey); + deviceTrustService.getDeviceKey.mockResolvedValue(mockDeviceKey); + deviceTrustService.decryptUserKeyWithDeviceKey.mockResolvedValue(mockUserKey); const cryptoSvcSetUserKeySpy = jest.spyOn(cryptoService, "setUserKey"); @@ -218,8 +218,8 @@ describe("SsoLoginStrategy", () => { await ssoLoginStrategy.logIn(credentials); // Assert - expect(deviceTrustCryptoService.getDeviceKey).toHaveBeenCalledTimes(1); - expect(deviceTrustCryptoService.decryptUserKeyWithDeviceKey).toHaveBeenCalledTimes(1); + expect(deviceTrustService.getDeviceKey).toHaveBeenCalledTimes(1); + expect(deviceTrustService.decryptUserKeyWithDeviceKey).toHaveBeenCalledTimes(1); expect(cryptoSvcSetUserKeySpy).toHaveBeenCalledTimes(1); expect(cryptoSvcSetUserKeySpy).toHaveBeenCalledWith(mockUserKey); }); @@ -232,8 +232,8 @@ describe("SsoLoginStrategy", () => { ); apiService.postIdentityToken.mockResolvedValue(idTokenResponse); // Set deviceKey to be null - deviceTrustCryptoService.getDeviceKey.mockResolvedValue(null); - deviceTrustCryptoService.decryptUserKeyWithDeviceKey.mockResolvedValue(mockUserKey); + deviceTrustService.getDeviceKey.mockResolvedValue(null); + deviceTrustService.decryptUserKeyWithDeviceKey.mockResolvedValue(mockUserKey); // Act await ssoLoginStrategy.logIn(credentials); @@ -254,7 +254,7 @@ describe("SsoLoginStrategy", () => { // Arrange const idTokenResponse = mockIdTokenResponseWithModifiedTrustedDeviceOption(valueName, null); apiService.postIdentityToken.mockResolvedValue(idTokenResponse); - deviceTrustCryptoService.getDeviceKey.mockResolvedValue(mockDeviceKey); + deviceTrustService.getDeviceKey.mockResolvedValue(mockDeviceKey); // Act await ssoLoginStrategy.logIn(credentials); @@ -271,9 +271,9 @@ describe("SsoLoginStrategy", () => { userDecryptionOptsServerResponseWithTdeOption, ); apiService.postIdentityToken.mockResolvedValue(idTokenResponse); - deviceTrustCryptoService.getDeviceKey.mockResolvedValue(mockDeviceKey); + deviceTrustService.getDeviceKey.mockResolvedValue(mockDeviceKey); // Set userKey to be null - deviceTrustCryptoService.decryptUserKeyWithDeviceKey.mockResolvedValue(null); + deviceTrustService.decryptUserKeyWithDeviceKey.mockResolvedValue(null); // Act await ssoLoginStrategy.logIn(credentials); @@ -321,7 +321,7 @@ describe("SsoLoginStrategy", () => { await ssoLoginStrategy.logIn(credentials); expect(authRequestService.setKeysAfterDecryptingSharedMasterKeyAndHash).toHaveBeenCalled(); - expect(deviceTrustCryptoService.decryptUserKeyWithDeviceKey).not.toHaveBeenCalled(); + expect(deviceTrustService.decryptUserKeyWithDeviceKey).not.toHaveBeenCalled(); }); it("sets the user key from approved admin request if exists", async () => { @@ -338,7 +338,7 @@ describe("SsoLoginStrategy", () => { await ssoLoginStrategy.logIn(credentials); expect(authRequestService.setUserKeyAfterDecryptingSharedUserKey).toHaveBeenCalled(); - expect(deviceTrustCryptoService.decryptUserKeyWithDeviceKey).not.toHaveBeenCalled(); + expect(deviceTrustService.decryptUserKeyWithDeviceKey).not.toHaveBeenCalled(); }); it("attempts to establish a trusted device if successful", async () => { @@ -355,7 +355,7 @@ describe("SsoLoginStrategy", () => { await ssoLoginStrategy.logIn(credentials); expect(authRequestService.setUserKeyAfterDecryptingSharedUserKey).toHaveBeenCalled(); - expect(deviceTrustCryptoService.trustDeviceIfRequired).toHaveBeenCalled(); + expect(deviceTrustService.trustDeviceIfRequired).toHaveBeenCalled(); }); it("clears the admin auth request if server returns a 404, meaning it was deleted", async () => { @@ -369,7 +369,7 @@ describe("SsoLoginStrategy", () => { authRequestService.setKeysAfterDecryptingSharedMasterKeyAndHash, ).not.toHaveBeenCalled(); expect(authRequestService.setUserKeyAfterDecryptingSharedUserKey).not.toHaveBeenCalled(); - expect(deviceTrustCryptoService.trustDeviceIfRequired).not.toHaveBeenCalled(); + expect(deviceTrustService.trustDeviceIfRequired).not.toHaveBeenCalled(); }); it("attempts to login with a trusted device if admin auth request isn't successful", async () => { @@ -382,11 +382,11 @@ describe("SsoLoginStrategy", () => { }; apiService.getAuthRequest.mockResolvedValue(adminAuthResponse as AuthRequestResponse); cryptoService.hasUserKey.mockResolvedValue(false); - deviceTrustCryptoService.getDeviceKey.mockResolvedValue("DEVICE_KEY" as any); + deviceTrustService.getDeviceKey.mockResolvedValue("DEVICE_KEY" as any); await ssoLoginStrategy.logIn(credentials); - expect(deviceTrustCryptoService.decryptUserKeyWithDeviceKey).toHaveBeenCalled(); + expect(deviceTrustService.decryptUserKeyWithDeviceKey).toHaveBeenCalled(); }); }); }); 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 d8efd78984..dc63f0fae1 100644 --- a/libs/auth/src/common/login-strategies/sso-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/sso-login.strategy.ts @@ -3,7 +3,6 @@ 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"; @@ -22,6 +21,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 { DeviceTrustServiceAbstraction } from "@bitwarden/common/src/auth/abstractions/device-trust.service.abstraction"; import { UserId } from "@bitwarden/common/types/guid"; import { @@ -94,7 +94,7 @@ export class SsoLoginStrategy extends LoginStrategy { twoFactorService: TwoFactorService, userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, private keyConnectorService: KeyConnectorService, - private deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, + private deviceTrustService: DeviceTrustServiceAbstraction, private authRequestService: AuthRequestServiceAbstraction, private i18nService: I18nService, billingAccountProfileStateService: BillingAccountProfileStateService, @@ -298,7 +298,7 @@ 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(userId); + await this.deviceTrustService.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 @@ -314,7 +314,7 @@ export class SsoLoginStrategy extends LoginStrategy { const userId = (await this.stateService.getUserId()) as UserId; - const deviceKey = await this.deviceTrustCryptoService.getDeviceKey(userId); + const deviceKey = await this.deviceTrustService.getDeviceKey(userId); const encDevicePrivateKey = trustedDeviceOption?.encryptedPrivateKey; const encUserKey = trustedDeviceOption?.encryptedUserKey; @@ -322,7 +322,7 @@ export class SsoLoginStrategy extends LoginStrategy { return; } - const userKey = await this.deviceTrustCryptoService.decryptUserKeyWithDeviceKey( + const userKey = await this.deviceTrustService.decryptUserKeyWithDeviceKey( userId, encDevicePrivateKey, encUserKey, 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 fcc0220d0a..33708885e2 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 @@ -2,7 +2,7 @@ import { MockProxy, mock } from "jest-mock-extended"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; 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"; @@ -62,7 +62,7 @@ describe("LoginStrategyService", () => { let encryptService: MockProxy<EncryptService>; let passwordStrengthService: MockProxy<PasswordStrengthServiceAbstraction>; let policyService: MockProxy<PolicyService>; - let deviceTrustCryptoService: MockProxy<DeviceTrustCryptoServiceAbstraction>; + let deviceTrustService: MockProxy<DeviceTrustServiceAbstraction>; let authRequestService: MockProxy<AuthRequestServiceAbstraction>; let userDecryptionOptionsService: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>; let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>; @@ -90,7 +90,7 @@ describe("LoginStrategyService", () => { encryptService = mock<EncryptService>(); passwordStrengthService = mock<PasswordStrengthServiceAbstraction>(); policyService = mock<PolicyService>(); - deviceTrustCryptoService = mock<DeviceTrustCryptoServiceAbstraction>(); + deviceTrustService = mock<DeviceTrustServiceAbstraction>(); authRequestService = mock<AuthRequestServiceAbstraction>(); userDecryptionOptionsService = mock<UserDecryptionOptionsService>(); billingAccountProfileStateService = mock<BillingAccountProfileStateService>(); @@ -114,7 +114,7 @@ describe("LoginStrategyService", () => { encryptService, passwordStrengthService, policyService, - deviceTrustCryptoService, + deviceTrustService, authRequestService, userDecryptionOptionsService, stateProvider, 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 a8bd7bc2ff..aee74e6607 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 @@ -10,7 +10,6 @@ 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"; @@ -36,6 +35,7 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv import { KdfType } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { GlobalState, GlobalStateProvider } from "@bitwarden/common/platform/state"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/src/auth/abstractions/device-trust.service.abstraction"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { MasterKey } from "@bitwarden/common/types/key"; @@ -100,7 +100,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { protected encryptService: EncryptService, protected passwordStrengthService: PasswordStrengthServiceAbstraction, protected policyService: PolicyService, - protected deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, + protected deviceTrustService: DeviceTrustServiceAbstraction, protected authRequestService: AuthRequestServiceAbstraction, protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, protected stateProvider: GlobalStateProvider, @@ -371,7 +371,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.twoFactorService, this.userDecryptionOptionsService, this.keyConnectorService, - this.deviceTrustCryptoService, + this.deviceTrustService, this.authRequestService, this.i18nService, this.billingAccountProfileStateService, @@ -410,7 +410,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.stateService, this.twoFactorService, this.userDecryptionOptionsService, - this.deviceTrustCryptoService, + this.deviceTrustService, this.billingAccountProfileStateService, ); case AuthenticationType.WebAuthn: diff --git a/libs/common/src/auth/abstractions/device-trust-crypto.service.abstraction.ts b/libs/common/src/auth/abstractions/device-trust.service.abstraction.ts similarity index 89% rename from libs/common/src/auth/abstractions/device-trust-crypto.service.abstraction.ts rename to libs/common/src/auth/abstractions/device-trust.service.abstraction.ts index 53fe214035..123f710338 100644 --- a/libs/common/src/auth/abstractions/device-trust-crypto.service.abstraction.ts +++ b/libs/common/src/auth/abstractions/device-trust.service.abstraction.ts @@ -3,9 +3,10 @@ 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"; -export abstract class DeviceTrustCryptoServiceAbstraction { +import { DeviceResponse } from "./devices/responses/device.response"; + +export abstract class DeviceTrustServiceAbstraction { supportsDeviceTrust$: Observable<boolean>; /** * @description Retrieves the users choice to trust the device which can only happen after decryption diff --git a/libs/common/src/auth/services/device-trust-crypto.service.implementation.ts b/libs/common/src/auth/services/device-trust.service.implementation.ts similarity index 98% rename from libs/common/src/auth/services/device-trust-crypto.service.implementation.ts rename to libs/common/src/auth/services/device-trust.service.implementation.ts index 6fb58eab28..ccf87acaf8 100644 --- a/libs/common/src/auth/services/device-trust-crypto.service.implementation.ts +++ b/libs/common/src/auth/services/device-trust.service.implementation.ts @@ -17,7 +17,7 @@ import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypt 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 { DeviceTrustServiceAbstraction } from "../abstractions/device-trust.service.abstraction"; import { DeviceResponse } from "../abstractions/devices/responses/device.response"; import { DevicesApiServiceAbstraction } from "../abstractions/devices-api.service.abstraction"; import { SecretVerificationRequest } from "../models/request/secret-verification.request"; @@ -42,7 +42,7 @@ export const SHOULD_TRUST_DEVICE = new UserKeyDefinition<boolean>( }, ); -export class DeviceTrustCryptoService implements DeviceTrustCryptoServiceAbstraction { +export class DeviceTrustService implements DeviceTrustServiceAbstraction { private readonly platformSupportsSecureStorage = this.platformUtilsService.supportsSecureStorage(); private readonly deviceKeySecureStorageKey: string = "_deviceKey"; diff --git a/libs/common/src/auth/services/device-trust-crypto.service.spec.ts b/libs/common/src/auth/services/device-trust.service.spec.ts similarity index 86% rename from libs/common/src/auth/services/device-trust-crypto.service.spec.ts rename to libs/common/src/auth/services/device-trust.service.spec.ts index af147b3481..12b8cf2eaa 100644 --- a/libs/common/src/auth/services/device-trust-crypto.service.spec.ts +++ b/libs/common/src/auth/services/device-trust.service.spec.ts @@ -33,11 +33,11 @@ import { ProtectedDeviceResponse } from "../models/response/protected-device.res import { SHOULD_TRUST_DEVICE, DEVICE_KEY, - DeviceTrustCryptoService, -} from "./device-trust-crypto.service.implementation"; + DeviceTrustService, +} from "./device-trust.service.implementation"; -describe("deviceTrustCryptoService", () => { - let deviceTrustCryptoService: DeviceTrustCryptoService; +describe("deviceTrustService", () => { + let deviceTrustService: DeviceTrustService; const keyGenerationService = mock<KeyGenerationService>(); const cryptoFunctionService = mock<CryptoFunctionService>(); @@ -70,11 +70,11 @@ describe("deviceTrustCryptoService", () => { jest.clearAllMocks(); 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); + deviceTrustService = createDeviceTrustService(mockUserId, supportsSecureStorage); }); it("instantiates", () => { - expect(deviceTrustCryptoService).not.toBeFalsy(); + expect(deviceTrustService).not.toBeFalsy(); }); describe("User Trust Device Choice For Decryption", () => { @@ -84,7 +84,7 @@ describe("deviceTrustCryptoService", () => { await stateProvider.setUserState(SHOULD_TRUST_DEVICE, newValue, mockUserId); - const result = await deviceTrustCryptoService.getShouldTrustDevice(mockUserId); + const result = await deviceTrustService.getShouldTrustDevice(mockUserId); expect(result).toEqual(newValue); }); @@ -95,9 +95,9 @@ describe("deviceTrustCryptoService", () => { await stateProvider.setUserState(SHOULD_TRUST_DEVICE, false, mockUserId); const newValue = true; - await deviceTrustCryptoService.setShouldTrustDevice(mockUserId, newValue); + await deviceTrustService.setShouldTrustDevice(mockUserId, newValue); - const result = await deviceTrustCryptoService.getShouldTrustDevice(mockUserId); + const result = await deviceTrustService.getShouldTrustDevice(mockUserId); expect(result).toEqual(newValue); }); }); @@ -105,25 +105,25 @@ describe("deviceTrustCryptoService", () => { describe("trustDeviceIfRequired", () => { it("should trust device and reset when getShouldTrustDevice returns true", async () => { - jest.spyOn(deviceTrustCryptoService, "getShouldTrustDevice").mockResolvedValue(true); - jest.spyOn(deviceTrustCryptoService, "trustDevice").mockResolvedValue({} as DeviceResponse); - jest.spyOn(deviceTrustCryptoService, "setShouldTrustDevice").mockResolvedValue(); + jest.spyOn(deviceTrustService, "getShouldTrustDevice").mockResolvedValue(true); + jest.spyOn(deviceTrustService, "trustDevice").mockResolvedValue({} as DeviceResponse); + jest.spyOn(deviceTrustService, "setShouldTrustDevice").mockResolvedValue(); - await deviceTrustCryptoService.trustDeviceIfRequired(mockUserId); + await deviceTrustService.trustDeviceIfRequired(mockUserId); - expect(deviceTrustCryptoService.getShouldTrustDevice).toHaveBeenCalledTimes(1); - expect(deviceTrustCryptoService.trustDevice).toHaveBeenCalledTimes(1); - expect(deviceTrustCryptoService.setShouldTrustDevice).toHaveBeenCalledWith(mockUserId, false); + expect(deviceTrustService.getShouldTrustDevice).toHaveBeenCalledTimes(1); + expect(deviceTrustService.trustDevice).toHaveBeenCalledTimes(1); + expect(deviceTrustService.setShouldTrustDevice).toHaveBeenCalledWith(mockUserId, false); }); it("should not trust device nor reset when getShouldTrustDevice returns false", async () => { const getShouldTrustDeviceSpy = jest - .spyOn(deviceTrustCryptoService, "getShouldTrustDevice") + .spyOn(deviceTrustService, "getShouldTrustDevice") .mockResolvedValue(false); - const trustDeviceSpy = jest.spyOn(deviceTrustCryptoService, "trustDevice"); - const setShouldTrustDeviceSpy = jest.spyOn(deviceTrustCryptoService, "setShouldTrustDevice"); + const trustDeviceSpy = jest.spyOn(deviceTrustService, "trustDevice"); + const setShouldTrustDeviceSpy = jest.spyOn(deviceTrustService, "setShouldTrustDevice"); - await deviceTrustCryptoService.trustDeviceIfRequired(mockUserId); + await deviceTrustService.trustDeviceIfRequired(mockUserId); expect(getShouldTrustDeviceSpy).toHaveBeenCalledTimes(1); expect(trustDeviceSpy).not.toHaveBeenCalled(); @@ -151,7 +151,7 @@ describe("deviceTrustCryptoService", () => { it("returns null when there is not an existing device key", async () => { await stateProvider.setUserState(DEVICE_KEY, null, mockUserId); - const deviceKey = await deviceTrustCryptoService.getDeviceKey(mockUserId); + const deviceKey = await deviceTrustService.getDeviceKey(mockUserId); expect(deviceKey).toBeNull(); expect(secureStorageService.get).not.toHaveBeenCalled(); @@ -160,7 +160,7 @@ describe("deviceTrustCryptoService", () => { 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); + const deviceKey = await deviceTrustService.getDeviceKey(mockUserId); expect(deviceKey).not.toBeNull(); expect(deviceKey).toBeInstanceOf(SymmetricCryptoKey); @@ -172,17 +172,14 @@ describe("deviceTrustCryptoService", () => { describe("Secure Storage supported", () => { beforeEach(() => { const supportsSecureStorage = true; - deviceTrustCryptoService = createDeviceTrustCryptoService( - mockUserId, - supportsSecureStorage, - ); + deviceTrustService = createDeviceTrustService(mockUserId, supportsSecureStorage); }); it("returns null when there is not an existing device key for the passed in user id", async () => { secureStorageService.get.mockResolvedValue(null); // Act - const deviceKey = await deviceTrustCryptoService.getDeviceKey(mockUserId); + const deviceKey = await deviceTrustService.getDeviceKey(mockUserId); // Assert expect(deviceKey).toBeNull(); @@ -193,7 +190,7 @@ describe("deviceTrustCryptoService", () => { secureStorageService.get.mockResolvedValue(existingDeviceKeyB64); // Act - const deviceKey = await deviceTrustCryptoService.getDeviceKey(mockUserId); + const deviceKey = await deviceTrustService.getDeviceKey(mockUserId); // Assert expect(deviceKey).not.toBeNull(); @@ -203,7 +200,7 @@ describe("deviceTrustCryptoService", () => { }); it("throws an error when no user id is passed in", async () => { - await expect(deviceTrustCryptoService.getDeviceKey(null)).rejects.toThrow( + await expect(deviceTrustService.getDeviceKey(null)).rejects.toThrow( "UserId is required. Cannot get device key.", ); }); @@ -220,7 +217,7 @@ describe("deviceTrustCryptoService", () => { // 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); + await (deviceTrustService as any).setDeviceKey(mockUserId, newDeviceKey); expect(stateProvider.mock.setUserState).toHaveBeenLastCalledWith( DEVICE_KEY, @@ -232,10 +229,7 @@ describe("deviceTrustCryptoService", () => { describe("Secure Storage supported", () => { beforeEach(() => { const supportsSecureStorage = true; - deviceTrustCryptoService = createDeviceTrustCryptoService( - mockUserId, - supportsSecureStorage, - ); + deviceTrustService = createDeviceTrustService(mockUserId, supportsSecureStorage); }); it("successfully sets the device key in secure storage", async () => { @@ -251,7 +245,7 @@ describe("deviceTrustCryptoService", () => { // 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); + await (deviceTrustService as any).setDeviceKey(mockUserId, newDeviceKey); // Assert expect(stateProvider.mock.setUserState).not.toHaveBeenCalledTimes(2); @@ -268,9 +262,9 @@ describe("deviceTrustCryptoService", () => { new Uint8Array(deviceKeyBytesLength) as CsprngArray, ) as DeviceKey; - await expect( - (deviceTrustCryptoService as any).setDeviceKey(null, newDeviceKey), - ).rejects.toThrow("UserId is required. Cannot set device key."); + await expect((deviceTrustService as any).setDeviceKey(null, newDeviceKey)).rejects.toThrow( + "UserId is required. Cannot set device key.", + ); }); }); @@ -285,7 +279,7 @@ describe("deviceTrustCryptoService", () => { // TypeScript will allow calling private methods if the object is of type 'any' // This is a hacky workaround, but it allows for cleaner tests - const deviceKey = await (deviceTrustCryptoService as any).makeDeviceKey(); + const deviceKey = await (deviceTrustService as any).makeDeviceKey(); expect(keyGenSvcGenerateKeySpy).toHaveBeenCalledTimes(1); expect(keyGenSvcGenerateKeySpy).toHaveBeenCalledWith(deviceKeyBytesLength * 8); @@ -362,7 +356,7 @@ describe("deviceTrustCryptoService", () => { // TypeScript will allow calling private methods if the object is of type 'any' makeDeviceKeySpy = jest - .spyOn(deviceTrustCryptoService as any, "makeDeviceKey") + .spyOn(deviceTrustService as any, "makeDeviceKey") .mockResolvedValue(mockDeviceKey); rsaGenerateKeyPairSpy = jest @@ -398,7 +392,7 @@ describe("deviceTrustCryptoService", () => { }); it("calls the required methods with the correct arguments and returns a DeviceResponse", async () => { - const response = await deviceTrustCryptoService.trustDevice(mockUserId); + const response = await deviceTrustService.trustDevice(mockUserId); expect(makeDeviceKeySpy).toHaveBeenCalledTimes(1); expect(rsaGenerateKeyPairSpy).toHaveBeenCalledTimes(1); @@ -429,7 +423,7 @@ describe("deviceTrustCryptoService", () => { // setup the spy to return null cryptoSvcGetUserKeySpy.mockResolvedValue(null); // check if the expected error is thrown - await expect(deviceTrustCryptoService.trustDevice(mockUserId)).rejects.toThrow( + await expect(deviceTrustService.trustDevice(mockUserId)).rejects.toThrow( "User symmetric key not found", ); @@ -439,7 +433,7 @@ describe("deviceTrustCryptoService", () => { // setup the spy to return undefined cryptoSvcGetUserKeySpy.mockResolvedValue(undefined); // check if the expected error is thrown - await expect(deviceTrustCryptoService.trustDevice(mockUserId)).rejects.toThrow( + await expect(deviceTrustService.trustDevice(mockUserId)).rejects.toThrow( "User symmetric key not found", ); }); @@ -479,9 +473,7 @@ describe("deviceTrustCryptoService", () => { it(`throws an error if ${method} fails`, async () => { const methodSpy = spy(); methodSpy.mockRejectedValue(new Error(errorText)); - await expect(deviceTrustCryptoService.trustDevice(mockUserId)).rejects.toThrow( - errorText, - ); + await expect(deviceTrustService.trustDevice(mockUserId)).rejects.toThrow(errorText); }); test.each([null, undefined])( @@ -489,14 +481,14 @@ describe("deviceTrustCryptoService", () => { async (invalidValue) => { const methodSpy = spy(); methodSpy.mockResolvedValue(invalidValue); - await expect(deviceTrustCryptoService.trustDevice(mockUserId)).rejects.toThrow(); + await expect(deviceTrustService.trustDevice(mockUserId)).rejects.toThrow(); }, ); }, ); it("throws an error when a null user id is passed in", async () => { - await expect(deviceTrustCryptoService.trustDevice(null)).rejects.toThrow( + await expect(deviceTrustService.trustDevice(null)).rejects.toThrow( "UserId is required. Cannot trust device.", ); }); @@ -530,7 +522,7 @@ describe("deviceTrustCryptoService", () => { it("throws an error when a null user id is passed in", async () => { await expect( - deviceTrustCryptoService.decryptUserKeyWithDeviceKey( + deviceTrustService.decryptUserKeyWithDeviceKey( null, mockEncryptedDevicePrivateKey, mockEncryptedUserKey, @@ -540,7 +532,7 @@ describe("deviceTrustCryptoService", () => { }); it("returns null when device key isn't provided", async () => { - const result = await deviceTrustCryptoService.decryptUserKeyWithDeviceKey( + const result = await deviceTrustService.decryptUserKeyWithDeviceKey( mockUserId, mockEncryptedDevicePrivateKey, mockEncryptedUserKey, @@ -558,7 +550,7 @@ describe("deviceTrustCryptoService", () => { .spyOn(cryptoService, "rsaDecrypt") .mockResolvedValue(new Uint8Array(userKeyBytesLength)); - const result = await deviceTrustCryptoService.decryptUserKeyWithDeviceKey( + const result = await deviceTrustService.decryptUserKeyWithDeviceKey( mockUserId, mockEncryptedDevicePrivateKey, mockEncryptedUserKey, @@ -574,9 +566,9 @@ describe("deviceTrustCryptoService", () => { const decryptToBytesSpy = jest .spyOn(encryptService, "decryptToBytes") .mockRejectedValue(new Error("Decryption error")); - const setDeviceKeySpy = jest.spyOn(deviceTrustCryptoService as any, "setDeviceKey"); + const setDeviceKeySpy = jest.spyOn(deviceTrustService as any, "setDeviceKey"); - const result = await deviceTrustCryptoService.decryptUserKeyWithDeviceKey( + const result = await deviceTrustService.decryptUserKeyWithDeviceKey( mockUserId, mockEncryptedDevicePrivateKey, mockEncryptedUserKey, @@ -606,7 +598,7 @@ describe("deviceTrustCryptoService", () => { it("throws an error when a null user id is passed in", async () => { await expect( - deviceTrustCryptoService.rotateDevicesTrust(null, fakeNewUserKey, ""), + deviceTrustService.rotateDevicesTrust(null, fakeNewUserKey, ""), ).rejects.toThrow("UserId is required. Cannot rotate device's trust."); }); @@ -615,7 +607,7 @@ describe("deviceTrustCryptoService", () => { stateProvider.activeUser.getFake(DEVICE_KEY); deviceKeyState.nextState(null); - await deviceTrustCryptoService.rotateDevicesTrust(mockUserId, fakeNewUserKey, ""); + await deviceTrustService.rotateDevicesTrust(mockUserId, fakeNewUserKey, ""); expect(devicesApiService.updateTrust).not.toHaveBeenCalled(); }); @@ -691,7 +683,7 @@ describe("deviceTrustCryptoService", () => { ); }); - await deviceTrustCryptoService.rotateDevicesTrust( + await deviceTrustService.rotateDevicesTrust( mockUserId, fakeNewUserKey, "my_password_hash", @@ -713,10 +705,7 @@ describe("deviceTrustCryptoService", () => { }); // Helpers - function createDeviceTrustCryptoService( - mockUserId: UserId | null, - supportsSecureStorage: boolean, - ) { + function createDeviceTrustService(mockUserId: UserId | null, supportsSecureStorage: boolean) { accountService = mockAccountServiceWith(mockUserId); stateProvider = new FakeStateProvider(accountService); @@ -725,7 +714,7 @@ describe("deviceTrustCryptoService", () => { decryptionOptions.next({} as any); userDecryptionOptionsService.userDecryptionOptions$ = decryptionOptions; - return new DeviceTrustCryptoService( + return new DeviceTrustService( keyGenerationService, cryptoFunctionService, cryptoService, diff --git a/libs/common/src/state-migrations/migrate.ts b/libs/common/src/state-migrations/migrate.ts index f9a8734731..2d8ef1619e 100644 --- a/libs/common/src/state-migrations/migrate.ts +++ b/libs/common/src/state-migrations/migrate.ts @@ -49,7 +49,7 @@ import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org- 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 { DeviceTrustServiceStateProviderMigrator } from "./migrations/53-migrate-device-trust-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"; @@ -117,7 +117,7 @@ export function createMigrationBuilder() { .with(KeyConnectorMigrator, 49, 50) .with(RememberedEmailMigrator, 50, 51) .with(DeleteInstalledVersion, 51, 52) - .with(DeviceTrustCryptoServiceStateProviderMigrator, 52, 53) + .with(DeviceTrustServiceStateProviderMigrator, 52, 53) .with(SendMigrator, 53, 54) .with(MoveMasterKeyStateToProviderMigrator, 54, 55) .with(AuthRequestMigrator, 55, 56) 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-svc-to-state-providers.spec.ts similarity index 92% rename from libs/common/src/state-migrations/migrations/53-migrate-device-trust-crypto-svc-to-state-providers.spec.ts rename to libs/common/src/state-migrations/migrations/53-migrate-device-trust-svc-to-state-providers.spec.ts index 79366a4716..343fbd03d9 100644 --- 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-svc-to-state-providers.spec.ts @@ -5,9 +5,9 @@ import { mockMigrationHelper } from "../migration-helper.spec"; import { DEVICE_KEY, - DeviceTrustCryptoServiceStateProviderMigrator, + DeviceTrustServiceStateProviderMigrator, SHOULD_TRUST_DEVICE, -} from "./53-migrate-device-trust-crypto-svc-to-state-providers"; +} from "./53-migrate-device-trust-svc-to-state-providers"; // Represents data in state service pre-migration function preMigrationJson() { @@ -79,14 +79,14 @@ function rollbackJSON() { }; } -describe("DeviceTrustCryptoServiceStateProviderMigrator", () => { +describe("DeviceTrustServiceStateProviderMigrator", () => { let helper: MockProxy<MigrationHelper>; - let sut: DeviceTrustCryptoServiceStateProviderMigrator; + let sut: DeviceTrustServiceStateProviderMigrator; describe("migrate", () => { beforeEach(() => { helper = mockMigrationHelper(preMigrationJson(), 52); - sut = new DeviceTrustCryptoServiceStateProviderMigrator(52, 53); + sut = new DeviceTrustServiceStateProviderMigrator(52, 53); }); // it should remove deviceKey and trustDeviceChoiceForDecryption from all accounts @@ -126,7 +126,7 @@ describe("DeviceTrustCryptoServiceStateProviderMigrator", () => { describe("rollback", () => { beforeEach(() => { helper = mockMigrationHelper(rollbackJSON(), 53); - sut = new DeviceTrustCryptoServiceStateProviderMigrator(52, 53); + sut = new DeviceTrustServiceStateProviderMigrator(52, 53); }); it("should null out newly migrated entries in state provider framework", async () => { 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-svc-to-state-providers.ts similarity index 94% rename from libs/common/src/state-migrations/migrations/53-migrate-device-trust-crypto-svc-to-state-providers.ts rename to libs/common/src/state-migrations/migrations/53-migrate-device-trust-svc-to-state-providers.ts index e19c7b3fa5..b6d2c19b15 100644 --- 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-svc-to-state-providers.ts @@ -16,7 +16,7 @@ type ExpectedAccountType = { }; export const DEVICE_KEY: KeyDefinitionLike = { - key: "deviceKey", // matches KeyDefinition.key in DeviceTrustCryptoService + key: "deviceKey", // matches KeyDefinition.key in DeviceTrustService stateDefinition: { name: "deviceTrust", // matches StateDefinition.name in StateDefinitions }, @@ -29,7 +29,7 @@ export const SHOULD_TRUST_DEVICE: KeyDefinitionLike = { }, }; -export class DeviceTrustCryptoServiceStateProviderMigrator extends Migrator<52, 53> { +export class DeviceTrustServiceStateProviderMigrator extends Migrator<52, 53> { async migrate(helper: MigrationHelper): Promise<void> { const accounts = await helper.getAccounts<ExpectedAccountType>(); async function migrateAccount(userId: string, account: ExpectedAccountType): Promise<void> { 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 index 9c6d3776fe..1fb3609267 100644 --- 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 @@ -4,7 +4,7 @@ import { IRREVERSIBLE, Migrator } from "../migrator"; type ExpectedAccountType = NonNullable<unknown>; export const REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE: KeyDefinitionLike = { - key: "refreshTokenMigratedToSecureStorage", // matches KeyDefinition.key in DeviceTrustCryptoService + key: "refreshTokenMigratedToSecureStorage", // matches KeyDefinition.key stateDefinition: { name: "token", // matches StateDefinition.name in StateDefinitions }, From e89c82defeb9d5bfdbb1ae6c532e0041a2fb6986 Mon Sep 17 00:00:00 2001 From: Will Martin <contact@willmartian.com> Date: Wed, 24 Apr 2024 14:52:29 -0400 Subject: [PATCH 275/351] [CL-236] Card component (#8900) * add card component; adjust section margin on small screens --- libs/components/src/card/card.component.ts | 15 +++++ libs/components/src/card/card.stories.ts | 62 +++++++++++++++++++ libs/components/src/card/index.ts | 1 + libs/components/src/index.ts | 1 + .../src/section/section.component.ts | 2 +- .../components/src/section/section.stories.ts | 2 +- 6 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 libs/components/src/card/card.component.ts create mode 100644 libs/components/src/card/card.stories.ts create mode 100644 libs/components/src/card/index.ts diff --git a/libs/components/src/card/card.component.ts b/libs/components/src/card/card.component.ts new file mode 100644 index 0000000000..da61d53664 --- /dev/null +++ b/libs/components/src/card/card.component.ts @@ -0,0 +1,15 @@ +import { CommonModule } from "@angular/common"; +import { ChangeDetectionStrategy, Component } from "@angular/core"; + +@Component({ + selector: "bit-card", + standalone: true, + imports: [CommonModule], + template: `<ng-content></ng-content>`, + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + class: + "tw-box-border tw-block tw-bg-background tw-text-main tw-border-solid tw-border-b tw-border-0 tw-border-b-secondary-300 tw-rounded-lg tw-py-4 tw-px-3", + }, +}) +export class CardComponent {} diff --git a/libs/components/src/card/card.stories.ts b/libs/components/src/card/card.stories.ts new file mode 100644 index 0000000000..702a8aeb63 --- /dev/null +++ b/libs/components/src/card/card.stories.ts @@ -0,0 +1,62 @@ +import { Meta, StoryObj, componentWrapperDecorator, moduleMetadata } from "@storybook/angular"; + +import { SectionComponent } from "../section"; +import { TypographyModule } from "../typography"; + +import { CardComponent } from "./card.component"; + +export default { + title: "Component Library/Card", + component: CardComponent, + decorators: [ + moduleMetadata({ + imports: [TypographyModule, SectionComponent], + }), + componentWrapperDecorator( + (story) => `<div class="tw-bg-background-alt tw-p-10 tw-text-main">${story}</div>`, + ), + ], +} as Meta; + +type Story = StoryObj<CardComponent>; + +/** Cards are presentational containers. */ +export const Default: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` + <bit-card> + <p bitTypography="body1" class="!tw-mb-0">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras vitae congue risus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nunc elementum odio nibh, eget pellentesque sem ornare vitae. Etiam vel ante et velit fringilla egestas a sed sem. Fusce molestie nisl et nisi accumsan dapibus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Sed eu risus ex. </p> + </bit-card> + `, + }), +}; + +/** Cards are often paired with [Sections](/docs/component-library-section--docs). */ +export const WithinSections: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` + <bit-section> + <h2 bitTypography="h5">Bar</h2> + <bit-card> + <p bitTypography="body1" class="!tw-mb-0">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras vitae congue risus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nunc elementum odio nibh, eget pellentesque sem ornare vitae. Etiam vel ante et velit fringilla egestas a sed sem. Fusce molestie nisl et nisi accumsan dapibus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Sed eu risus ex. </p> + </bit-card> + </bit-section> + + <bit-section> + <h2 bitTypography="h5">Bar</h2> + <bit-card> + <p bitTypography="body1" class="!tw-mb-0">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras vitae congue risus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nunc elementum odio nibh, eget pellentesque sem ornare vitae. Etiam vel ante et velit fringilla egestas a sed sem. Fusce molestie nisl et nisi accumsan dapibus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Sed eu risus ex. </p> + </bit-card> + </bit-section> + + <bit-section> + <h2 bitTypography="h5">Bar</h2> + <bit-card> + <p bitTypography="body1" class="!tw-mb-0">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras vitae congue risus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nunc elementum odio nibh, eget pellentesque sem ornare vitae. Etiam vel ante et velit fringilla egestas a sed sem. Fusce molestie nisl et nisi accumsan dapibus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Sed eu risus ex. </p> + </bit-card> + </bit-section> + `, + }), +}; diff --git a/libs/components/src/card/index.ts b/libs/components/src/card/index.ts new file mode 100644 index 0000000000..8151bac4c8 --- /dev/null +++ b/libs/components/src/card/index.ts @@ -0,0 +1 @@ +export * from "./card.component"; diff --git a/libs/components/src/index.ts b/libs/components/src/index.ts index 527d5f3615..36185911a6 100644 --- a/libs/components/src/index.ts +++ b/libs/components/src/index.ts @@ -7,6 +7,7 @@ export * from "./breadcrumbs"; export * from "./button"; export { ButtonType } from "./shared/button-like.abstraction"; export * from "./callout"; +export * from "./card"; export * from "./checkbox"; export * from "./color-password"; export * from "./container"; diff --git a/libs/components/src/section/section.component.ts b/libs/components/src/section/section.component.ts index a681dcf7d9..a60e232eec 100644 --- a/libs/components/src/section/section.component.ts +++ b/libs/components/src/section/section.component.ts @@ -6,7 +6,7 @@ import { Component } from "@angular/core"; standalone: true, imports: [CommonModule], template: ` - <section class="tw-mb-12"> + <section class="tw-mb-6 md:tw-mb-12"> <ng-content></ng-content> </section> `, diff --git a/libs/components/src/section/section.stories.ts b/libs/components/src/section/section.stories.ts index fb9948e9be..65b6a67d47 100644 --- a/libs/components/src/section/section.stories.ts +++ b/libs/components/src/section/section.stories.ts @@ -17,7 +17,7 @@ export default { type Story = StoryObj<SectionComponent>; -/** Sections are simple containers that apply a bottom margin. They often contain a heading. */ +/** Sections are simple containers that apply a responsive bottom margin. They often contain a heading. */ export const Default: Story = { render: (args) => ({ props: args, From a8ba48898b13222b8742d9b0d63ca238368b99ff Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Wed, 24 Apr 2024 16:29:00 -0400 Subject: [PATCH 276/351] Use new endpoint to determine SM standalone (#8904) --- .../organizations/members/people.component.ts | 13 +++++++------ .../src/services/jslib-services.module.ts | 1 - .../billilng-api.service.abstraction.ts | 5 ++++- .../abstractions/organization-billing.service.ts | 2 -- .../organization-billing-metadata.response.ts | 10 ++++++++++ .../src/billing/services/billing-api.service.ts | 16 ++++++++++++++++ .../services/organization-billing.service.ts | 15 --------------- 7 files changed, 37 insertions(+), 25 deletions(-) create mode 100644 libs/common/src/billing/models/response/organization-billing-metadata.response.ts 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 0df247d7b0..af04d83c34 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 @@ -37,7 +37,7 @@ import { import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request"; -import { OrganizationBillingServiceAbstraction as OrganizationBillingService } from "@bitwarden/common/billing/abstractions/organization-billing.service"; +import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billilng-api.service.abstraction"; import { ProductType } from "@bitwarden/common/enums"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -121,7 +121,7 @@ export class PeopleComponent extends BasePeopleComponent<OrganizationUserView> { private groupService: GroupService, private collectionService: CollectionService, organizationManagementPreferencesService: OrganizationManagementPreferencesService, - private organizationBillingService: OrganizationBillingService, + private billingApiService: BillingApiServiceAbstraction, ) { super( apiService, @@ -190,10 +190,11 @@ export class PeopleComponent extends BasePeopleComponent<OrganizationUserView> { .find((p) => p.organizationId === this.organization.id); this.orgResetPasswordPolicyEnabled = resetPasswordPolicy?.enabled; - this.orgIsOnSecretsManagerStandalone = - await this.organizationBillingService.isOnSecretsManagerStandalone( - this.organization.id, - ); + const billingMetadata = await this.billingApiService.getOrganizationBillingMetadata( + this.organization.id, + ); + + this.orgIsOnSecretsManagerStandalone = billingMetadata.isOnSecretsManagerStandalone; await this.load(); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index aabf823c0b..42879a8424 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -1058,7 +1058,6 @@ const safeProviders: SafeProvider[] = [ useClass: OrganizationBillingService, deps: [ ApiServiceAbstraction, - BillingApiServiceAbstraction, CryptoServiceAbstraction, EncryptService, I18nServiceAbstraction, 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 15f0d4b551..063b3c370b 100644 --- a/libs/common/src/billing/abstractions/billilng-api.service.abstraction.ts +++ b/libs/common/src/billing/abstractions/billilng-api.service.abstraction.ts @@ -1,4 +1,5 @@ import { SubscriptionCancellationRequest } from "../../billing/models/request/subscription-cancellation.request"; +import { OrganizationBillingMetadataResponse } from "../../billing/models/response/organization-billing-metadata.response"; import { OrganizationBillingStatusResponse } from "../../billing/models/response/organization-billing-status.response"; import { OrganizationSubscriptionResponse } from "../../billing/models/response/organization-subscription.response"; import { PlanResponse } from "../../billing/models/response/plan.response"; @@ -12,13 +13,15 @@ export abstract class BillingApiServiceAbstraction { organizationId: string, request: SubscriptionCancellationRequest, ) => Promise<void>; - cancelPremiumUserSubscription: (request: SubscriptionCancellationRequest) => Promise<void>; createClientOrganization: ( providerId: string, request: CreateClientOrganizationRequest, ) => Promise<void>; getBillingStatus: (id: string) => Promise<OrganizationBillingStatusResponse>; + getOrganizationBillingMetadata: ( + organizationId: string, + ) => Promise<OrganizationBillingMetadataResponse>; getOrganizationSubscription: ( organizationId: string, ) => Promise<OrganizationSubscriptionResponse>; diff --git a/libs/common/src/billing/abstractions/organization-billing.service.ts b/libs/common/src/billing/abstractions/organization-billing.service.ts index 0917025eec..d19724b600 100644 --- a/libs/common/src/billing/abstractions/organization-billing.service.ts +++ b/libs/common/src/billing/abstractions/organization-billing.service.ts @@ -41,8 +41,6 @@ export type SubscriptionInformation = { }; export abstract class OrganizationBillingServiceAbstraction { - isOnSecretsManagerStandalone: (organizationId: string) => Promise<boolean>; - purchaseSubscription: (subscription: SubscriptionInformation) => Promise<OrganizationResponse>; startFree: (subscription: SubscriptionInformation) => Promise<OrganizationResponse>; diff --git a/libs/common/src/billing/models/response/organization-billing-metadata.response.ts b/libs/common/src/billing/models/response/organization-billing-metadata.response.ts new file mode 100644 index 0000000000..33d7907fa8 --- /dev/null +++ b/libs/common/src/billing/models/response/organization-billing-metadata.response.ts @@ -0,0 +1,10 @@ +import { BaseResponse } from "../../../models/response/base.response"; + +export class OrganizationBillingMetadataResponse extends BaseResponse { + isOnSecretsManagerStandalone: boolean; + + constructor(response: any) { + super(response); + this.isOnSecretsManagerStandalone = this.getResponseProperty("IsOnSecretsManagerStandalone"); + } +} diff --git a/libs/common/src/billing/services/billing-api.service.ts b/libs/common/src/billing/services/billing-api.service.ts index 1c119b971d..d21c1c9046 100644 --- a/libs/common/src/billing/services/billing-api.service.ts +++ b/libs/common/src/billing/services/billing-api.service.ts @@ -1,3 +1,5 @@ +import { OrganizationBillingMetadataResponse } from "@bitwarden/common/billing/models/response/organization-billing-metadata.response"; + import { ApiService } from "../../abstractions/api.service"; import { BillingApiServiceAbstraction } from "../../billing/abstractions/billilng-api.service.abstraction"; import { SubscriptionCancellationRequest } from "../../billing/models/request/subscription-cancellation.request"; @@ -53,6 +55,20 @@ export class BillingApiService implements BillingApiServiceAbstraction { return new OrganizationBillingStatusResponse(r); } + async getOrganizationBillingMetadata( + organizationId: string, + ): Promise<OrganizationBillingMetadataResponse> { + const r = await this.apiService.send( + "GET", + "/organizations/" + organizationId + "/billing/metadata", + null, + true, + true, + ); + + return new OrganizationBillingMetadataResponse(r); + } + async getOrganizationSubscription( organizationId: string, ): Promise<OrganizationSubscriptionResponse> { diff --git a/libs/common/src/billing/services/organization-billing.service.ts b/libs/common/src/billing/services/organization-billing.service.ts index fb2084bb6a..6b326472c9 100644 --- a/libs/common/src/billing/services/organization-billing.service.ts +++ b/libs/common/src/billing/services/organization-billing.service.ts @@ -9,7 +9,6 @@ import { I18nService } from "../../platform/abstractions/i18n.service"; import { EncString } from "../../platform/models/domain/enc-string"; import { OrgKey } from "../../types/key"; import { SyncService } from "../../vault/abstractions/sync/sync.service.abstraction"; -import { BillingApiServiceAbstraction as BillingApiService } from "../abstractions/billilng-api.service.abstraction"; import { OrganizationBillingServiceAbstraction, OrganizationInformation, @@ -29,7 +28,6 @@ interface OrganizationKeys { export class OrganizationBillingService implements OrganizationBillingServiceAbstraction { constructor( private apiService: ApiService, - private billingApiService: BillingApiService, private cryptoService: CryptoService, private encryptService: EncryptService, private i18nService: I18nService, @@ -37,19 +35,6 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs private syncService: SyncService, ) {} - async isOnSecretsManagerStandalone(organizationId: string): Promise<boolean> { - const response = await this.billingApiService.getOrganizationSubscription(organizationId); - if (response.customerDiscount?.id === "sm-standalone") { - const productIds = response.subscription.items.map((item) => item.productId); - return ( - response.customerDiscount?.appliesTo.filter((appliesToProductId) => - productIds.includes(appliesToProductId), - ).length > 0 - ); - } - return false; - } - async purchaseSubscription(subscription: SubscriptionInformation): Promise<OrganizationResponse> { const request = new OrganizationCreateRequest(); From 3f4adff2c502a7a47d6cc1e2b867c176c57f12df Mon Sep 17 00:00:00 2001 From: Jake Fink <jfink@bitwarden.com> Date: Wed, 24 Apr 2024 16:32:18 -0400 Subject: [PATCH 277/351] set auto key on command in cli (#8905) --- apps/cli/src/bw.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index 8163aa2945..be3ad9ea0e 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -77,6 +77,7 @@ import { MigrationBuilderService } from "@bitwarden/common/platform/services/mig import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; import { StateService } from "@bitwarden/common/platform/services/state.service"; import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider"; +import { UserKeyInitService } from "@bitwarden/common/platform/services/user-key-init.service"; import { ActiveUserStateProvider, DerivedStateProvider, @@ -233,6 +234,7 @@ export class Main { biometricStateService: BiometricStateService; billingAccountProfileStateService: BillingAccountProfileStateService; providerApiService: ProviderApiServiceAbstraction; + userKeyInitService: UserKeyInitService; constructor() { let p = null; @@ -692,6 +694,12 @@ export class Main { ); this.providerApiService = new ProviderApiService(this.apiService); + + this.userKeyInitService = new UserKeyInitService( + this.accountService, + this.cryptoService, + this.logService, + ); } async run() { @@ -735,6 +743,7 @@ export class Main { this.containerService.attachToGlobal(global); await this.i18nService.init(); this.twoFactorService.init(); + this.userKeyInitService.listenForActiveUserChangesToSetUserKey(); } } From a6755f5f202527e558ccc3cb5cb5c7b0921f3e39 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Wed, 24 Apr 2024 16:54:16 -0400 Subject: [PATCH 278/351] [PM-7687] Fix `reloadPopup` Recursion (#8902) * Fix Message Sending Recursion * Remove Change That Didn't Help * Prefer `isExternalMessage` Guard * Rollback Compare Change --------- Co-authored-by: Cesar Gonzalez <cesar.a.gonzalezcs@gmail.com> --- apps/browser/src/background/runtime.background.ts | 6 ++++-- .../local-backed-session-storage.service.ts | 13 ++++++++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts index f457889e96..294346fe9f 100644 --- a/apps/browser/src/background/runtime.background.ts +++ b/apps/browser/src/background/runtime.background.ts @@ -10,7 +10,7 @@ import { SystemService } from "@bitwarden/common/platform/abstractions/system.se import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CipherType } from "@bitwarden/common/vault/enums"; -import { MessageListener } from "../../../../libs/common/src/platform/messaging"; +import { MessageListener, isExternalMessage } from "../../../../libs/common/src/platform/messaging"; import { closeUnlockPopout, openSsoAuthResultPopout, @@ -266,7 +266,9 @@ export default class RuntimeBackground { break; } case "reloadPopup": - this.messagingService.send("reloadPopup"); + if (isExternalMessage(msg)) { + this.messagingService.send("reloadPopup"); + } break; case "emailVerificationRequired": this.messagingService.send("showDialog", { diff --git a/apps/browser/src/platform/services/local-backed-session-storage.service.ts b/apps/browser/src/platform/services/local-backed-session-storage.service.ts index 5432e8d918..0fa359181d 100644 --- a/apps/browser/src/platform/services/local-backed-session-storage.service.ts +++ b/apps/browser/src/platform/services/local-backed-session-storage.service.ts @@ -92,9 +92,16 @@ export class LocalBackedSessionStorageService // This is for observation purposes only. At some point, we don't want to write to local session storage if the value is the same. if (this.platformUtilsService.isDev()) { const existingValue = this.cache[key] as T; - if (this.compareValues<T>(existingValue, obj)) { - this.logService.warning(`Possible unnecessary write to local session storage. Key: ${key}`); - this.logService.warning(obj as any); + try { + if (this.compareValues<T>(existingValue, obj)) { + this.logService.warning( + `Possible unnecessary write to local session storage. Key: ${key}`, + ); + this.logService.warning(obj as any); + } + } catch (err) { + this.logService.warning(`Error while comparing values for key: ${key}`); + this.logService.warning(err); } } From dba910d0b946176344f13f55e081b3a7965ee9f4 Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Wed, 24 Apr 2024 23:41:35 +0200 Subject: [PATCH 279/351] Create and use `safeGetString()` instead of `instanceof` checks to determine type (#8906) `safeGetString` takes a `string` or `EncString` and return the appropiate value based on it's type Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com> --- libs/common/src/models/export/card.export.ts | 23 +++----- .../common/src/models/export/cipher.export.ts | 22 ++----- .../src/models/export/collection.export.ts | 8 +-- .../models/export/fido2-credential.export.ts | 41 +++++-------- libs/common/src/models/export/field.export.ts | 11 ++-- .../common/src/models/export/folder.export.ts | 8 +-- .../src/models/export/identity.export.ts | 59 +++++++------------ .../src/models/export/login-uri.export.ts | 8 +-- libs/common/src/models/export/login.export.ts | 19 ++---- .../models/export/password-history.export.ts | 8 +-- libs/common/src/models/export/utils.ts | 12 ++++ 11 files changed, 82 insertions(+), 137 deletions(-) create mode 100644 libs/common/src/models/export/utils.ts diff --git a/libs/common/src/models/export/card.export.ts b/libs/common/src/models/export/card.export.ts index 55bb3a7be1..151b447e86 100644 --- a/libs/common/src/models/export/card.export.ts +++ b/libs/common/src/models/export/card.export.ts @@ -2,6 +2,8 @@ import { EncString } from "../../platform/models/domain/enc-string"; import { Card as CardDomain } from "../../vault/models/domain/card"; import { CardView } from "../../vault/models/view/card.view"; +import { safeGetString } from "./utils"; + export class CardExport { static template(): CardExport { const req = new CardExport(); @@ -46,20 +48,11 @@ export class CardExport { return; } - if (o instanceof CardView) { - this.cardholderName = o.cardholderName; - this.brand = o.brand; - this.number = o.number; - this.expMonth = o.expMonth; - this.expYear = o.expYear; - this.code = o.code; - } else { - this.cardholderName = o.cardholderName?.encryptedString; - this.brand = o.brand?.encryptedString; - this.number = o.number?.encryptedString; - this.expMonth = o.expMonth?.encryptedString; - this.expYear = o.expYear?.encryptedString; - this.code = o.code?.encryptedString; - } + this.cardholderName = safeGetString(o.cardholderName); + this.brand = safeGetString(o.brand); + this.number = safeGetString(o.number); + this.expMonth = safeGetString(o.expMonth); + this.expYear = safeGetString(o.expYear); + this.code = safeGetString(o.code); } } diff --git a/libs/common/src/models/export/cipher.export.ts b/libs/common/src/models/export/cipher.export.ts index 3ae6c9757d..64583f7fce 100644 --- a/libs/common/src/models/export/cipher.export.ts +++ b/libs/common/src/models/export/cipher.export.ts @@ -10,6 +10,7 @@ import { IdentityExport } from "./identity.export"; import { LoginExport } from "./login.export"; import { PasswordHistoryExport } from "./password-history.export"; import { SecureNoteExport } from "./secure-note.export"; +import { safeGetString } from "./utils"; export class CipherExport { static template(): CipherExport { @@ -145,23 +146,16 @@ export class CipherExport { this.type = o.type; this.reprompt = o.reprompt; - if (o instanceof CipherView) { - this.name = o.name; - this.notes = o.notes; - } else { - this.name = o.name?.encryptedString; - this.notes = o.notes?.encryptedString; + this.name = safeGetString(o.name); + this.notes = safeGetString(o.notes); + if ("key" in o) { this.key = o.key?.encryptedString; } this.favorite = o.favorite; if (o.fields != null) { - if (o instanceof CipherView) { - this.fields = o.fields.map((f) => new FieldExport(f)); - } else { - this.fields = o.fields.map((f) => new FieldExport(f)); - } + this.fields = o.fields.map((f) => new FieldExport(f)); } switch (o.type) { @@ -180,11 +174,7 @@ export class CipherExport { } if (o.passwordHistory != null) { - if (o instanceof CipherView) { - this.passwordHistory = o.passwordHistory.map((ph) => new PasswordHistoryExport(ph)); - } else { - this.passwordHistory = o.passwordHistory.map((ph) => new PasswordHistoryExport(ph)); - } + this.passwordHistory = o.passwordHistory.map((ph) => new PasswordHistoryExport(ph)); } this.creationDate = o.creationDate; diff --git a/libs/common/src/models/export/collection.export.ts b/libs/common/src/models/export/collection.export.ts index 48251d581f..c94d5bc0ca 100644 --- a/libs/common/src/models/export/collection.export.ts +++ b/libs/common/src/models/export/collection.export.ts @@ -2,6 +2,8 @@ import { EncString } from "../../platform/models/domain/enc-string"; import { Collection as CollectionDomain } from "../../vault/models/domain/collection"; import { CollectionView } from "../../vault/models/view/collection.view"; +import { safeGetString } from "./utils"; + export class CollectionExport { static template(): CollectionExport { const req = new CollectionExport(); @@ -36,11 +38,7 @@ export class CollectionExport { // Use build method instead of ctor so that we can control order of JSON stringify for pretty print build(o: CollectionView | CollectionDomain) { this.organizationId = o.organizationId; - if (o instanceof CollectionView) { - this.name = o.name; - } else { - this.name = o.name?.encryptedString; - } + this.name = safeGetString(o.name); this.externalId = o.externalId; } } diff --git a/libs/common/src/models/export/fido2-credential.export.ts b/libs/common/src/models/export/fido2-credential.export.ts index d41b7d67c9..4c60d148db 100644 --- a/libs/common/src/models/export/fido2-credential.export.ts +++ b/libs/common/src/models/export/fido2-credential.export.ts @@ -2,6 +2,8 @@ import { EncString } from "../../platform/models/domain/enc-string"; import { Fido2Credential } from "../../vault/models/domain/fido2-credential"; import { Fido2CredentialView } from "../../vault/models/view/fido2-credential.view"; +import { safeGetString } from "./utils"; + /** * Represents format of Fido2 Credentials in JSON exports. */ @@ -99,33 +101,18 @@ export class Fido2CredentialExport { return; } - if (o instanceof Fido2CredentialView) { - this.credentialId = o.credentialId; - this.keyType = o.keyType; - this.keyAlgorithm = o.keyAlgorithm; - this.keyCurve = o.keyCurve; - this.keyValue = o.keyValue; - this.rpId = o.rpId; - this.userHandle = o.userHandle; - this.userName = o.userName; - this.counter = String(o.counter); - this.rpName = o.rpName; - this.userDisplayName = o.userDisplayName; - this.discoverable = String(o.discoverable); - } else { - this.credentialId = o.credentialId?.encryptedString; - this.keyType = o.keyType?.encryptedString; - this.keyAlgorithm = o.keyAlgorithm?.encryptedString; - this.keyCurve = o.keyCurve?.encryptedString; - this.keyValue = o.keyValue?.encryptedString; - this.rpId = o.rpId?.encryptedString; - this.userHandle = o.userHandle?.encryptedString; - this.userName = o.userName?.encryptedString; - this.counter = o.counter?.encryptedString; - this.rpName = o.rpName?.encryptedString; - this.userDisplayName = o.userDisplayName?.encryptedString; - this.discoverable = o.discoverable?.encryptedString; - } + this.credentialId = safeGetString(o.credentialId); + this.keyType = safeGetString(o.keyType); + this.keyAlgorithm = safeGetString(o.keyAlgorithm); + this.keyCurve = safeGetString(o.keyCurve); + this.keyValue = safeGetString(o.keyValue); + this.rpId = safeGetString(o.rpId); + this.userHandle = safeGetString(o.userHandle); + this.userName = safeGetString(o.userName); + this.counter = safeGetString(String(o.counter)); + this.rpName = safeGetString(o.rpName); + this.userDisplayName = safeGetString(o.userDisplayName); + this.discoverable = safeGetString(String(o.discoverable)); this.creationDate = o.creationDate; } } diff --git a/libs/common/src/models/export/field.export.ts b/libs/common/src/models/export/field.export.ts index 098249312c..5ba341af61 100644 --- a/libs/common/src/models/export/field.export.ts +++ b/libs/common/src/models/export/field.export.ts @@ -3,6 +3,8 @@ import { FieldType, LinkedIdType } from "../../vault/enums"; import { Field as FieldDomain } from "../../vault/models/domain/field"; import { FieldView } from "../../vault/models/view/field.view"; +import { safeGetString } from "./utils"; + export class FieldExport { static template(): FieldExport { const req = new FieldExport(); @@ -38,13 +40,8 @@ export class FieldExport { return; } - if (o instanceof FieldView) { - this.name = o.name; - this.value = o.value; - } else { - this.name = o.name?.encryptedString; - this.value = o.value?.encryptedString; - } + this.name = safeGetString(o.name); + this.value = safeGetString(o.value); this.type = o.type; this.linkedId = o.linkedId; } diff --git a/libs/common/src/models/export/folder.export.ts b/libs/common/src/models/export/folder.export.ts index 4015034ebe..6a2a63a77d 100644 --- a/libs/common/src/models/export/folder.export.ts +++ b/libs/common/src/models/export/folder.export.ts @@ -2,6 +2,8 @@ import { EncString } from "../../platform/models/domain/enc-string"; import { Folder as FolderDomain } from "../../vault/models/domain/folder"; import { FolderView } from "../../vault/models/view/folder.view"; +import { safeGetString } from "./utils"; + export class FolderExport { static template(): FolderExport { const req = new FolderExport(); @@ -23,10 +25,6 @@ export class FolderExport { // Use build method instead of ctor so that we can control order of JSON stringify for pretty print build(o: FolderView | FolderDomain) { - if (o instanceof FolderView) { - this.name = o.name; - } else { - this.name = o.name?.encryptedString; - } + this.name = safeGetString(o.name); } } diff --git a/libs/common/src/models/export/identity.export.ts b/libs/common/src/models/export/identity.export.ts index 2eb9c8364f..6722333d79 100644 --- a/libs/common/src/models/export/identity.export.ts +++ b/libs/common/src/models/export/identity.export.ts @@ -2,6 +2,8 @@ import { EncString } from "../../platform/models/domain/enc-string"; import { Identity as IdentityDomain } from "../../vault/models/domain/identity"; import { IdentityView } from "../../vault/models/view/identity.view"; +import { safeGetString } from "./utils"; + export class IdentityExport { static template(): IdentityExport { const req = new IdentityExport(); @@ -94,44 +96,23 @@ export class IdentityExport { return; } - if (o instanceof IdentityView) { - this.title = o.title; - this.firstName = o.firstName; - this.middleName = o.middleName; - this.lastName = o.lastName; - this.address1 = o.address1; - this.address2 = o.address2; - this.address3 = o.address3; - this.city = o.city; - this.state = o.state; - this.postalCode = o.postalCode; - this.country = o.country; - this.company = o.company; - this.email = o.email; - this.phone = o.phone; - this.ssn = o.ssn; - this.username = o.username; - this.passportNumber = o.passportNumber; - this.licenseNumber = o.licenseNumber; - } else { - this.title = o.title?.encryptedString; - this.firstName = o.firstName?.encryptedString; - this.middleName = o.middleName?.encryptedString; - this.lastName = o.lastName?.encryptedString; - this.address1 = o.address1?.encryptedString; - this.address2 = o.address2?.encryptedString; - this.address3 = o.address3?.encryptedString; - this.city = o.city?.encryptedString; - this.state = o.state?.encryptedString; - this.postalCode = o.postalCode?.encryptedString; - this.country = o.country?.encryptedString; - this.company = o.company?.encryptedString; - this.email = o.email?.encryptedString; - this.phone = o.phone?.encryptedString; - this.ssn = o.ssn?.encryptedString; - this.username = o.username?.encryptedString; - this.passportNumber = o.passportNumber?.encryptedString; - this.licenseNumber = o.licenseNumber?.encryptedString; - } + this.title = safeGetString(o.title); + this.firstName = safeGetString(o.firstName); + this.middleName = safeGetString(o.middleName); + this.lastName = safeGetString(o.lastName); + this.address1 = safeGetString(o.address1); + this.address2 = safeGetString(o.address2); + this.address3 = safeGetString(o.address3); + this.city = safeGetString(o.city); + this.state = safeGetString(o.state); + this.postalCode = safeGetString(o.postalCode); + this.country = safeGetString(o.country); + this.company = safeGetString(o.company); + this.email = safeGetString(o.email); + this.phone = safeGetString(o.phone); + this.ssn = safeGetString(o.ssn); + this.username = safeGetString(o.username); + this.passportNumber = safeGetString(o.passportNumber); + this.licenseNumber = safeGetString(o.licenseNumber); } } diff --git a/libs/common/src/models/export/login-uri.export.ts b/libs/common/src/models/export/login-uri.export.ts index 83a7d25eff..a053446061 100644 --- a/libs/common/src/models/export/login-uri.export.ts +++ b/libs/common/src/models/export/login-uri.export.ts @@ -3,6 +3,8 @@ import { EncString } from "../../platform/models/domain/enc-string"; import { LoginUri as LoginUriDomain } from "../../vault/models/domain/login-uri"; import { LoginUriView } from "../../vault/models/view/login-uri.view"; +import { safeGetString } from "./utils"; + export class LoginUriExport { static template(): LoginUriExport { const req = new LoginUriExport(); @@ -33,10 +35,8 @@ export class LoginUriExport { return; } - if (o instanceof LoginUriView) { - this.uri = o.uri; - } else { - this.uri = o.uri?.encryptedString; + this.uri = safeGetString(o.uri); + if ("uriChecksum" in o) { this.uriChecksum = o.uriChecksum?.encryptedString; } this.match = o.match; diff --git a/libs/common/src/models/export/login.export.ts b/libs/common/src/models/export/login.export.ts index a5d9348c2c..6982d386c3 100644 --- a/libs/common/src/models/export/login.export.ts +++ b/libs/common/src/models/export/login.export.ts @@ -4,6 +4,7 @@ import { LoginView } from "../../vault/models/view/login.view"; import { Fido2CredentialExport } from "./fido2-credential.export"; import { LoginUriExport } from "./login-uri.export"; +import { safeGetString } from "./utils"; export class LoginExport { static template(): LoginExport { @@ -53,25 +54,15 @@ export class LoginExport { } if (o.uris != null) { - if (o instanceof LoginView) { - this.uris = o.uris.map((u) => new LoginUriExport(u)); - } else { - this.uris = o.uris.map((u) => new LoginUriExport(u)); - } + this.uris = o.uris.map((u) => new LoginUriExport(u)); } if (o.fido2Credentials != null) { this.fido2Credentials = o.fido2Credentials.map((key) => new Fido2CredentialExport(key)); } - if (o instanceof LoginView) { - this.username = o.username; - this.password = o.password; - this.totp = o.totp; - } else { - this.username = o.username?.encryptedString; - this.password = o.password?.encryptedString; - this.totp = o.totp?.encryptedString; - } + this.username = safeGetString(o.username); + this.password = safeGetString(o.password); + this.totp = safeGetString(o.totp); } } diff --git a/libs/common/src/models/export/password-history.export.ts b/libs/common/src/models/export/password-history.export.ts index 0bdbc6697a..fff22de8de 100644 --- a/libs/common/src/models/export/password-history.export.ts +++ b/libs/common/src/models/export/password-history.export.ts @@ -2,6 +2,8 @@ import { EncString } from "../../platform/models/domain/enc-string"; import { Password } from "../../vault/models/domain/password"; import { PasswordHistoryView } from "../../vault/models/view/password-history.view"; +import { safeGetString } from "./utils"; + export class PasswordHistoryExport { static template(): PasswordHistoryExport { const req = new PasswordHistoryExport(); @@ -30,11 +32,7 @@ export class PasswordHistoryExport { return; } - if (o instanceof PasswordHistoryView) { - this.password = o.password; - } else { - this.password = o.password?.encryptedString; - } + this.password = safeGetString(o.password); this.lastUsedDate = o.lastUsedDate; } } diff --git a/libs/common/src/models/export/utils.ts b/libs/common/src/models/export/utils.ts new file mode 100644 index 0000000000..630b489850 --- /dev/null +++ b/libs/common/src/models/export/utils.ts @@ -0,0 +1,12 @@ +import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; + +export function safeGetString(value: string | EncString) { + if (value == null) { + return null; + } + + if (typeof value == "string") { + return value; + } + return value?.encryptedString; +} From 1e4158fd878994093710db9d072f356a9fc8bdc8 Mon Sep 17 00:00:00 2001 From: Ike <137194738+ike-kottlowski@users.noreply.github.com> Date: Thu, 25 Apr 2024 11:26:01 -0700 Subject: [PATCH 280/351] [PM-5735] Create kdf Service (#8715) * key connector migration initial * migrator complete * fix dependencies * finalized tests * fix deps and sync main * clean up definition file * fixing tests * fixed tests * fixing CLI, Browser, Desktop builds * fixed factory options * reverting exports * implemented UserKeyDefinition clearOn * Initial Kdf Service Changes * rename and account setting kdfconfig * fixing tests and renaming migration * fixed DI ordering for browser * rename and fix DI * Clean up Migrations * fixing migrations * begin data structure changes for kdf config * Make KDF more type safe; co-author: jlf0dev * fixing tests * Fixed CLI login and comments * set now accepts userId and test updates --------- Co-authored-by: Jake Fink <jfink@bitwarden.com> --- .../kdf-config-service.factory.ts | 28 ++++ .../login-strategy-service.factory.ts | 5 +- .../pin-crypto-service.factory.ts | 6 +- .../user-verification-service.factory.ts | 5 +- apps/browser/src/auth/popup/lock.component.ts | 3 + .../browser/src/background/main.background.ts | 14 +- .../crypto-service.factory.ts | 14 +- .../services/browser-crypto.service.ts | 3 + apps/cli/src/auth/commands/login.command.ts | 6 +- apps/cli/src/auth/commands/unlock.command.ts | 7 +- apps/cli/src/bw.ts | 13 +- apps/cli/src/commands/serve.command.ts | 1 + apps/cli/src/program.ts | 3 + .../src/app/services/services.module.ts | 2 + apps/desktop/src/auth/lock.component.spec.ts | 5 + apps/desktop/src/auth/lock.component.ts | 3 + .../src/auth/set-password.component.ts | 3 + .../services/electron-crypto.service.spec.ts | 3 + .../services/electron-crypto.service.ts | 3 + ...rganization-user-reset-password.service.ts | 16 +- .../services/emergency-access.service.ts | 33 ++-- .../user-key-rotation.service.spec.ts | 4 + .../key-rotation/user-key-rotation.service.ts | 5 +- .../account/change-email.component.ts | 6 +- .../settings/change-password.component.ts | 3 + .../emergency-access-takeover.component.ts | 3 + .../change-kdf-confirmation.component.ts | 22 +-- .../change-kdf/change-kdf.component.html | 12 +- .../change-kdf/change-kdf.component.ts | 31 ++-- .../src/app/auth/update-password.component.ts | 3 + .../vault/individual-vault/vault.component.ts | 8 +- .../components/change-password.component.ts | 10 +- .../src/auth/components/lock.component.ts | 6 +- .../src/auth/components/register.component.ts | 9 +- .../auth/components/set-password.component.ts | 18 +-- .../src/auth/components/set-pin.component.ts | 5 +- .../components/update-password.component.ts | 6 +- .../update-temp-password.component.ts | 7 +- .../src/services/jslib-services.module.ts | 15 +- .../auth-request-login.strategy.spec.ts | 4 + .../auth-request-login.strategy.ts | 3 + .../login-strategies/login.strategy.spec.ts | 7 +- .../common/login-strategies/login.strategy.ts | 19 ++- .../password-login.strategy.spec.ts | 4 + .../password-login.strategy.ts | 3 + .../sso-login.strategy.spec.ts | 4 + .../login-strategies/sso-login.strategy.ts | 3 + .../user-api-login.strategy.spec.ts | 4 + .../user-api-login.strategy.ts | 3 + .../webauthn-login.strategy.spec.ts | 4 + .../webauthn-login.strategy.ts | 3 + .../login-strategy.service.spec.ts | 4 + .../login-strategy.service.ts | 32 ++-- .../pin-crypto.service.implementation.ts | 8 +- .../pin-crypto/pin-crypto.service.spec.ts | 9 +- .../auth/abstractions/kdf-config.service.ts | 7 + .../src/auth/models/domain/kdf-config.ts | 91 ++++++++++- .../request/set-key-connector-key.request.ts | 16 +- .../auth/services/kdf-config.service.spec.ts | 104 ++++++++++++ .../src/auth/services/kdf-config.service.ts | 41 +++++ .../auth/services/key-connector.service.ts | 10 +- .../user-verification.service.ts | 8 +- .../platform/abstractions/crypto.service.ts | 31 +--- .../abstractions/key-generation.service.ts | 3 - .../platform/abstractions/state.service.ts | 6 - .../src/platform/enums/kdf-type.enum.ts | 4 +- .../platform/services/crypto.service.spec.ts | 3 + .../src/platform/services/crypto.service.ts | 79 ++------- .../services/key-generation.service.spec.ts | 13 +- .../services/key-generation.service.ts | 5 +- .../src/platform/services/state.service.ts | 46 +----- .../src/platform/state/state-definitions.ts | 1 + libs/common/src/state-migrations/migrate.ts | 6 +- ...-move-kdf-config-to-state-provider.spec.ts | 153 ++++++++++++++++++ .../59-move-kdf-config-to-state-provider.ts | 78 +++++++++ .../src/tools/send/services/send.service.ts | 5 +- .../bitwarden-password-protected-importer.ts | 18 ++- .../src/services/base-vault-export.service.ts | 18 ++- .../individual-vault-export.service.spec.ts | 18 +-- .../individual-vault-export.service.ts | 6 +- .../src/services/org-vault-export.service.ts | 6 +- .../src/services/vault-export.service.spec.ts | 18 +-- 82 files changed, 896 insertions(+), 361 deletions(-) create mode 100644 apps/browser/src/auth/background/service-factories/kdf-config-service.factory.ts create mode 100644 libs/common/src/auth/abstractions/kdf-config.service.ts create mode 100644 libs/common/src/auth/services/kdf-config.service.spec.ts create mode 100644 libs/common/src/auth/services/kdf-config.service.ts create mode 100644 libs/common/src/state-migrations/migrations/59-move-kdf-config-to-state-provider.spec.ts create mode 100644 libs/common/src/state-migrations/migrations/59-move-kdf-config-to-state-provider.ts diff --git a/apps/browser/src/auth/background/service-factories/kdf-config-service.factory.ts b/apps/browser/src/auth/background/service-factories/kdf-config-service.factory.ts new file mode 100644 index 0000000000..eb5ba3a264 --- /dev/null +++ b/apps/browser/src/auth/background/service-factories/kdf-config-service.factory.ts @@ -0,0 +1,28 @@ +import { KdfConfigService as AbstractKdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; +import { KdfConfigService } from "@bitwarden/common/auth/services/kdf-config.service"; + +import { + FactoryOptions, + CachedServices, + factory, +} from "../../../platform/background/service-factories/factory-options"; +import { + StateProviderInitOptions, + stateProviderFactory, +} from "../../../platform/background/service-factories/state-provider.factory"; + +type KdfConfigServiceFactoryOptions = FactoryOptions; + +export type KdfConfigServiceInitOptions = KdfConfigServiceFactoryOptions & StateProviderInitOptions; + +export function kdfConfigServiceFactory( + cache: { kdfConfigService?: AbstractKdfConfigService } & CachedServices, + opts: KdfConfigServiceInitOptions, +): Promise<AbstractKdfConfigService> { + return factory( + cache, + "kdfConfigService", + opts, + async () => new KdfConfigService(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 075ba614b7..c414300431 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 @@ -68,6 +68,7 @@ import { deviceTrustServiceFactory, DeviceTrustServiceInitOptions, } from "./device-trust-service.factory"; +import { kdfConfigServiceFactory, KdfConfigServiceInitOptions } from "./kdf-config-service.factory"; import { keyConnectorServiceFactory, KeyConnectorServiceInitOptions, @@ -106,7 +107,8 @@ export type LoginStrategyServiceInitOptions = LoginStrategyServiceFactoryOptions AuthRequestServiceInitOptions & UserDecryptionOptionsServiceInitOptions & GlobalStateProviderInitOptions & - BillingAccountProfileStateServiceInitOptions; + BillingAccountProfileStateServiceInitOptions & + KdfConfigServiceInitOptions; export function loginStrategyServiceFactory( cache: { loginStrategyService?: LoginStrategyServiceAbstraction } & CachedServices, @@ -140,6 +142,7 @@ export function loginStrategyServiceFactory( await internalUserDecryptionOptionServiceFactory(cache, opts), await globalStateProviderFactory(cache, opts), await billingAccountProfileStateServiceFactory(cache, opts), + await kdfConfigServiceFactory(cache, opts), ), ); } diff --git a/apps/browser/src/auth/background/service-factories/pin-crypto-service.factory.ts b/apps/browser/src/auth/background/service-factories/pin-crypto-service.factory.ts index f5360f48fa..db16245f67 100644 --- a/apps/browser/src/auth/background/service-factories/pin-crypto-service.factory.ts +++ b/apps/browser/src/auth/background/service-factories/pin-crypto-service.factory.ts @@ -22,13 +22,16 @@ import { stateServiceFactory, } from "../../../platform/background/service-factories/state-service.factory"; +import { KdfConfigServiceInitOptions, kdfConfigServiceFactory } from "./kdf-config-service.factory"; + type PinCryptoServiceFactoryOptions = FactoryOptions; export type PinCryptoServiceInitOptions = PinCryptoServiceFactoryOptions & StateServiceInitOptions & CryptoServiceInitOptions & VaultTimeoutSettingsServiceInitOptions & - LogServiceInitOptions; + LogServiceInitOptions & + KdfConfigServiceInitOptions; export function pinCryptoServiceFactory( cache: { pinCryptoService?: PinCryptoServiceAbstraction } & CachedServices, @@ -44,6 +47,7 @@ export function pinCryptoServiceFactory( await cryptoServiceFactory(cache, opts), await vaultTimeoutSettingsServiceFactory(cache, opts), await logServiceFactory(cache, opts), + await kdfConfigServiceFactory(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 a8b67b21ca..d6f9ce7624 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 @@ -32,6 +32,7 @@ import { } from "../../../platform/background/service-factories/state-service.factory"; import { accountServiceFactory, AccountServiceInitOptions } from "./account-service.factory"; +import { KdfConfigServiceInitOptions, kdfConfigServiceFactory } from "./kdf-config-service.factory"; import { internalMasterPasswordServiceFactory, MasterPasswordServiceInitOptions, @@ -59,7 +60,8 @@ export type UserVerificationServiceInitOptions = UserVerificationServiceFactoryO PinCryptoServiceInitOptions & LogServiceInitOptions & VaultTimeoutSettingsServiceInitOptions & - PlatformUtilsServiceInitOptions; + PlatformUtilsServiceInitOptions & + KdfConfigServiceInitOptions; export function userVerificationServiceFactory( cache: { userVerificationService?: AbstractUserVerificationService } & CachedServices, @@ -82,6 +84,7 @@ export function userVerificationServiceFactory( await logServiceFactory(cache, opts), await vaultTimeoutSettingsServiceFactory(cache, opts), await platformUtilsServiceFactory(cache, opts), + await kdfConfigServiceFactory(cache, opts), ), ); } diff --git a/apps/browser/src/auth/popup/lock.component.ts b/apps/browser/src/auth/popup/lock.component.ts index 78039d793f..4d47417df6 100644 --- a/apps/browser/src/auth/popup/lock.component.ts +++ b/apps/browser/src/auth/popup/lock.component.ts @@ -12,6 +12,7 @@ import { InternalPolicyService } from "@bitwarden/common/admin-console/abstracti import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.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 { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; @@ -66,6 +67,7 @@ export class LockComponent extends BaseLockComponent { private routerService: BrowserRouterService, biometricStateService: BiometricStateService, accountService: AccountService, + kdfConfigService: KdfConfigService, ) { super( masterPasswordService, @@ -90,6 +92,7 @@ export class LockComponent extends BaseLockComponent { pinCryptoService, biometricStateService, accountService, + kdfConfigService, ); this.successRoute = "/tabs/current"; this.isInitialLockScreen = (window as any).previousPopupUrl == null; diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index dc93de7803..b4375df7d5 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -33,6 +33,7 @@ import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/aut import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; +import { KdfConfigService as kdfConfigServiceAbstraction } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; @@ -48,6 +49,7 @@ import { AvatarService } from "@bitwarden/common/auth/services/avatar.service"; import { DeviceTrustService } from "@bitwarden/common/auth/services/device-trust.service.implementation"; import { DevicesServiceImplementation } from "@bitwarden/common/auth/services/devices/devices.service.implementation"; import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation"; +import { KdfConfigService } from "@bitwarden/common/auth/services/kdf-config.service"; import { KeyConnectorService } from "@bitwarden/common/auth/services/key-connector.service"; import { MasterPasswordService } from "@bitwarden/common/auth/services/master-password/master-password.service"; import { SsoLoginService } from "@bitwarden/common/auth/services/sso-login.service"; @@ -339,6 +341,7 @@ export default class MainBackground { intraprocessMessagingSubject: Subject<Message<object>>; userKeyInitService: UserKeyInitService; scriptInjectorService: BrowserScriptInjectorService; + kdfConfigService: kdfConfigServiceAbstraction; onUpdatedRan: boolean; onReplacedRan: boolean; @@ -542,6 +545,9 @@ export default class MainBackground { this.masterPasswordService = new MasterPasswordService(this.stateProvider); this.i18nService = new I18nService(BrowserApi.getUILanguage(), this.globalStateProvider); + + this.kdfConfigService = new KdfConfigService(this.stateProvider); + this.cryptoService = new BrowserCryptoService( this.masterPasswordService, this.keyGenerationService, @@ -553,6 +559,7 @@ export default class MainBackground { this.accountService, this.stateProvider, this.biometricStateService, + this.kdfConfigService, ); this.appIdService = new AppIdService(this.globalStateProvider); @@ -675,6 +682,7 @@ export default class MainBackground { this.userDecryptionOptionsService, this.globalStateProvider, this.billingAccountProfileStateService, + this.kdfConfigService, ); this.ssoLoginService = new SsoLoginService(this.stateProvider); @@ -725,6 +733,7 @@ export default class MainBackground { this.cryptoService, this.vaultTimeoutSettingsService, this.logService, + this.kdfConfigService, ); this.userVerificationService = new UserVerificationService( @@ -739,6 +748,7 @@ export default class MainBackground { this.logService, this.vaultTimeoutSettingsService, this.platformUtilsService, + this.kdfConfigService, ); this.vaultFilterService = new VaultFilterService( @@ -861,7 +871,7 @@ export default class MainBackground { this.cipherService, this.cryptoService, this.cryptoFunctionService, - this.stateService, + this.kdfConfigService, ); this.organizationVaultExportService = new OrganizationVaultExportService( @@ -869,8 +879,8 @@ export default class MainBackground { this.apiService, this.cryptoService, this.cryptoFunctionService, - this.stateService, this.collectionService, + this.kdfConfigService, ); this.exportService = new VaultExportService( diff --git a/apps/browser/src/platform/background/service-factories/crypto-service.factory.ts b/apps/browser/src/platform/background/service-factories/crypto-service.factory.ts index ed4fde162c..1f848e1d0f 100644 --- a/apps/browser/src/platform/background/service-factories/crypto-service.factory.ts +++ b/apps/browser/src/platform/background/service-factories/crypto-service.factory.ts @@ -4,6 +4,10 @@ import { AccountServiceInitOptions, accountServiceFactory, } from "../../../auth/background/service-factories/account-service.factory"; +import { + KdfConfigServiceInitOptions, + kdfConfigServiceFactory, +} from "../../../auth/background/service-factories/kdf-config-service.factory"; import { internalMasterPasswordServiceFactory, MasterPasswordServiceInitOptions, @@ -18,7 +22,10 @@ import { } from "../../background/service-factories/log-service.factory"; import { BrowserCryptoService } from "../../services/browser-crypto.service"; -import { biometricStateServiceFactory } from "./biometric-state-service.factory"; +import { + BiometricStateServiceInitOptions, + biometricStateServiceFactory, +} from "./biometric-state-service.factory"; import { cryptoFunctionServiceFactory, CryptoFunctionServiceInitOptions, @@ -46,7 +53,9 @@ export type CryptoServiceInitOptions = CryptoServiceFactoryOptions & LogServiceInitOptions & StateServiceInitOptions & AccountServiceInitOptions & - StateProviderInitOptions; + StateProviderInitOptions & + BiometricStateServiceInitOptions & + KdfConfigServiceInitOptions; export function cryptoServiceFactory( cache: { cryptoService?: AbstractCryptoService } & CachedServices, @@ -68,6 +77,7 @@ export function cryptoServiceFactory( await accountServiceFactory(cache, opts), await stateProviderFactory(cache, opts), await biometricStateServiceFactory(cache, opts), + await kdfConfigServiceFactory(cache, opts), ), ); } diff --git a/apps/browser/src/platform/services/browser-crypto.service.ts b/apps/browser/src/platform/services/browser-crypto.service.ts index d7533a22d6..cd23c916c6 100644 --- a/apps/browser/src/platform/services/browser-crypto.service.ts +++ b/apps/browser/src/platform/services/browser-crypto.service.ts @@ -1,6 +1,7 @@ import { firstValueFrom } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.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"; @@ -28,6 +29,7 @@ export class BrowserCryptoService extends CryptoService { accountService: AccountService, stateProvider: StateProvider, private biometricStateService: BiometricStateService, + kdfConfigService: KdfConfigService, ) { super( masterPasswordService, @@ -39,6 +41,7 @@ export class BrowserCryptoService extends CryptoService { stateService, accountService, stateProvider, + kdfConfigService, ); } override async hasUserKeyStored(keySuffix: KeySuffixOptions, userId?: UserId): Promise<boolean> { diff --git a/apps/cli/src/auth/commands/login.command.ts b/apps/cli/src/auth/commands/login.command.ts index a91e876e92..3606285c72 100644 --- a/apps/cli/src/auth/commands/login.command.ts +++ b/apps/cli/src/auth/commands/login.command.ts @@ -16,6 +16,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction 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 { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; @@ -68,6 +69,7 @@ export class LoginCommand { protected policyApiService: PolicyApiServiceAbstraction, protected orgService: OrganizationService, protected logoutCallback: () => Promise<void>, + protected kdfConfigService: KdfConfigService, ) {} async run(email: string, password: string, options: OptionValues) { @@ -563,14 +565,12 @@ export class LoginCommand { message: "Master Password Hint (optional):", }); const masterPasswordHint = hint.input; - const kdf = await this.stateService.getKdfType(); - const kdfConfig = await this.stateService.getKdfConfig(); + const kdfConfig = await this.kdfConfigService.getKdfConfig(); // Create new key and hash new password const newMasterKey = await this.cryptoService.makeMasterKey( masterPassword, this.email.trim().toLowerCase(), - kdf, kdfConfig, ); const newPasswordHash = await this.cryptoService.hashMasterKey(masterPassword, newMasterKey); diff --git a/apps/cli/src/auth/commands/unlock.command.ts b/apps/cli/src/auth/commands/unlock.command.ts index d52468139a..6b97b59c88 100644 --- a/apps/cli/src/auth/commands/unlock.command.ts +++ b/apps/cli/src/auth/commands/unlock.command.ts @@ -3,6 +3,7 @@ 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 { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.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"; @@ -34,6 +35,7 @@ export class UnlockCommand { private syncService: SyncService, private organizationApiService: OrganizationApiServiceAbstraction, private logout: () => Promise<void>, + private kdfConfigService: KdfConfigService, ) {} async run(password: string, cmdOptions: Record<string, any>) { @@ -48,9 +50,8 @@ export class UnlockCommand { await this.setNewSessionKey(); const email = await this.stateService.getEmail(); - const kdf = await this.stateService.getKdfType(); - const kdfConfig = await this.stateService.getKdfConfig(); - const masterKey = await this.cryptoService.makeMasterKey(password, email, kdf, kdfConfig); + const kdfConfig = await this.kdfConfigService.getKdfConfig(); + const masterKey = await this.cryptoService.makeMasterKey(password, email, kdfConfig); const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; const storedMasterKeyHash = await firstValueFrom( this.masterPasswordService.masterKeyHash$(userId), diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index be3ad9ea0e..ffe6c128b5 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -30,12 +30,14 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/auth/abstractions/avatar.service"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; +import { KdfConfigService as KdfConfigServiceAbstraction } from "@bitwarden/common/auth/abstractions/kdf-config.service"; 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 { DeviceTrustService } from "@bitwarden/common/auth/services/device-trust.service.implementation"; import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation"; +import { KdfConfigService } from "@bitwarden/common/auth/services/kdf-config.service"; 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"; @@ -235,6 +237,7 @@ export class Main { billingAccountProfileStateService: BillingAccountProfileStateService; providerApiService: ProviderApiServiceAbstraction; userKeyInitService: UserKeyInitService; + kdfConfigService: KdfConfigServiceAbstraction; constructor() { let p = null; @@ -357,6 +360,8 @@ export class Main { this.masterPasswordService = new MasterPasswordService(this.stateProvider); + this.kdfConfigService = new KdfConfigService(this.stateProvider); + this.cryptoService = new CryptoService( this.masterPasswordService, this.keyGenerationService, @@ -367,6 +372,7 @@ export class Main { this.stateService, this.accountService, this.stateProvider, + this.kdfConfigService, ); this.appIdService = new AppIdService(this.globalStateProvider); @@ -512,6 +518,7 @@ export class Main { this.userDecryptionOptionsService, this.globalStateProvider, this.billingAccountProfileStateService, + this.kdfConfigService, ); this.authService = new AuthService( @@ -574,6 +581,7 @@ export class Main { this.cryptoService, this.vaultTimeoutSettingsService, this.logService, + this.kdfConfigService, ); this.userVerificationService = new UserVerificationService( @@ -588,6 +596,7 @@ export class Main { this.logService, this.vaultTimeoutSettingsService, this.platformUtilsService, + this.kdfConfigService, ); this.vaultTimeoutService = new VaultTimeoutService( @@ -654,7 +663,7 @@ export class Main { this.cipherService, this.cryptoService, this.cryptoFunctionService, - this.stateService, + this.kdfConfigService, ); this.organizationExportService = new OrganizationVaultExportService( @@ -662,8 +671,8 @@ export class Main { this.apiService, this.cryptoService, this.cryptoFunctionService, - this.stateService, this.collectionService, + this.kdfConfigService, ); this.exportService = new VaultExportService( diff --git a/apps/cli/src/commands/serve.command.ts b/apps/cli/src/commands/serve.command.ts index 76447f769c..7a11dc4b4a 100644 --- a/apps/cli/src/commands/serve.command.ts +++ b/apps/cli/src/commands/serve.command.ts @@ -134,6 +134,7 @@ export class ServeCommand { this.main.syncService, this.main.organizationApiService, async () => await this.main.logout(), + this.main.kdfConfigService, ); this.sendCreateCommand = new SendCreateCommand( diff --git a/apps/cli/src/program.ts b/apps/cli/src/program.ts index fa71a88f54..5d26b0850e 100644 --- a/apps/cli/src/program.ts +++ b/apps/cli/src/program.ts @@ -156,6 +156,7 @@ export class Program { this.main.policyApiService, this.main.organizationService, async () => await this.main.logout(), + this.main.kdfConfigService, ); const response = await command.run(email, password, options); this.processResponse(response, true); @@ -265,6 +266,7 @@ export class Program { this.main.syncService, this.main.organizationApiService, async () => await this.main.logout(), + this.main.kdfConfigService, ); const response = await command.run(password, cmd); this.processResponse(response); @@ -627,6 +629,7 @@ export class Program { this.main.syncService, this.main.organizationApiService, this.main.logout, + this.main.kdfConfigService, ); const response = await command.run(null, null); if (!response.success) { diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index c15743ba5c..b888df8013 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -21,6 +21,7 @@ import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaul 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 { KdfConfigService as KdfConfigServiceAbstraction } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; @@ -258,6 +259,7 @@ const safeProviders: SafeProvider[] = [ AccountServiceAbstraction, StateProvider, BiometricStateService, + KdfConfigServiceAbstraction, ], }), safeProvider({ diff --git a/apps/desktop/src/auth/lock.component.spec.ts b/apps/desktop/src/auth/lock.component.spec.ts index 480e443eab..f998e75d7a 100644 --- a/apps/desktop/src/auth/lock.component.spec.ts +++ b/apps/desktop/src/auth/lock.component.spec.ts @@ -14,6 +14,7 @@ import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abs import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.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 { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; @@ -164,6 +165,10 @@ describe("LockComponent", () => { provide: AccountService, useValue: accountService, }, + { + provide: KdfConfigService, + useValue: mock<KdfConfigService>(), + }, ], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); diff --git a/apps/desktop/src/auth/lock.component.ts b/apps/desktop/src/auth/lock.component.ts index b8feef4ab5..8e87b6663f 100644 --- a/apps/desktop/src/auth/lock.component.ts +++ b/apps/desktop/src/auth/lock.component.ts @@ -11,6 +11,7 @@ import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abs import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.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 { DeviceType } from "@bitwarden/common/enums"; @@ -63,6 +64,7 @@ export class LockComponent extends BaseLockComponent { pinCryptoService: PinCryptoServiceAbstraction, biometricStateService: BiometricStateService, accountService: AccountService, + kdfConfigService: KdfConfigService, ) { super( masterPasswordService, @@ -87,6 +89,7 @@ export class LockComponent extends BaseLockComponent { pinCryptoService, biometricStateService, accountService, + kdfConfigService, ); } diff --git a/apps/desktop/src/auth/set-password.component.ts b/apps/desktop/src/auth/set-password.component.ts index 93dfe0abd8..feea5edd86 100644 --- a/apps/desktop/src/auth/set-password.component.ts +++ b/apps/desktop/src/auth/set-password.component.ts @@ -9,6 +9,7 @@ import { OrganizationUserService } from "@bitwarden/common/admin-console/abstrac 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 { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.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"; @@ -52,6 +53,7 @@ export class SetPasswordComponent extends BaseSetPasswordComponent implements On userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction, dialogService: DialogService, + kdfConfigService: KdfConfigService, ) { super( accountService, @@ -73,6 +75,7 @@ export class SetPasswordComponent extends BaseSetPasswordComponent implements On userDecryptionOptionsService, ssoLoginService, dialogService, + kdfConfigService, ); } 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 3d9171b52e..86463dccaa 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 { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; 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"; @@ -35,6 +36,7 @@ describe("electronCryptoService", () => { let accountService: FakeAccountService; let stateProvider: FakeStateProvider; const biometricStateService = mock<BiometricStateService>(); + const kdfConfigService = mock<KdfConfigService>(); const mockUserId = "mock user id" as UserId; @@ -54,6 +56,7 @@ describe("electronCryptoService", () => { accountService, stateProvider, biometricStateService, + kdfConfigService, ); }); diff --git a/apps/desktop/src/platform/services/electron-crypto.service.ts b/apps/desktop/src/platform/services/electron-crypto.service.ts index d113a18200..0ed0f73d41 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 { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.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"; @@ -31,6 +32,7 @@ export class ElectronCryptoService extends CryptoService { accountService: AccountService, stateProvider: StateProvider, private biometricStateService: BiometricStateService, + kdfConfigService: KdfConfigService, ) { super( masterPasswordService, @@ -42,6 +44,7 @@ export class ElectronCryptoService extends CryptoService { stateService, accountService, stateProvider, + kdfConfigService, ); } diff --git a/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.ts b/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.ts index cd94513f19..fcdbe1e496 100644 --- a/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.ts +++ b/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.ts @@ -7,10 +7,15 @@ import { OrganizationUserResetPasswordRequest, OrganizationUserResetPasswordWithIdRequest, } from "@bitwarden/common/admin-console/abstractions/organization-user/requests"; -import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; +import { + Argon2KdfConfig, + KdfConfig, + PBKDF2KdfConfig, +} from "@bitwarden/common/auth/models/domain/kdf-config"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { KdfType } from "@bitwarden/common/platform/enums"; 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"; @@ -90,12 +95,17 @@ export class OrganizationUserResetPasswordService { const decValue = await this.cryptoService.rsaDecrypt(response.resetPasswordKey, decPrivateKey); const existingUserKey = new SymmetricCryptoKey(decValue) as UserKey; + // determine Kdf Algorithm + const kdfConfig: KdfConfig = + response.kdf === KdfType.PBKDF2_SHA256 + ? new PBKDF2KdfConfig(response.kdfIterations) + : new Argon2KdfConfig(response.kdfIterations, response.kdfMemory, response.kdfParallelism); + // Create new master key and hash new password const newMasterKey = await this.cryptoService.makeMasterKey( newMasterPassword, email.trim().toLowerCase(), - response.kdf, - new KdfConfig(response.kdfIterations, response.kdfMemory, response.kdfParallelism), + kdfConfig, ); const newMasterKeyHash = await this.cryptoService.hashMasterKey( newMasterPassword, diff --git a/apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts b/apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts index 6bcb933e51..dbc1ce820c 100644 --- a/apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts +++ b/apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts @@ -3,10 +3,15 @@ import { Injectable } from "@angular/core"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyData } from "@bitwarden/common/admin-console/models/data/policy.data"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; -import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; +import { + Argon2KdfConfig, + KdfConfig, + PBKDF2KdfConfig, +} from "@bitwarden/common/auth/models/domain/kdf-config"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { KdfType } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncryptedString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; @@ -231,16 +236,22 @@ export class EmergencyAccessService { const grantorUserKey = new SymmetricCryptoKey(grantorKeyBuffer) as UserKey; - const masterKey = await this.cryptoService.makeMasterKey( - masterPassword, - email, - takeoverResponse.kdf, - new KdfConfig( - takeoverResponse.kdfIterations, - takeoverResponse.kdfMemory, - takeoverResponse.kdfParallelism, - ), - ); + let config: KdfConfig; + + switch (takeoverResponse.kdf) { + case KdfType.PBKDF2_SHA256: + config = new PBKDF2KdfConfig(takeoverResponse.kdfIterations); + break; + case KdfType.Argon2id: + config = new Argon2KdfConfig( + takeoverResponse.kdfIterations, + takeoverResponse.kdfMemory, + takeoverResponse.kdfParallelism, + ); + break; + } + + const masterKey = await this.cryptoService.makeMasterKey(masterPassword, email, config); const masterKeyHash = await this.cryptoService.hashMasterKey(masterPassword, masterKey); const encKey = await this.cryptoService.encryptUserKeyWithMasterKey(masterKey, grantorUserKey); 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 ed665fe773..ec68556931 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,6 +2,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject } from "rxjs"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; 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"; @@ -47,6 +48,7 @@ describe("KeyRotationService", () => { let mockEncryptService: MockProxy<EncryptService>; let mockStateService: MockProxy<StateService>; let mockConfigService: MockProxy<ConfigService>; + let mockKdfConfigService: MockProxy<KdfConfigService>; const mockUserId = Utils.newGuid() as UserId; const mockAccountService: FakeAccountService = mockAccountServiceWith(mockUserId); @@ -65,6 +67,7 @@ describe("KeyRotationService", () => { mockEncryptService = mock<EncryptService>(); mockStateService = mock<StateService>(); mockConfigService = mock<ConfigService>(); + mockKdfConfigService = mock<KdfConfigService>(); keyRotationService = new UserKeyRotationService( mockMasterPasswordService, @@ -80,6 +83,7 @@ describe("KeyRotationService", () => { mockStateService, mockAccountService, mockConfigService, + mockKdfConfigService, ); }); 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 2ff48809a0..94c6208115 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 @@ -3,6 +3,7 @@ import { firstValueFrom } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -39,6 +40,7 @@ export class UserKeyRotationService { private stateService: StateService, private accountService: AccountService, private configService: ConfigService, + private kdfConfigService: KdfConfigService, ) {} /** @@ -54,8 +56,7 @@ export class UserKeyRotationService { const masterKey = await this.cryptoService.makeMasterKey( masterPassword, await this.stateService.getEmail(), - await this.stateService.getKdfType(), - await this.stateService.getKdfConfig(), + await this.kdfConfigService.getKdfConfig(), ); if (!masterKey) { diff --git a/apps/web/src/app/auth/settings/account/change-email.component.ts b/apps/web/src/app/auth/settings/account/change-email.component.ts index 372b344b10..e5a3c72337 100644 --- a/apps/web/src/app/auth/settings/account/change-email.component.ts +++ b/apps/web/src/app/auth/settings/account/change-email.component.ts @@ -2,6 +2,7 @@ import { Component, OnInit } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; import { EmailTokenRequest } from "@bitwarden/common/auth/models/request/email-token.request"; import { EmailRequest } from "@bitwarden/common/auth/models/request/email.request"; @@ -37,6 +38,7 @@ export class ChangeEmailComponent implements OnInit { private logService: LogService, private stateService: StateService, private formBuilder: FormBuilder, + private kdfConfigService: KdfConfigService, ) {} async ngOnInit() { @@ -83,12 +85,10 @@ export class ChangeEmailComponent implements OnInit { step1Value.masterPassword, await this.cryptoService.getOrDeriveMasterKey(step1Value.masterPassword), ); - const kdf = await this.stateService.getKdfType(); - const kdfConfig = await this.stateService.getKdfConfig(); + const kdfConfig = await this.kdfConfigService.getKdfConfig(); const newMasterKey = await this.cryptoService.makeMasterKey( step1Value.masterPassword, newEmail, - kdf, kdfConfig, ); request.newMasterPasswordHash = await this.cryptoService.hashMasterKey( diff --git a/apps/web/src/app/auth/settings/change-password.component.ts b/apps/web/src/app/auth/settings/change-password.component.ts index 6d16893170..454d96f2bd 100644 --- a/apps/web/src/app/auth/settings/change-password.component.ts +++ b/apps/web/src/app/auth/settings/change-password.component.ts @@ -5,6 +5,7 @@ import { ChangePasswordComponent as BaseChangePasswordComponent } from "@bitward import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -48,6 +49,7 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent { dialogService: DialogService, private userVerificationService: UserVerificationService, private keyRotationService: UserKeyRotationService, + kdfConfigService: KdfConfigService, ) { super( i18nService, @@ -58,6 +60,7 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent { policyService, stateService, dialogService, + kdfConfigService, ); } diff --git a/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.ts b/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.ts index 575c6f4a23..73b1fa775d 100644 --- a/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.ts @@ -5,6 +5,7 @@ import { takeUntil } from "rxjs"; import { ChangePasswordComponent } from "@bitwarden/angular/auth/components/change-password.component"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-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"; @@ -58,6 +59,7 @@ export class EmergencyAccessTakeoverComponent private logService: LogService, dialogService: DialogService, private dialogRef: DialogRef<EmergencyAccessTakeoverResultType>, + kdfConfigService: KdfConfigService, ) { super( i18nService, @@ -68,6 +70,7 @@ export class EmergencyAccessTakeoverComponent policyService, stateService, dialogService, + kdfConfigService, ); } diff --git a/apps/web/src/app/auth/settings/security/change-kdf/change-kdf-confirmation.component.ts b/apps/web/src/app/auth/settings/security/change-kdf/change-kdf-confirmation.component.ts index 0284c665d8..985fb3e038 100644 --- a/apps/web/src/app/auth/settings/security/change-kdf/change-kdf-confirmation.component.ts +++ b/apps/web/src/app/auth/settings/security/change-kdf/change-kdf-confirmation.component.ts @@ -3,6 +3,7 @@ import { Component, Inject } from "@angular/core"; import { FormGroup, FormControl, Validators } from "@angular/forms"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; import { KdfRequest } from "@bitwarden/common/models/request/kdf.request"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -18,7 +19,6 @@ import { KdfType } from "@bitwarden/common/platform/enums"; templateUrl: "change-kdf-confirmation.component.html", }) export class ChangeKdfConfirmationComponent { - kdf: KdfType; kdfConfig: KdfConfig; form = new FormGroup({ @@ -37,9 +37,9 @@ export class ChangeKdfConfirmationComponent { private messagingService: MessagingService, private stateService: StateService, private logService: LogService, - @Inject(DIALOG_DATA) params: { kdf: KdfType; kdfConfig: KdfConfig }, + private kdfConfigService: KdfConfigService, + @Inject(DIALOG_DATA) params: { kdfConfig: KdfConfig }, ) { - this.kdf = params.kdf; this.kdfConfig = params.kdfConfig; this.masterPassword = null; } @@ -65,22 +65,24 @@ export class ChangeKdfConfirmationComponent { private async makeKeyAndSaveAsync() { const masterPassword = this.form.value.masterPassword; + + // Ensure the KDF config is valid. + this.kdfConfig.validateKdfConfig(); + const request = new KdfRequest(); - request.kdf = this.kdf; + request.kdf = this.kdfConfig.kdfType; request.kdfIterations = this.kdfConfig.iterations; - request.kdfMemory = this.kdfConfig.memory; - request.kdfParallelism = this.kdfConfig.parallelism; + if (this.kdfConfig.kdfType === KdfType.Argon2id) { + request.kdfMemory = this.kdfConfig.memory; + request.kdfParallelism = this.kdfConfig.parallelism; + } const masterKey = await this.cryptoService.getOrDeriveMasterKey(masterPassword); request.masterPasswordHash = await this.cryptoService.hashMasterKey(masterPassword, masterKey); const email = await this.stateService.getEmail(); - // Ensure the KDF config is valid. - this.cryptoService.validateKdfConfig(this.kdf, this.kdfConfig); - const newMasterKey = await this.cryptoService.makeMasterKey( masterPassword, email, - this.kdf, this.kdfConfig, ); request.newMasterPasswordHash = await this.cryptoService.hashMasterKey( diff --git a/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.html b/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.html index 9b16c446be..8b1dec8e13 100644 --- a/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.html +++ b/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.html @@ -19,14 +19,14 @@ <select id="kdf" name="Kdf" - [(ngModel)]="kdf" + [(ngModel)]="kdfConfig.kdfType" (ngModelChange)="onChangeKdf($event)" class="form-control mb-3" required > <option *ngFor="let o of kdfOptions" [ngValue]="o.value">{{ o.name }}</option> </select> - <ng-container *ngIf="kdf == kdfType.Argon2id"> + <ng-container *ngIf="isArgon2(kdfConfig)"> <label for="kdfMemory">{{ "kdfMemory" | i18n }}</label> <input id="kdfMemory" @@ -43,7 +43,7 @@ </div> <div class="col-6"> <div class="form-group mb-0"> - <ng-container *ngIf="kdf == kdfType.PBKDF2_SHA256"> + <ng-container *ngIf="isPBKDF2(kdfConfig)"> <label for="kdfIterations">{{ "kdfIterations" | i18n }}</label> <a class="ml-auto" @@ -65,7 +65,7 @@ required /> </ng-container> - <ng-container *ngIf="kdf == kdfType.Argon2id"> + <ng-container *ngIf="isArgon2(kdfConfig)"> <label for="kdfIterations">{{ "kdfIterations" | i18n }}</label> <input id="iterations" @@ -92,7 +92,7 @@ </div> </div> <div class="col-12"> - <ng-container *ngIf="kdf == kdfType.PBKDF2_SHA256"> + <ng-container *ngIf="isPBKDF2(kdfConfig)"> <p class="small form-text text-muted"> {{ "kdfIterationsDesc" | i18n: (PBKDF2_ITERATIONS.defaultValue | number) }} </p> @@ -100,7 +100,7 @@ {{ "kdfIterationsWarning" | i18n: (100000 | number) }} </bit-callout> </ng-container> - <ng-container *ngIf="kdf == kdfType.Argon2id"> + <ng-container *ngIf="isArgon2(kdfConfig)"> <p class="small form-text text-muted">{{ "argon2Desc" | i18n }}</p> <bit-callout type="warning"> {{ "argon2Warning" | i18n }}</bit-callout> </ng-container> diff --git a/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.ts b/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.ts index d91fb8d083..5c05f1ba2a 100644 --- a/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.ts +++ b/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.ts @@ -1,7 +1,11 @@ import { Component, OnInit } from "@angular/core"; -import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; +import { + Argon2KdfConfig, + KdfConfig, + PBKDF2KdfConfig, +} from "@bitwarden/common/auth/models/domain/kdf-config"; import { DEFAULT_KDF_CONFIG, PBKDF2_ITERATIONS, @@ -19,7 +23,6 @@ import { ChangeKdfConfirmationComponent } from "./change-kdf-confirmation.compon templateUrl: "change-kdf.component.html", }) export class ChangeKdfComponent implements OnInit { - kdf = KdfType.PBKDF2_SHA256; kdfConfig: KdfConfig = DEFAULT_KDF_CONFIG; kdfType = KdfType; kdfOptions: any[] = []; @@ -31,8 +34,8 @@ export class ChangeKdfComponent implements OnInit { protected ARGON2_PARALLELISM = ARGON2_PARALLELISM; constructor( - private stateService: StateService, private dialogService: DialogService, + private kdfConfigService: KdfConfigService, ) { this.kdfOptions = [ { name: "PBKDF2 SHA-256", value: KdfType.PBKDF2_SHA256 }, @@ -41,19 +44,22 @@ export class ChangeKdfComponent implements OnInit { } async ngOnInit() { - this.kdf = await this.stateService.getKdfType(); - this.kdfConfig = await this.stateService.getKdfConfig(); + this.kdfConfig = await this.kdfConfigService.getKdfConfig(); + } + + isPBKDF2(t: KdfConfig): t is PBKDF2KdfConfig { + return t instanceof PBKDF2KdfConfig; + } + + isArgon2(t: KdfConfig): t is Argon2KdfConfig { + return t instanceof Argon2KdfConfig; } async onChangeKdf(newValue: KdfType) { if (newValue === KdfType.PBKDF2_SHA256) { - this.kdfConfig = new KdfConfig(PBKDF2_ITERATIONS.defaultValue); + this.kdfConfig = new PBKDF2KdfConfig(); } else if (newValue === KdfType.Argon2id) { - this.kdfConfig = new KdfConfig( - ARGON2_ITERATIONS.defaultValue, - ARGON2_MEMORY.defaultValue, - ARGON2_PARALLELISM.defaultValue, - ); + this.kdfConfig = new Argon2KdfConfig(); } else { throw new Error("Unknown KDF type."); } @@ -62,7 +68,6 @@ export class ChangeKdfComponent implements OnInit { async openConfirmationModal() { this.dialogService.open(ChangeKdfConfirmationComponent, { data: { - kdf: this.kdf, kdfConfig: this.kdfConfig, }, }); diff --git a/apps/web/src/app/auth/update-password.component.ts b/apps/web/src/app/auth/update-password.component.ts index 2844d2d862..123e3e4ac1 100644 --- a/apps/web/src/app/auth/update-password.component.ts +++ b/apps/web/src/app/auth/update-password.component.ts @@ -4,6 +4,7 @@ import { Router } from "@angular/router"; import { UpdatePasswordComponent as BaseUpdatePasswordComponent } from "@bitwarden/angular/auth/components/update-password.component"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -32,6 +33,7 @@ export class UpdatePasswordComponent extends BaseUpdatePasswordComponent { stateService: StateService, userVerificationService: UserVerificationService, dialogService: DialogService, + kdfConfigService: KdfConfigService, ) { super( router, @@ -46,6 +48,7 @@ export class UpdatePasswordComponent extends BaseUpdatePasswordComponent { userVerificationService, logService, dialogService, + kdfConfigService, ); } } diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index 2c20328336..c97dd93d76 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -35,6 +35,7 @@ import { EventCollectionService } from "@bitwarden/common/abstractions/event/eve import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; @@ -184,6 +185,7 @@ export class VaultComponent implements OnInit, OnDestroy { private apiService: ApiService, private userVerificationService: UserVerificationService, private billingAccountProfileStateService: BillingAccountProfileStateService, + protected kdfConfigService: KdfConfigService, ) {} async ngOnInit() { @@ -972,10 +974,10 @@ export class VaultComponent implements OnInit, OnDestroy { } async isLowKdfIteration() { - const kdfType = await this.stateService.getKdfType(); - const kdfOptions = await this.stateService.getKdfConfig(); + const kdfConfig = await this.kdfConfigService.getKdfConfig(); return ( - kdfType === KdfType.PBKDF2_SHA256 && kdfOptions.iterations < PBKDF2_ITERATIONS.defaultValue + kdfConfig.kdfType === KdfType.PBKDF2_SHA256 && + kdfConfig.iterations < PBKDF2_ITERATIONS.defaultValue ); } diff --git a/libs/angular/src/auth/components/change-password.component.ts b/libs/angular/src/auth/components/change-password.component.ts index 1086428f4c..b1f75de58c 100644 --- a/libs/angular/src/auth/components/change-password.component.ts +++ b/libs/angular/src/auth/components/change-password.component.ts @@ -3,13 +3,13 @@ import { Subject, takeUntil } from "rxjs"; 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 { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; 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 { KdfType } from "@bitwarden/common/platform/enums"; 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"; @@ -31,7 +31,6 @@ export class ChangePasswordComponent implements OnInit, OnDestroy { minimumLength = Utils.minimumPasswordLength; protected email: string; - protected kdf: KdfType; protected kdfConfig: KdfConfig; protected destroy$ = new Subject<void>(); @@ -45,6 +44,7 @@ export class ChangePasswordComponent implements OnInit, OnDestroy { protected policyService: PolicyService, protected stateService: StateService, protected dialogService: DialogService, + protected kdfConfigService: KdfConfigService, ) {} async ngOnInit() { @@ -73,18 +73,14 @@ export class ChangePasswordComponent implements OnInit, OnDestroy { } const email = await this.stateService.getEmail(); - if (this.kdf == null) { - this.kdf = await this.stateService.getKdfType(); - } if (this.kdfConfig == null) { - this.kdfConfig = await this.stateService.getKdfConfig(); + this.kdfConfig = await this.kdfConfigService.getKdfConfig(); } // Create new master key const newMasterKey = await this.cryptoService.makeMasterKey( this.masterPassword, email.trim().toLowerCase(), - this.kdf, this.kdfConfig, ); const newMasterKeyHash = await this.cryptoService.hashMasterKey( diff --git a/libs/angular/src/auth/components/lock.component.ts b/libs/angular/src/auth/components/lock.component.ts index 927fbb27b1..89af31da81 100644 --- a/libs/angular/src/auth/components/lock.component.ts +++ b/libs/angular/src/auth/components/lock.component.ts @@ -12,6 +12,7 @@ import { InternalPolicyService } from "@bitwarden/common/admin-console/abstracti import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.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 { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; @@ -79,6 +80,7 @@ export class LockComponent implements OnInit, OnDestroy { protected pinCryptoService: PinCryptoServiceAbstraction, protected biometricStateService: BiometricStateService, protected accountService: AccountService, + protected kdfConfigService: KdfConfigService, ) {} async ngOnInit() { @@ -208,14 +210,12 @@ export class LockComponent implements OnInit, OnDestroy { } private async doUnlockWithMasterPassword() { + const kdfConfig = await this.kdfConfigService.getKdfConfig(); const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; - const kdf = await this.stateService.getKdfType(); - const kdfConfig = await this.stateService.getKdfConfig(); const masterKey = await this.cryptoService.makeMasterKey( this.masterPassword, this.email, - kdf, kdfConfig, ); const storedMasterKeyHash = await firstValueFrom( diff --git a/libs/angular/src/auth/components/register.component.ts b/libs/angular/src/auth/components/register.component.ts index 3cffebe71b..2ba7669290 100644 --- a/libs/angular/src/auth/components/register.component.ts +++ b/libs/angular/src/auth/components/register.component.ts @@ -15,7 +15,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic 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 { DEFAULT_KDF_CONFIG, DEFAULT_KDF_TYPE } from "@bitwarden/common/platform/enums"; +import { DEFAULT_KDF_CONFIG } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { DialogService } from "@bitwarden/components"; @@ -273,9 +273,8 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn name: string, ): Promise<RegisterRequest> { const hint = this.formGroup.value.hint; - const kdf = DEFAULT_KDF_TYPE; const kdfConfig = DEFAULT_KDF_CONFIG; - const key = await this.cryptoService.makeMasterKey(masterPassword, email, kdf, kdfConfig); + const key = await this.cryptoService.makeMasterKey(masterPassword, email, kdfConfig); const newUserKey = await this.cryptoService.makeUserKey(key); const masterKeyHash = await this.cryptoService.hashMasterKey(masterPassword, key); const keys = await this.cryptoService.makeKeyPair(newUserKey[0]); @@ -287,10 +286,8 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn newUserKey[1].encryptedString, this.referenceData, this.captchaToken, - kdf, + kdfConfig.kdfType, kdfConfig.iterations, - kdfConfig.memory, - kdfConfig.parallelism, ); request.keys = new KeysRequest(keys[0], keys[1].encryptedString); const orgInvite = await this.stateService.getOrganizationInvitation(); diff --git a/libs/angular/src/auth/components/set-password.component.ts b/libs/angular/src/auth/components/set-password.component.ts index eebf87655b..00a36434b0 100644 --- a/libs/angular/src/auth/components/set-password.component.ts +++ b/libs/angular/src/auth/components/set-password.component.ts @@ -13,6 +13,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli 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 { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.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"; @@ -23,11 +24,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic 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 { - HashPurpose, - DEFAULT_KDF_TYPE, - DEFAULT_KDF_CONFIG, -} from "@bitwarden/common/platform/enums"; +import { HashPurpose, DEFAULT_KDF_CONFIG } from "@bitwarden/common/platform/enums"; 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"; @@ -73,6 +70,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { private userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, private ssoLoginService: SsoLoginServiceAbstraction, dialogService: DialogService, + kdfConfigService: KdfConfigService, ) { super( i18nService, @@ -83,6 +81,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { policyService, stateService, dialogService, + kdfConfigService, ); } @@ -139,7 +138,6 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { } async setupSubmitActions() { - this.kdf = DEFAULT_KDF_TYPE; this.kdfConfig = DEFAULT_KDF_CONFIG; return true; } @@ -169,10 +167,8 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { this.hint, this.orgSsoIdentifier, keysRequest, - this.kdf, + this.kdfConfig.kdfType, //always PBKDF2 --> see this.setupSubmitActions this.kdfConfig.iterations, - this.kdfConfig.memory, - this.kdfConfig.parallelism, ); try { if (this.resetPasswordAutoEnroll) { @@ -246,9 +242,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { ); userDecryptionOpts.hasMasterPassword = true; await this.userDecryptionOptionsService.setUserDecryptionOptions(userDecryptionOpts); - - await this.stateService.setKdfType(this.kdf); - await this.stateService.setKdfConfig(this.kdfConfig); + await this.kdfConfigService.setKdfConfig(this.userId, this.kdfConfig); await this.masterPasswordService.setMasterKey(masterKey, this.userId); await this.cryptoService.setUserKey(userKey[0]); diff --git a/libs/angular/src/auth/components/set-pin.component.ts b/libs/angular/src/auth/components/set-pin.component.ts index ade23f4fef..f0b66b8e70 100644 --- a/libs/angular/src/auth/components/set-pin.component.ts +++ b/libs/angular/src/auth/components/set-pin.component.ts @@ -2,6 +2,7 @@ import { DialogRef } from "@angular/cdk/dialog"; import { Directive, OnInit } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; @@ -22,6 +23,7 @@ export class SetPinComponent implements OnInit { private userVerificationService: UserVerificationService, private stateService: StateService, private formBuilder: FormBuilder, + private kdfConfigService: KdfConfigService, ) {} async ngOnInit() { @@ -43,8 +45,7 @@ export class SetPinComponent implements OnInit { const pinKey = await this.cryptoService.makePinKey( pin, await this.stateService.getEmail(), - await this.stateService.getKdfType(), - await this.stateService.getKdfConfig(), + await this.kdfConfigService.getKdfConfig(), ); const userKey = await this.cryptoService.getUserKey(); const pinProtectedKey = await this.cryptoService.encrypt(userKey.key, pinKey); diff --git a/libs/angular/src/auth/components/update-password.component.ts b/libs/angular/src/auth/components/update-password.component.ts index 2ffffb6c5d..264f351542 100644 --- a/libs/angular/src/auth/components/update-password.component.ts +++ b/libs/angular/src/auth/components/update-password.component.ts @@ -4,6 +4,7 @@ import { Router } from "@angular/router"; 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 { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { VerificationType } from "@bitwarden/common/auth/enums/verification-type"; import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request"; @@ -44,6 +45,7 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent { private userVerificationService: UserVerificationService, private logService: LogService, dialogService: DialogService, + kdfConfigService: KdfConfigService, ) { super( i18nService, @@ -54,6 +56,7 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent { policyService, stateService, dialogService, + kdfConfigService, ); } @@ -90,8 +93,7 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent { return false; } - this.kdf = await this.stateService.getKdfType(); - this.kdfConfig = await this.stateService.getKdfConfig(); + this.kdfConfig = await this.kdfConfigService.getKdfConfig(); return true; } 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 54fdc83239..bd6da6b760 100644 --- a/libs/angular/src/auth/components/update-temp-password.component.ts +++ b/libs/angular/src/auth/components/update-temp-password.component.ts @@ -6,6 +6,7 @@ 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 { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.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"; @@ -59,6 +60,7 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent { private userVerificationService: UserVerificationService, protected router: Router, dialogService: DialogService, + kdfConfigService: KdfConfigService, private accountService: AccountService, private masterPasswordService: InternalMasterPasswordServiceAbstraction, ) { @@ -71,6 +73,7 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent { policyService, stateService, dialogService, + kdfConfigService, ); } @@ -104,8 +107,7 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent { async setupSubmitActions(): Promise<boolean> { this.email = await this.stateService.getEmail(); - this.kdf = await this.stateService.getKdfType(); - this.kdfConfig = await this.stateService.getKdfConfig(); + this.kdfConfig = await this.kdfConfigService.getKdfConfig(); return true; } @@ -124,7 +126,6 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent { const newMasterKey = await this.cryptoService.makeMasterKey( this.masterPassword, this.email.trim().toLowerCase(), - this.kdf, this.kdfConfig, ); const newPasswordHash = await this.cryptoService.hashMasterKey( diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 42879a8424..88494a1cbb 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -63,6 +63,7 @@ import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/aut import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; +import { KdfConfigService as KdfConfigServiceAbstraction } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { InternalMasterPasswordServiceAbstraction, @@ -85,6 +86,7 @@ import { AvatarService } from "@bitwarden/common/auth/services/avatar.service"; import { DeviceTrustService } from "@bitwarden/common/auth/services/device-trust.service.implementation"; import { DevicesServiceImplementation } from "@bitwarden/common/auth/services/devices/devices.service.implementation"; import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation"; +import { KdfConfigService } from "@bitwarden/common/auth/services/kdf-config.service"; import { KeyConnectorService } from "@bitwarden/common/auth/services/key-connector.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"; @@ -390,6 +392,7 @@ const safeProviders: SafeProvider[] = [ InternalUserDecryptionOptionsServiceAbstraction, GlobalStateProvider, BillingAccountProfileStateService, + KdfConfigServiceAbstraction, ], }), safeProvider({ @@ -543,6 +546,7 @@ const safeProviders: SafeProvider[] = [ StateServiceAbstraction, AccountServiceAbstraction, StateProvider, + KdfConfigServiceAbstraction, ], }), safeProvider({ @@ -713,7 +717,7 @@ const safeProviders: SafeProvider[] = [ CipherServiceAbstraction, CryptoServiceAbstraction, CryptoFunctionServiceAbstraction, - StateServiceAbstraction, + KdfConfigServiceAbstraction, ], }), safeProvider({ @@ -724,8 +728,8 @@ const safeProviders: SafeProvider[] = [ ApiServiceAbstraction, CryptoServiceAbstraction, CryptoFunctionServiceAbstraction, - StateServiceAbstraction, CollectionServiceAbstraction, + KdfConfigServiceAbstraction, ], }), safeProvider({ @@ -834,6 +838,7 @@ const safeProviders: SafeProvider[] = [ LogService, VaultTimeoutSettingsServiceAbstraction, PlatformUtilsServiceAbstraction, + KdfConfigServiceAbstraction, ], }), safeProvider({ @@ -985,6 +990,7 @@ const safeProviders: SafeProvider[] = [ CryptoServiceAbstraction, VaultTimeoutSettingsServiceAbstraction, LogService, + KdfConfigServiceAbstraction, ], }), safeProvider({ @@ -1150,6 +1156,11 @@ const safeProviders: SafeProvider[] = [ useClass: ProviderApiService, deps: [ApiServiceAbstraction], }), + safeProvider({ + provide: KdfConfigServiceAbstraction, + useClass: KdfConfigService, + deps: [StateProvider], + }), ]; function encryptServiceFactory( 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 4e0b1ac3ac..5e70c348f4 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 @@ -2,6 +2,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; 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"; @@ -44,6 +45,7 @@ describe("AuthRequestLoginStrategy", () => { let userDecryptionOptions: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>; let deviceTrustService: MockProxy<DeviceTrustServiceAbstraction>; let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>; + let kdfConfigService: MockProxy<KdfConfigService>; const mockUserId = Utils.newGuid() as UserId; let accountService: FakeAccountService; @@ -77,6 +79,7 @@ describe("AuthRequestLoginStrategy", () => { userDecryptionOptions = mock<InternalUserDecryptionOptionsServiceAbstraction>(); deviceTrustService = mock<DeviceTrustServiceAbstraction>(); billingAccountProfileStateService = mock<BillingAccountProfileStateService>(); + kdfConfigService = mock<KdfConfigService>(); accountService = mockAccountServiceWith(mockUserId); masterPasswordService = new FakeMasterPasswordService(); @@ -101,6 +104,7 @@ describe("AuthRequestLoginStrategy", () => { userDecryptionOptions, deviceTrustService, billingAccountProfileStateService, + kdfConfigService, ); tokenResponse = identityTokenResponseFactory(); 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 5220e432de..a66d987984 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 @@ -3,6 +3,7 @@ import { Jsonify } from "type-fest"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.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"; @@ -63,6 +64,7 @@ export class AuthRequestLoginStrategy extends LoginStrategy { userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, private deviceTrustService: DeviceTrustServiceAbstraction, billingAccountProfileStateService: BillingAccountProfileStateService, + kdfConfigService: KdfConfigService, ) { super( accountService, @@ -78,6 +80,7 @@ export class AuthRequestLoginStrategy extends LoginStrategy { twoFactorService, userDecryptionOptionsService, billingAccountProfileStateService, + kdfConfigService, ); this.cache = new BehaviorSubject(data); 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 e0833342ce..7c022db23b 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.spec.ts @@ -2,6 +2,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; 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"; @@ -117,6 +118,7 @@ describe("LoginStrategy", () => { let policyService: MockProxy<PolicyService>; let passwordStrengthService: MockProxy<PasswordStrengthServiceAbstraction>; let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>; + let kdfConfigService: MockProxy<KdfConfigService>; let passwordLoginStrategy: PasswordLoginStrategy; let credentials: PasswordLoginCredentials; @@ -136,6 +138,7 @@ describe("LoginStrategy", () => { stateService = mock<StateService>(); twoFactorService = mock<TwoFactorService>(); userDecryptionOptionsService = mock<InternalUserDecryptionOptionsServiceAbstraction>(); + kdfConfigService = mock<KdfConfigService>(); policyService = mock<PolicyService>(); passwordStrengthService = mock<PasswordStrengthService>(); billingAccountProfileStateService = mock<BillingAccountProfileStateService>(); @@ -162,6 +165,7 @@ describe("LoginStrategy", () => { policyService, loginStrategyService, billingAccountProfileStateService, + kdfConfigService, ); credentials = new PasswordLoginCredentials(email, masterPassword); }); @@ -208,8 +212,6 @@ describe("LoginStrategy", () => { userId: userId, name: name, email: email, - kdfIterations: kdfIterations, - kdfType: kdf, }, }, keys: new AccountKeys(), @@ -404,6 +406,7 @@ describe("LoginStrategy", () => { policyService, loginStrategyService, billingAccountProfileStateService, + kdfConfigService, ); apiService.postIdentityToken.mockResolvedValue(identityTokenResponseFactory()); diff --git a/libs/auth/src/common/login-strategies/login.strategy.ts b/libs/auth/src/common/login-strategies/login.strategy.ts index a73c32e120..06fc98db13 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.ts @@ -2,12 +2,14 @@ import { BehaviorSubject } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.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"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; +import { Argon2KdfConfig, PBKDF2KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; import { DeviceRequest } from "@bitwarden/common/auth/models/request/identity-token/device.request"; import { PasswordTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/password-token.request"; import { SsoTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/sso-token.request"; @@ -27,6 +29,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 { KdfType } from "@bitwarden/common/platform/enums"; import { Account, AccountProfile } from "@bitwarden/common/platform/models/domain/account"; import { UserId } from "@bitwarden/common/types/guid"; @@ -72,6 +75,7 @@ export abstract class LoginStrategy { protected twoFactorService: TwoFactorService, protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, protected billingAccountProfileStateService: BillingAccountProfileStateService, + protected KdfConfigService: KdfConfigService, ) {} abstract exportCache(): CacheData; @@ -182,10 +186,6 @@ export abstract class LoginStrategy { userId, name: accountInformation.name, email: accountInformation.email, - kdfIterations: tokenResponse.kdfIterations, - kdfMemory: tokenResponse.kdfMemory, - kdfParallelism: tokenResponse.kdfParallelism, - kdfType: tokenResponse.kdf, }, }, }), @@ -195,6 +195,17 @@ export abstract class LoginStrategy { UserDecryptionOptions.fromResponse(tokenResponse), ); + await this.KdfConfigService.setKdfConfig( + userId as UserId, + tokenResponse.kdf === KdfType.PBKDF2_SHA256 + ? new PBKDF2KdfConfig(tokenResponse.kdfIterations) + : new Argon2KdfConfig( + tokenResponse.kdfIterations, + tokenResponse.kdfMemory, + tokenResponse.kdfParallelism, + ), + ); + await this.billingAccountProfileStateService.setHasPremium(accountInformation.premium, false); return userId as UserId; } 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 b902fff574..be09448fdd 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 @@ -2,6 +2,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; 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"; @@ -71,6 +72,7 @@ describe("PasswordLoginStrategy", () => { let policyService: MockProxy<PolicyService>; let passwordStrengthService: MockProxy<PasswordStrengthServiceAbstraction>; let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>; + let kdfConfigService: MockProxy<KdfConfigService>; let passwordLoginStrategy: PasswordLoginStrategy; let credentials: PasswordLoginCredentials; @@ -94,6 +96,7 @@ describe("PasswordLoginStrategy", () => { policyService = mock<PolicyService>(); passwordStrengthService = mock<PasswordStrengthService>(); billingAccountProfileStateService = mock<BillingAccountProfileStateService>(); + kdfConfigService = mock<KdfConfigService>(); appIdService.getAppId.mockResolvedValue(deviceId); tokenService.decodeAccessToken.mockResolvedValue({}); @@ -127,6 +130,7 @@ describe("PasswordLoginStrategy", () => { policyService, loginStrategyService, billingAccountProfileStateService, + kdfConfigService, ); credentials = new PasswordLoginCredentials(email, masterPassword); tokenResponse = identityTokenResponseFactory(masterPasswordPolicy); 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 2490c35a00..d3ce8fa9e8 100644 --- a/libs/auth/src/common/login-strategies/password-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/password-login.strategy.ts @@ -5,6 +5,7 @@ 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 { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.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"; @@ -89,6 +90,7 @@ export class PasswordLoginStrategy extends LoginStrategy { private policyService: PolicyService, private loginStrategyService: LoginStrategyServiceAbstraction, billingAccountProfileStateService: BillingAccountProfileStateService, + kdfConfigService: KdfConfigService, ) { super( accountService, @@ -104,6 +106,7 @@ export class PasswordLoginStrategy extends LoginStrategy { twoFactorService, userDecryptionOptionsService, billingAccountProfileStateService, + kdfConfigService, ); this.cache = new BehaviorSubject(data); 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 df33415247..3439a1c199 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 @@ -2,6 +2,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.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"; @@ -54,6 +55,7 @@ describe("SsoLoginStrategy", () => { let authRequestService: MockProxy<AuthRequestServiceAbstraction>; let i18nService: MockProxy<I18nService>; let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>; + let kdfConfigService: MockProxy<KdfConfigService>; let ssoLoginStrategy: SsoLoginStrategy; let credentials: SsoLoginCredentials; @@ -86,6 +88,7 @@ describe("SsoLoginStrategy", () => { authRequestService = mock<AuthRequestServiceAbstraction>(); i18nService = mock<I18nService>(); billingAccountProfileStateService = mock<BillingAccountProfileStateService>(); + kdfConfigService = mock<KdfConfigService>(); tokenService.getTwoFactorToken.mockResolvedValue(null); appIdService.getAppId.mockResolvedValue(deviceId); @@ -110,6 +113,7 @@ describe("SsoLoginStrategy", () => { authRequestService, i18nService, billingAccountProfileStateService, + kdfConfigService, ); credentials = new SsoLoginCredentials(ssoCode, ssoCodeVerifier, ssoRedirectUrl, ssoOrgId); }); 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 dc63f0fae1..c7cd9052f8 100644 --- a/libs/auth/src/common/login-strategies/sso-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/sso-login.strategy.ts @@ -3,6 +3,7 @@ import { Jsonify } from "type-fest"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.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"; @@ -98,6 +99,7 @@ export class SsoLoginStrategy extends LoginStrategy { private authRequestService: AuthRequestServiceAbstraction, private i18nService: I18nService, billingAccountProfileStateService: BillingAccountProfileStateService, + kdfConfigService: KdfConfigService, ) { super( accountService, @@ -113,6 +115,7 @@ export class SsoLoginStrategy extends LoginStrategy { twoFactorService, userDecryptionOptionsService, billingAccountProfileStateService, + kdfConfigService, ); this.cache = new BehaviorSubject(data); 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 5e7d7985b1..5fce8b0b82 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 @@ -2,6 +2,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.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"; @@ -49,6 +50,7 @@ describe("UserApiLoginStrategy", () => { let keyConnectorService: MockProxy<KeyConnectorService>; let environmentService: MockProxy<EnvironmentService>; let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>; + let kdfConfigService: MockProxy<KdfConfigService>; let apiLogInStrategy: UserApiLoginStrategy; let credentials: UserApiLoginCredentials; @@ -76,6 +78,7 @@ describe("UserApiLoginStrategy", () => { keyConnectorService = mock<KeyConnectorService>(); environmentService = mock<EnvironmentService>(); billingAccountProfileStateService = mock<BillingAccountProfileStateService>(); + kdfConfigService = mock<KdfConfigService>(); appIdService.getAppId.mockResolvedValue(deviceId); tokenService.getTwoFactorToken.mockResolvedValue(null); @@ -98,6 +101,7 @@ describe("UserApiLoginStrategy", () => { environmentService, keyConnectorService, billingAccountProfileStateService, + kdfConfigService, ); credentials = new UserApiLoginCredentials(apiClientId, apiClientSecret); 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 4a0d005b1c..d7ee6fdc4b 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 @@ -3,6 +3,7 @@ import { Jsonify } from "type-fest"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.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"; @@ -57,6 +58,7 @@ export class UserApiLoginStrategy extends LoginStrategy { private environmentService: EnvironmentService, private keyConnectorService: KeyConnectorService, billingAccountProfileStateService: BillingAccountProfileStateService, + protected kdfConfigService: KdfConfigService, ) { super( accountService, @@ -72,6 +74,7 @@ export class UserApiLoginStrategy extends LoginStrategy { twoFactorService, userDecryptionOptionsService, billingAccountProfileStateService, + kdfConfigService, ); this.cache = new BehaviorSubject(data); } 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 1d96921286..d75e194980 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 @@ -1,6 +1,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; 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"; @@ -42,6 +43,7 @@ describe("WebAuthnLoginStrategy", () => { let twoFactorService!: MockProxy<TwoFactorService>; let userDecryptionOptionsService: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>; let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>; + let kdfConfigService: MockProxy<KdfConfigService>; let webAuthnLoginStrategy!: WebAuthnLoginStrategy; @@ -81,6 +83,7 @@ describe("WebAuthnLoginStrategy", () => { twoFactorService = mock<TwoFactorService>(); userDecryptionOptionsService = mock<InternalUserDecryptionOptionsServiceAbstraction>(); billingAccountProfileStateService = mock<BillingAccountProfileStateService>(); + kdfConfigService = mock<KdfConfigService>(); tokenService.getTwoFactorToken.mockResolvedValue(null); appIdService.getAppId.mockResolvedValue(deviceId); @@ -101,6 +104,7 @@ describe("WebAuthnLoginStrategy", () => { twoFactorService, userDecryptionOptionsService, billingAccountProfileStateService, + kdfConfigService, ); // Create credentials 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 8a62a8fb3c..ac487b3a82 100644 --- a/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts @@ -3,6 +3,7 @@ import { Jsonify } from "type-fest"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.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"; @@ -57,6 +58,7 @@ export class WebAuthnLoginStrategy extends LoginStrategy { twoFactorService: TwoFactorService, userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, billingAccountProfileStateService: BillingAccountProfileStateService, + kdfConfigService: KdfConfigService, ) { super( accountService, @@ -72,6 +74,7 @@ export class WebAuthnLoginStrategy extends LoginStrategy { twoFactorService, userDecryptionOptionsService, billingAccountProfileStateService, + kdfConfigService, ); this.cache = new BehaviorSubject(data); 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 33708885e2..f1b5590404 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 @@ -3,6 +3,7 @@ import { MockProxy, mock } from "jest-mock-extended"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.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"; @@ -66,6 +67,7 @@ describe("LoginStrategyService", () => { let authRequestService: MockProxy<AuthRequestServiceAbstraction>; let userDecryptionOptionsService: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>; let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>; + let kdfConfigService: MockProxy<KdfConfigService>; let stateProvider: FakeGlobalStateProvider; let loginStrategyCacheExpirationState: FakeGlobalState<Date | null>; @@ -95,6 +97,7 @@ describe("LoginStrategyService", () => { userDecryptionOptionsService = mock<UserDecryptionOptionsService>(); billingAccountProfileStateService = mock<BillingAccountProfileStateService>(); stateProvider = new FakeGlobalStateProvider(); + kdfConfigService = mock<KdfConfigService>(); sut = new LoginStrategyService( accountService, @@ -119,6 +122,7 @@ describe("LoginStrategyService", () => { userDecryptionOptionsService, stateProvider, billingAccountProfileStateService, + kdfConfigService, ); loginStrategyCacheExpirationState = stateProvider.getFake(CACHE_EXPIRATION_KEY); 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 aee74e6607..13cca69b3a 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 @@ -10,13 +10,18 @@ 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 { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.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 { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; -import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; +import { + Argon2KdfConfig, + KdfConfig, + PBKDF2KdfConfig, +} from "@bitwarden/common/auth/models/domain/kdf-config"; import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request"; import { PasswordlessAuthRequest } from "@bitwarden/common/auth/models/request/passwordless-auth.request"; import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; @@ -32,7 +37,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 { KdfType } from "@bitwarden/common/platform/enums"; +import { KdfType } from "@bitwarden/common/platform/enums/kdf-type.enum"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { GlobalState, GlobalStateProvider } from "@bitwarden/common/platform/state"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/src/auth/abstractions/device-trust.service.abstraction"; @@ -105,6 +110,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, protected stateProvider: GlobalStateProvider, protected billingAccountProfileStateService: BillingAccountProfileStateService, + protected kdfConfigService: KdfConfigService, ) { this.currentAuthnTypeState = this.stateProvider.get(CURRENT_LOGIN_STRATEGY_KEY); this.loginStrategyCacheState = this.stateProvider.get(CACHE_KEY); @@ -233,24 +239,25 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { async makePreloginKey(masterPassword: string, email: string): Promise<MasterKey> { email = email.trim().toLowerCase(); - let kdf: KdfType = null; let kdfConfig: KdfConfig = null; try { const preloginResponse = await this.apiService.postPrelogin(new PreloginRequest(email)); if (preloginResponse != null) { - kdf = preloginResponse.kdf; - kdfConfig = new KdfConfig( - preloginResponse.kdfIterations, - preloginResponse.kdfMemory, - preloginResponse.kdfParallelism, - ); + kdfConfig = + preloginResponse.kdf === KdfType.PBKDF2_SHA256 + ? new PBKDF2KdfConfig(preloginResponse.kdfIterations) + : new Argon2KdfConfig( + preloginResponse.kdfIterations, + preloginResponse.kdfMemory, + preloginResponse.kdfParallelism, + ); } } catch (e) { if (e == null || e.statusCode !== 404) { throw e; } } - return await this.cryptoService.makeMasterKey(masterPassword, email, kdf, kdfConfig); + return await this.cryptoService.makeMasterKey(masterPassword, email, kdfConfig); } // TODO: move to auth request service @@ -354,6 +361,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.policyService, this, this.billingAccountProfileStateService, + this.kdfConfigService, ); case AuthenticationType.Sso: return new SsoLoginStrategy( @@ -375,6 +383,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.authRequestService, this.i18nService, this.billingAccountProfileStateService, + this.kdfConfigService, ); case AuthenticationType.UserApiKey: return new UserApiLoginStrategy( @@ -394,6 +403,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.environmentService, this.keyConnectorService, this.billingAccountProfileStateService, + this.kdfConfigService, ); case AuthenticationType.AuthRequest: return new AuthRequestLoginStrategy( @@ -412,6 +422,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.userDecryptionOptionsService, this.deviceTrustService, this.billingAccountProfileStateService, + this.kdfConfigService, ); case AuthenticationType.WebAuthn: return new WebAuthnLoginStrategy( @@ -429,6 +440,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.twoFactorService, this.userDecryptionOptionsService, this.billingAccountProfileStateService, + this.kdfConfigService, ); } }), diff --git a/libs/auth/src/common/services/pin-crypto/pin-crypto.service.implementation.ts b/libs/auth/src/common/services/pin-crypto/pin-crypto.service.implementation.ts index 149d5d9a53..85d36b8d73 100644 --- a/libs/auth/src/common/services/pin-crypto/pin-crypto.service.implementation.ts +++ b/libs/auth/src/common/services/pin-crypto/pin-crypto.service.implementation.ts @@ -1,9 +1,9 @@ import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { KdfType } from "@bitwarden/common/platform/enums"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { PinLockType } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service"; import { UserKey } from "@bitwarden/common/types/key"; @@ -16,6 +16,7 @@ export class PinCryptoService implements PinCryptoServiceAbstraction { private cryptoService: CryptoService, private vaultTimeoutSettingsService: VaultTimeoutSettingsService, private logService: LogService, + private kdfConfigService: KdfConfigService, ) {} async decryptUserKeyWithPin(pin: string): Promise<UserKey | null> { try { @@ -24,8 +25,7 @@ export class PinCryptoService implements PinCryptoServiceAbstraction { const { pinKeyEncryptedUserKey, oldPinKeyEncryptedMasterKey } = await this.getPinKeyEncryptedKeys(pinLockType); - const kdf: KdfType = await this.stateService.getKdfType(); - const kdfConfig: KdfConfig = await this.stateService.getKdfConfig(); + const kdfConfig: KdfConfig = await this.kdfConfigService.getKdfConfig(); let userKey: UserKey; const email = await this.stateService.getEmail(); if (oldPinKeyEncryptedMasterKey) { @@ -33,7 +33,6 @@ export class PinCryptoService implements PinCryptoServiceAbstraction { pinLockType === "TRANSIENT", pin, email, - kdf, kdfConfig, oldPinKeyEncryptedMasterKey, ); @@ -41,7 +40,6 @@ export class PinCryptoService implements PinCryptoServiceAbstraction { userKey = await this.cryptoService.decryptUserKeyWithPin( pin, email, - kdf, kdfConfig, pinKeyEncryptedUserKey, ); diff --git a/libs/auth/src/common/services/pin-crypto/pin-crypto.service.spec.ts b/libs/auth/src/common/services/pin-crypto/pin-crypto.service.spec.ts index 17e0e14c51..c6fddf8efb 100644 --- a/libs/auth/src/common/services/pin-crypto/pin-crypto.service.spec.ts +++ b/libs/auth/src/common/services/pin-crypto/pin-crypto.service.spec.ts @@ -1,9 +1,10 @@ import { mock } from "jest-mock-extended"; -import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { DEFAULT_KDF_CONFIG } from "@bitwarden/common/platform/enums"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { @@ -13,6 +14,7 @@ import { import { UserKey } from "@bitwarden/common/types/key"; import { PinCryptoService } from "./pin-crypto.service.implementation"; + describe("PinCryptoService", () => { let pinCryptoService: PinCryptoService; @@ -20,6 +22,7 @@ describe("PinCryptoService", () => { const cryptoService = mock<CryptoService>(); const vaultTimeoutSettingsService = mock<VaultTimeoutSettingsService>(); const logService = mock<LogService>(); + const kdfConfigService = mock<KdfConfigService>(); beforeEach(() => { jest.clearAllMocks(); @@ -29,6 +32,7 @@ describe("PinCryptoService", () => { cryptoService, vaultTimeoutSettingsService, logService, + kdfConfigService, ); }); @@ -39,7 +43,6 @@ describe("PinCryptoService", () => { describe("decryptUserKeyWithPin(...)", () => { const mockPin = "1234"; const mockProtectedPin = "protectedPin"; - const DEFAULT_PBKDF2_ITERATIONS = 600000; const mockUserEmail = "user@example.com"; const mockUserKey = new SymmetricCryptoKey(randomBytes(32)) as UserKey; @@ -49,7 +52,7 @@ describe("PinCryptoService", () => { ) { vaultTimeoutSettingsService.isPinLockSet.mockResolvedValue(pinLockType); - stateService.getKdfConfig.mockResolvedValue(new KdfConfig(DEFAULT_PBKDF2_ITERATIONS)); + kdfConfigService.getKdfConfig.mockResolvedValue(DEFAULT_KDF_CONFIG); stateService.getEmail.mockResolvedValue(mockUserEmail); if (migrationStatus === "PRE") { diff --git a/libs/common/src/auth/abstractions/kdf-config.service.ts b/libs/common/src/auth/abstractions/kdf-config.service.ts new file mode 100644 index 0000000000..6b41979e1b --- /dev/null +++ b/libs/common/src/auth/abstractions/kdf-config.service.ts @@ -0,0 +1,7 @@ +import { UserId } from "../../types/guid"; +import { KdfConfig } from "../models/domain/kdf-config"; + +export abstract class KdfConfigService { + setKdfConfig: (userId: UserId, KdfConfig: KdfConfig) => Promise<void>; + getKdfConfig: () => Promise<KdfConfig>; +} diff --git a/libs/common/src/auth/models/domain/kdf-config.ts b/libs/common/src/auth/models/domain/kdf-config.ts index a25ba586e9..ce01f09702 100644 --- a/libs/common/src/auth/models/domain/kdf-config.ts +++ b/libs/common/src/auth/models/domain/kdf-config.ts @@ -1,11 +1,86 @@ -export class KdfConfig { - iterations: number; - memory?: number; - parallelism?: number; +import { Jsonify } from "type-fest"; - constructor(iterations: number, memory?: number, parallelism?: number) { - this.iterations = iterations; - this.memory = memory; - this.parallelism = parallelism; +import { + ARGON2_ITERATIONS, + ARGON2_MEMORY, + ARGON2_PARALLELISM, + KdfType, + PBKDF2_ITERATIONS, +} from "../../../platform/enums/kdf-type.enum"; + +/** + * Represents a type safe KDF configuration. + */ +export type KdfConfig = PBKDF2KdfConfig | Argon2KdfConfig; + +/** + * Password-Based Key Derivation Function 2 (PBKDF2) KDF configuration. + */ +export class PBKDF2KdfConfig { + kdfType: KdfType.PBKDF2_SHA256 = KdfType.PBKDF2_SHA256; + iterations: number; + + constructor(iterations?: number) { + this.iterations = iterations ?? PBKDF2_ITERATIONS.defaultValue; + } + + /** + * Validates the PBKDF2 KDF configuration. + * A Valid PBKDF2 KDF configuration has KDF iterations between the 600_000 and 2_000_000. + */ + validateKdfConfig(): void { + if (!PBKDF2_ITERATIONS.inRange(this.iterations)) { + throw new Error( + `PBKDF2 iterations must be between ${PBKDF2_ITERATIONS.min} and ${PBKDF2_ITERATIONS.max}`, + ); + } + } + + static fromJSON(json: Jsonify<PBKDF2KdfConfig>): PBKDF2KdfConfig { + return new PBKDF2KdfConfig(json.iterations); + } +} + +/** + * Argon2 KDF configuration. + */ +export class Argon2KdfConfig { + kdfType: KdfType.Argon2id = KdfType.Argon2id; + iterations: number; + memory: number; + parallelism: number; + + constructor(iterations?: number, memory?: number, parallelism?: number) { + this.iterations = iterations ?? ARGON2_ITERATIONS.defaultValue; + this.memory = memory ?? ARGON2_MEMORY.defaultValue; + this.parallelism = parallelism ?? ARGON2_PARALLELISM.defaultValue; + } + + /** + * Validates the Argon2 KDF configuration. + * A Valid Argon2 KDF configuration has iterations between 2 and 10, memory between 16mb and 1024mb, and parallelism between 1 and 16. + */ + validateKdfConfig(): void { + if (!ARGON2_ITERATIONS.inRange(this.iterations)) { + throw new Error( + `Argon2 iterations must be between ${ARGON2_ITERATIONS.min} and ${ARGON2_ITERATIONS.max}`, + ); + } + + if (!ARGON2_MEMORY.inRange(this.memory)) { + throw new Error( + `Argon2 memory must be between ${ARGON2_MEMORY.min}mb and ${ARGON2_MEMORY.max}mb`, + ); + } + + if (!ARGON2_PARALLELISM.inRange(this.parallelism)) { + throw new Error( + `Argon2 parallelism must be between ${ARGON2_PARALLELISM.min} and ${ARGON2_PARALLELISM.max}.`, + ); + } + } + + static fromJSON(json: Jsonify<Argon2KdfConfig>): Argon2KdfConfig { + return new Argon2KdfConfig(json.iterations, json.memory, json.parallelism); } } diff --git a/libs/common/src/auth/models/request/set-key-connector-key.request.ts b/libs/common/src/auth/models/request/set-key-connector-key.request.ts index dfd32689d8..c8081bdec2 100644 --- a/libs/common/src/auth/models/request/set-key-connector-key.request.ts +++ b/libs/common/src/auth/models/request/set-key-connector-key.request.ts @@ -11,18 +11,14 @@ export class SetKeyConnectorKeyRequest { kdfParallelism?: number; orgIdentifier: string; - constructor( - key: string, - kdf: KdfType, - kdfConfig: KdfConfig, - orgIdentifier: string, - keys: KeysRequest, - ) { + constructor(key: string, kdfConfig: KdfConfig, orgIdentifier: string, keys: KeysRequest) { this.key = key; - this.kdf = kdf; + this.kdf = kdfConfig.kdfType; this.kdfIterations = kdfConfig.iterations; - this.kdfMemory = kdfConfig.memory; - this.kdfParallelism = kdfConfig.parallelism; + if (kdfConfig.kdfType === KdfType.Argon2id) { + this.kdfMemory = kdfConfig.memory; + this.kdfParallelism = kdfConfig.parallelism; + } this.orgIdentifier = orgIdentifier; this.keys = keys; } diff --git a/libs/common/src/auth/services/kdf-config.service.spec.ts b/libs/common/src/auth/services/kdf-config.service.spec.ts new file mode 100644 index 0000000000..67bcf721bc --- /dev/null +++ b/libs/common/src/auth/services/kdf-config.service.spec.ts @@ -0,0 +1,104 @@ +import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from "../../../spec"; +import { + ARGON2_ITERATIONS, + ARGON2_MEMORY, + ARGON2_PARALLELISM, + PBKDF2_ITERATIONS, +} from "../../platform/enums/kdf-type.enum"; +import { Utils } from "../../platform/misc/utils"; +import { UserId } from "../../types/guid"; +import { Argon2KdfConfig, PBKDF2KdfConfig } from "../models/domain/kdf-config"; + +import { KdfConfigService } from "./kdf-config.service"; + +describe("KdfConfigService", () => { + let sutKdfConfigService: KdfConfigService; + + let fakeStateProvider: FakeStateProvider; + let fakeAccountService: FakeAccountService; + const mockUserId = Utils.newGuid() as UserId; + + beforeEach(() => { + jest.clearAllMocks(); + + fakeAccountService = mockAccountServiceWith(mockUserId); + fakeStateProvider = new FakeStateProvider(fakeAccountService); + sutKdfConfigService = new KdfConfigService(fakeStateProvider); + }); + + it("setKdfConfig(): should set the KDF config", async () => { + const kdfConfig: PBKDF2KdfConfig = new PBKDF2KdfConfig(600_000); + await sutKdfConfigService.setKdfConfig(mockUserId, kdfConfig); + await expect(sutKdfConfigService.getKdfConfig()).resolves.toEqual(kdfConfig); + }); + + it("setKdfConfig(): should get the KDF config", async () => { + const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig(3, 64, 4); + await sutKdfConfigService.setKdfConfig(mockUserId, kdfConfig); + await expect(sutKdfConfigService.getKdfConfig()).resolves.toEqual(kdfConfig); + }); + + it("setKdfConfig(): should throw error KDF cannot be null", async () => { + const kdfConfig: Argon2KdfConfig = null; + try { + await sutKdfConfigService.setKdfConfig(mockUserId, kdfConfig); + } catch (e) { + expect(e).toEqual(new Error("kdfConfig cannot be null")); + } + }); + + it("setKdfConfig(): should throw error userId cannot be null", async () => { + const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig(3, 64, 4); + try { + await sutKdfConfigService.setKdfConfig(null, kdfConfig); + } catch (e) { + expect(e).toEqual(new Error("userId cannot be null")); + } + }); + + it("getKdfConfig(): should throw error KdfConfig for active user account state is null", async () => { + try { + await sutKdfConfigService.getKdfConfig(); + } catch (e) { + expect(e).toEqual(new Error("KdfConfig for active user account state is null")); + } + }); + + it("validateKdfConfig(): should validate the PBKDF2 KDF config", () => { + const kdfConfig: PBKDF2KdfConfig = new PBKDF2KdfConfig(600_000); + expect(() => kdfConfig.validateKdfConfig()).not.toThrow(); + }); + + it("validateKdfConfig(): should validate the Argon2id KDF config", () => { + const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig(3, 64, 4); + expect(() => kdfConfig.validateKdfConfig()).not.toThrow(); + }); + + it("validateKdfConfig(): should throw an error for invalid PBKDF2 iterations", () => { + const kdfConfig: PBKDF2KdfConfig = new PBKDF2KdfConfig(100); + expect(() => kdfConfig.validateKdfConfig()).toThrow( + `PBKDF2 iterations must be between ${PBKDF2_ITERATIONS.min} and ${PBKDF2_ITERATIONS.max}`, + ); + }); + + it("validateKdfConfig(): should throw an error for invalid Argon2 iterations", () => { + const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig(11, 64, 4); + expect(() => kdfConfig.validateKdfConfig()).toThrow( + `Argon2 iterations must be between ${ARGON2_ITERATIONS.min} and ${ARGON2_ITERATIONS.max}`, + ); + }); + + it("validateKdfConfig(): should throw an error for invalid Argon2 memory", () => { + const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig(3, 1025, 4); + expect(() => kdfConfig.validateKdfConfig()).toThrow( + `Argon2 memory must be between ${ARGON2_MEMORY.min}mb and ${ARGON2_MEMORY.max}mb`, + ); + }); + + it("validateKdfConfig(): should throw an error for invalid Argon2 parallelism", () => { + const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig(3, 64, 17); + expect(() => kdfConfig.validateKdfConfig()).toThrow( + `Argon2 parallelism must be between ${ARGON2_PARALLELISM.min} and ${ARGON2_PARALLELISM.max}`, + ); + }); +}); diff --git a/libs/common/src/auth/services/kdf-config.service.ts b/libs/common/src/auth/services/kdf-config.service.ts new file mode 100644 index 0000000000..cfd2a3e1de --- /dev/null +++ b/libs/common/src/auth/services/kdf-config.service.ts @@ -0,0 +1,41 @@ +import { firstValueFrom } from "rxjs"; + +import { KdfType } from "../../platform/enums/kdf-type.enum"; +import { KDF_CONFIG_DISK, StateProvider, UserKeyDefinition } from "../../platform/state"; +import { UserId } from "../../types/guid"; +import { KdfConfigService as KdfConfigServiceAbstraction } from "../abstractions/kdf-config.service"; +import { Argon2KdfConfig, KdfConfig, PBKDF2KdfConfig } from "../models/domain/kdf-config"; + +export const KDF_CONFIG = new UserKeyDefinition<KdfConfig>(KDF_CONFIG_DISK, "kdfConfig", { + deserializer: (kdfConfig: KdfConfig) => { + if (kdfConfig == null) { + return null; + } + return kdfConfig.kdfType === KdfType.PBKDF2_SHA256 + ? PBKDF2KdfConfig.fromJSON(kdfConfig) + : Argon2KdfConfig.fromJSON(kdfConfig); + }, + clearOn: ["logout"], +}); + +export class KdfConfigService implements KdfConfigServiceAbstraction { + constructor(private stateProvider: StateProvider) {} + async setKdfConfig(userId: UserId, kdfConfig: KdfConfig) { + if (!userId) { + throw new Error("userId cannot be null"); + } + if (kdfConfig === null) { + throw new Error("kdfConfig cannot be null"); + } + await this.stateProvider.setUserState(KDF_CONFIG, kdfConfig, userId); + } + + async getKdfConfig(): Promise<KdfConfig> { + const userId = await firstValueFrom(this.stateProvider.activeUserId$); + const state = await firstValueFrom(this.stateProvider.getUser(userId, KDF_CONFIG).state$); + if (state === null) { + throw new Error("KdfConfig for active user account state is null"); + } + return state; + } +} diff --git a/libs/common/src/auth/services/key-connector.service.ts b/libs/common/src/auth/services/key-connector.service.ts index f8e523cce4..c19185ae91 100644 --- a/libs/common/src/auth/services/key-connector.service.ts +++ b/libs/common/src/auth/services/key-connector.service.ts @@ -7,6 +7,7 @@ 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 { KdfType } from "../../platform/enums/kdf-type.enum"; import { Utils } from "../../platform/misc/utils"; import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; import { @@ -20,7 +21,7 @@ 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 { Argon2KdfConfig, KdfConfig, PBKDF2KdfConfig } 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"; @@ -133,12 +134,14 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { userDecryptionOptions, } = tokenResponse; const password = await this.keyGenerationService.createKey(512); - const kdfConfig = new KdfConfig(kdfIterations, kdfMemory, kdfParallelism); + const kdfConfig: KdfConfig = + kdf === KdfType.PBKDF2_SHA256 + ? new PBKDF2KdfConfig(kdfIterations) + : new Argon2KdfConfig(kdfIterations, kdfMemory, kdfParallelism); const masterKey = await this.cryptoService.makeMasterKey( password.keyB64, await this.tokenService.getEmail(), - kdf, kdfConfig, ); const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64); @@ -162,7 +165,6 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { const keys = new KeysRequest(pubKey, privKey.encryptedString); const setPasswordRequest = new SetKeyConnectorKeyRequest( userKey[1].encryptedString, - kdf, kdfConfig, orgId, keys, 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 5a443b784d..94adad8bc7 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 @@ -13,6 +13,7 @@ import { KeySuffixOptions } from "../../../platform/enums/key-suffix-options.enu import { UserId } from "../../../types/guid"; import { UserKey } from "../../../types/key"; import { AccountService } from "../../abstractions/account.service"; +import { KdfConfigService } from "../../abstractions/kdf-config.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"; @@ -47,6 +48,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti private logService: LogService, private vaultTimeoutSettingsService: VaultTimeoutSettingsServiceAbstraction, private platformUtilsService: PlatformUtilsService, + private kdfConfigService: KdfConfigService, ) {} async getAvailableVerificationOptions( @@ -118,8 +120,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti masterKey = await this.cryptoService.makeMasterKey( verification.secret, await this.stateService.getEmail(), - await this.stateService.getKdfType(), - await this.stateService.getKdfConfig(), + await this.kdfConfigService.getKdfConfig(), ); } request.masterPasswordHash = alreadyHashed @@ -176,8 +177,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti masterKey = await this.cryptoService.makeMasterKey( verification.secret, await this.stateService.getEmail(), - await this.stateService.getKdfType(), - await this.stateService.getKdfConfig(), + await this.kdfConfigService.getKdfConfig(), ); } const passwordValid = await this.cryptoService.compareAndUpdateKeyHash( diff --git a/libs/common/src/platform/abstractions/crypto.service.ts b/libs/common/src/platform/abstractions/crypto.service.ts index 6609a1014e..79a58f9d57 100644 --- a/libs/common/src/platform/abstractions/crypto.service.ts +++ b/libs/common/src/platform/abstractions/crypto.service.ts @@ -6,7 +6,7 @@ import { ProfileProviderResponse } from "../../admin-console/models/response/pro import { KdfConfig } from "../../auth/models/domain/kdf-config"; import { OrganizationId, ProviderId, UserId } from "../../types/guid"; import { UserKey, MasterKey, OrgKey, ProviderKey, PinKey, CipherKey } from "../../types/key"; -import { KeySuffixOptions, KdfType, HashPurpose } from "../enums"; +import { KeySuffixOptions, HashPurpose } from "../enums"; import { EncArrayBuffer } from "../models/domain/enc-array-buffer"; import { EncString } from "../models/domain/enc-string"; import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; @@ -114,16 +114,10 @@ export abstract class CryptoService { * Generates a master key from the provided password * @param password The user's master password * @param email The user's email - * @param kdf The user's selected key derivation function to use * @param KdfConfig The user's key derivation function configuration * @returns A master key derived from the provided password */ - abstract makeMasterKey( - password: string, - email: string, - kdf: KdfType, - KdfConfig: KdfConfig, - ): Promise<MasterKey>; + abstract makeMasterKey(password: string, email: string, KdfConfig: KdfConfig): Promise<MasterKey>; /** * Encrypts the existing (or provided) user key with the * provided master key @@ -258,16 +252,10 @@ export abstract class CryptoService { /** * @param pin The user's pin * @param salt The user's salt - * @param kdf The user's kdf * @param kdfConfig The user's kdf config * @returns A key derived from the user's pin */ - abstract makePinKey( - pin: string, - salt: string, - kdf: KdfType, - kdfConfig: KdfConfig, - ): Promise<PinKey>; + abstract makePinKey(pin: string, salt: string, kdfConfig: KdfConfig): Promise<PinKey>; /** * Clears the user's pin keys from storage * Note: This will remove the stored pin and as a result, @@ -279,7 +267,6 @@ export abstract class CryptoService { * Decrypts the user key with their pin * @param pin The user's PIN * @param salt The user's salt - * @param kdf The user's KDF * @param kdfConfig The user's KDF config * @param pinProtectedUserKey The user's PIN protected symmetric key, if not provided * it will be retrieved from storage @@ -288,7 +275,6 @@ export abstract class CryptoService { abstract decryptUserKeyWithPin( pin: string, salt: string, - kdf: KdfType, kdfConfig: KdfConfig, protectedKeyCs?: EncString, ): Promise<UserKey>; @@ -298,7 +284,6 @@ export abstract class CryptoService { * @param masterPasswordOnRestart True if Master Password on Restart is enabled * @param pin User's PIN * @param email User's email - * @param kdf User's KdfType * @param kdfConfig User's KdfConfig * @param oldPinKey The old Pin key from state (retrieved from different * places depending on if Master Password on Restart was enabled) @@ -308,7 +293,6 @@ export abstract class CryptoService { masterPasswordOnRestart: boolean, pin: string, email: string, - kdf: KdfType, kdfConfig: KdfConfig, oldPinKey: EncString, ): Promise<UserKey>; @@ -358,21 +342,12 @@ export abstract class CryptoService { privateKey: EncString; }>; - /** - * Validate that the KDF config follows the requirements for the given KDF type. - * - * @remarks - * Should always be called before updating a users KDF config. - */ - abstract validateKdfConfig(kdf: KdfType, kdfConfig: KdfConfig): void; - /** * @deprecated Left for migration purposes. Use decryptUserKeyWithPin instead. */ abstract decryptMasterKeyWithPin( pin: string, salt: string, - kdf: KdfType, kdfConfig: KdfConfig, protectedKeyCs?: EncString, ): Promise<MasterKey>; diff --git a/libs/common/src/platform/abstractions/key-generation.service.ts b/libs/common/src/platform/abstractions/key-generation.service.ts index 223eb75038..3a6971ba5d 100644 --- a/libs/common/src/platform/abstractions/key-generation.service.ts +++ b/libs/common/src/platform/abstractions/key-generation.service.ts @@ -1,6 +1,5 @@ import { KdfConfig } from "../../auth/models/domain/kdf-config"; import { CsprngArray } from "../../types/csprng"; -import { KdfType } from "../enums"; import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; export abstract class KeyGenerationService { @@ -46,14 +45,12 @@ export abstract class KeyGenerationService { * Derives a 32 byte key from a password using a key derivation function. * @param password Password to derive the key from. * @param salt Salt for the key derivation function. - * @param kdf Key derivation function to use. * @param kdfConfig Configuration for the key derivation function. * @returns 32 byte derived key. */ abstract deriveKeyFromPassword( password: string | Uint8Array, salt: string | Uint8Array, - kdf: KdfType, kdfConfig: KdfConfig, ): Promise<SymmetricCryptoKey>; } diff --git a/libs/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index f1d4b3848e..13c33305d1 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -1,12 +1,10 @@ import { Observable } from "rxjs"; -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 { UserId } from "../../types/guid"; -import { KdfType } from "../enums"; import { Account } from "../models/domain/account"; import { EncString } from "../models/domain/enc-string"; import { StorageOptions } from "../models/domain/storage-options"; @@ -149,10 +147,6 @@ export abstract class StateService<T extends Account = Account> { */ setEncryptedPinProtected: (value: string, options?: StorageOptions) => Promise<void>; getIsAuthenticated: (options?: StorageOptions) => Promise<boolean>; - getKdfConfig: (options?: StorageOptions) => Promise<KdfConfig>; - setKdfConfig: (kdfConfig: KdfConfig, options?: StorageOptions) => Promise<void>; - getKdfType: (options?: StorageOptions) => Promise<KdfType>; - setKdfType: (value: KdfType, options?: StorageOptions) => Promise<void>; getLastActive: (options?: StorageOptions) => Promise<number>; setLastActive: (value: number, options?: StorageOptions) => Promise<void>; getLastSync: (options?: StorageOptions) => Promise<string>; diff --git a/libs/common/src/platform/enums/kdf-type.enum.ts b/libs/common/src/platform/enums/kdf-type.enum.ts index 97157910f5..fd29bf308c 100644 --- a/libs/common/src/platform/enums/kdf-type.enum.ts +++ b/libs/common/src/platform/enums/kdf-type.enum.ts @@ -1,4 +1,4 @@ -import { KdfConfig } from "../../auth/models/domain/kdf-config"; +import { PBKDF2KdfConfig } from "../../auth/models/domain/kdf-config"; import { RangeWithDefault } from "../misc/range-with-default"; export enum KdfType { @@ -12,4 +12,4 @@ export const ARGON2_ITERATIONS = new RangeWithDefault(2, 10, 3); export const DEFAULT_KDF_TYPE = KdfType.PBKDF2_SHA256; export const PBKDF2_ITERATIONS = new RangeWithDefault(600_000, 2_000_000, 600_000); -export const DEFAULT_KDF_CONFIG = new KdfConfig(PBKDF2_ITERATIONS.defaultValue); +export const DEFAULT_KDF_CONFIG = new PBKDF2KdfConfig(PBKDF2_ITERATIONS.defaultValue); diff --git a/libs/common/src/platform/services/crypto.service.spec.ts b/libs/common/src/platform/services/crypto.service.spec.ts index 2f68cf2ce7..d9992adb57 100644 --- a/libs/common/src/platform/services/crypto.service.spec.ts +++ b/libs/common/src/platform/services/crypto.service.spec.ts @@ -4,6 +4,7 @@ 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 { KdfConfigService } from "../../auth/abstractions/kdf-config.service"; import { FakeMasterPasswordService } from "../../auth/services/master-password/fake-master-password.service"; import { CsprngArray } from "../../types/csprng"; import { UserId } from "../../types/guid"; @@ -37,6 +38,7 @@ describe("cryptoService", () => { const platformUtilService = mock<PlatformUtilsService>(); const logService = mock<LogService>(); const stateService = mock<StateService>(); + const kdfConfigService = mock<KdfConfigService>(); let stateProvider: FakeStateProvider; const mockUserId = Utils.newGuid() as UserId; @@ -58,6 +60,7 @@ describe("cryptoService", () => { stateService, accountService, stateProvider, + kdfConfigService, ); }); diff --git a/libs/common/src/platform/services/crypto.service.ts b/libs/common/src/platform/services/crypto.service.ts index 3cd443c073..798173f513 100644 --- a/libs/common/src/platform/services/crypto.service.ts +++ b/libs/common/src/platform/services/crypto.service.ts @@ -6,6 +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 { KdfConfigService } from "../../auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "../../auth/abstractions/master-password.service.abstraction"; import { KdfConfig } from "../../auth/models/domain/kdf-config"; import { Utils } from "../../platform/misc/utils"; @@ -28,16 +29,7 @@ import { KeyGenerationService } from "../abstractions/key-generation.service"; import { LogService } from "../abstractions/log.service"; import { PlatformUtilsService } from "../abstractions/platform-utils.service"; import { StateService } from "../abstractions/state.service"; -import { - KeySuffixOptions, - HashPurpose, - KdfType, - ARGON2_ITERATIONS, - ARGON2_MEMORY, - ARGON2_PARALLELISM, - EncryptionType, - PBKDF2_ITERATIONS, -} from "../enums"; +import { KeySuffixOptions, HashPurpose, EncryptionType } from "../enums"; import { sequentialize } from "../misc/sequentialize"; import { EFFLongWordList } from "../misc/wordlist"; import { EncArrayBuffer } from "../models/domain/enc-array-buffer"; @@ -91,6 +83,7 @@ export class CryptoService implements CryptoServiceAbstraction { protected stateService: StateService, protected accountService: AccountService, protected stateProvider: StateProvider, + protected kdfConfigService: KdfConfigService, ) { // User Key this.activeUserKeyState = stateProvider.getActive(USER_KEY); @@ -283,8 +276,7 @@ export class CryptoService implements CryptoServiceAbstraction { return (masterKey ||= await this.makeMasterKey( password, await this.stateService.getEmail({ userId: userId }), - await this.stateService.getKdfType({ userId: userId }), - await this.stateService.getKdfConfig({ userId: userId }), + await this.kdfConfigService.getKdfConfig(), )); } @@ -295,16 +287,10 @@ export class CryptoService implements CryptoServiceAbstraction { * 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, - email: string, - kdf: KdfType, - KdfConfig: KdfConfig, - ): Promise<MasterKey> { + async makeMasterKey(password: string, email: string, KdfConfig: KdfConfig): Promise<MasterKey> { return (await this.keyGenerationService.deriveKeyFromPassword( password, email, - kdf, KdfConfig, )) as MasterKey; } @@ -560,8 +546,8 @@ export class CryptoService implements CryptoServiceAbstraction { await this.stateProvider.setUserState(USER_ENCRYPTED_PRIVATE_KEY, null, userId); } - async makePinKey(pin: string, salt: string, kdf: KdfType, kdfConfig: KdfConfig): Promise<PinKey> { - const pinKey = await this.keyGenerationService.deriveKeyFromPassword(pin, salt, kdf, kdfConfig); + async makePinKey(pin: string, salt: string, kdfConfig: KdfConfig): Promise<PinKey> { + const pinKey = await this.keyGenerationService.deriveKeyFromPassword(pin, salt, kdfConfig); return (await this.stretchKey(pinKey)) as PinKey; } @@ -575,7 +561,6 @@ export class CryptoService implements CryptoServiceAbstraction { async decryptUserKeyWithPin( pin: string, salt: string, - kdf: KdfType, kdfConfig: KdfConfig, pinProtectedUserKey?: EncString, ): Promise<UserKey> { @@ -584,7 +569,7 @@ export class CryptoService implements CryptoServiceAbstraction { if (!pinProtectedUserKey) { throw new Error("No PIN protected key found."); } - const pinKey = await this.makePinKey(pin, salt, kdf, kdfConfig); + const pinKey = await this.makePinKey(pin, salt, kdfConfig); const userKey = await this.encryptService.decryptToBytes(pinProtectedUserKey, pinKey); return new SymmetricCryptoKey(userKey) as UserKey; } @@ -593,7 +578,6 @@ export class CryptoService implements CryptoServiceAbstraction { async decryptMasterKeyWithPin( pin: string, salt: string, - kdf: KdfType, kdfConfig: KdfConfig, pinProtectedMasterKey?: EncString, ): Promise<MasterKey> { @@ -604,7 +588,7 @@ export class CryptoService implements CryptoServiceAbstraction { } pinProtectedMasterKey = new EncString(pinProtectedMasterKeyString); } - const pinKey = await this.makePinKey(pin, salt, kdf, kdfConfig); + const pinKey = await this.makePinKey(pin, salt, kdfConfig); const masterKey = await this.encryptService.decryptToBytes(pinProtectedMasterKey, pinKey); return new SymmetricCryptoKey(masterKey) as MasterKey; } @@ -831,8 +815,7 @@ export class CryptoService implements CryptoServiceAbstraction { const pinKey = await this.makePinKey( pin, await this.stateService.getEmail({ userId: userId }), - await this.stateService.getKdfType({ userId: userId }), - await this.stateService.getKdfConfig({ userId: userId }), + await this.kdfConfigService.getKdfConfig(), ); const encPin = await this.encryptService.encrypt(key.key, pinKey); @@ -873,43 +856,6 @@ export class CryptoService implements CryptoServiceAbstraction { return null; } - /** - * Validate that the KDF config follows the requirements for the given KDF type. - * - * @remarks - * Should always be called before updating a users KDF config. - */ - validateKdfConfig(kdf: KdfType, kdfConfig: KdfConfig): void { - switch (kdf) { - case KdfType.PBKDF2_SHA256: - if (!PBKDF2_ITERATIONS.inRange(kdfConfig.iterations)) { - throw new Error( - `PBKDF2 iterations must be between ${PBKDF2_ITERATIONS.min} and ${PBKDF2_ITERATIONS.max}`, - ); - } - break; - case KdfType.Argon2id: - if (!ARGON2_ITERATIONS.inRange(kdfConfig.iterations)) { - throw new Error( - `Argon2 iterations must be between ${ARGON2_ITERATIONS.min} and ${ARGON2_ITERATIONS.max}`, - ); - } - - if (!ARGON2_MEMORY.inRange(kdfConfig.memory)) { - throw new Error( - `Argon2 memory must be between ${ARGON2_MEMORY.min}mb and ${ARGON2_MEMORY.max}mb`, - ); - } - - if (!ARGON2_PARALLELISM.inRange(kdfConfig.parallelism)) { - throw new Error( - `Argon2 parallelism must be between ${ARGON2_PARALLELISM.min} and ${ARGON2_PARALLELISM.max}.`, - ); - } - break; - } - } - protected async clearAllStoredUserKeys(userId?: UserId): Promise<void> { await this.stateService.setUserKeyAutoUnlock(null, { userId: userId }); await this.stateService.setPinKeyEncryptedUserKeyEphemeral(null, { userId: userId }); @@ -1007,16 +953,15 @@ export class CryptoService implements CryptoServiceAbstraction { masterPasswordOnRestart: boolean, pin: string, email: string, - kdf: KdfType, kdfConfig: KdfConfig, oldPinKey: EncString, ): Promise<UserKey> { // Decrypt - const masterKey = await this.decryptMasterKeyWithPin(pin, email, kdf, kdfConfig, oldPinKey); + const masterKey = await this.decryptMasterKeyWithPin(pin, email, kdfConfig, oldPinKey); const encUserKey = await this.stateService.getEncryptedCryptoSymmetricKey(); const userKey = await this.decryptUserKeyWithMasterKey(masterKey, new EncString(encUserKey)); // Migrate - const pinKey = await this.makePinKey(pin, email, kdf, kdfConfig); + const pinKey = await this.makePinKey(pin, email, kdfConfig); const pinProtectedKey = await this.encryptService.encrypt(userKey.key, pinKey); if (masterPasswordOnRestart) { await this.stateService.setDecryptedPinProtected(null); diff --git a/libs/common/src/platform/services/key-generation.service.spec.ts b/libs/common/src/platform/services/key-generation.service.spec.ts index b3e0aa6d4e..4f04eebd04 100644 --- a/libs/common/src/platform/services/key-generation.service.spec.ts +++ b/libs/common/src/platform/services/key-generation.service.spec.ts @@ -1,9 +1,8 @@ import { mock } from "jest-mock-extended"; -import { KdfConfig } from "../../auth/models/domain/kdf-config"; +import { Argon2KdfConfig, PBKDF2KdfConfig } from "../../auth/models/domain/kdf-config"; import { CsprngArray } from "../../types/csprng"; import { CryptoFunctionService } from "../abstractions/crypto-function.service"; -import { KdfType } from "../enums"; import { KeyGenerationService } from "./key-generation.service"; @@ -75,12 +74,11 @@ describe("KeyGenerationService", () => { it("should derive a 32 byte key from a password using pbkdf2", async () => { const password = "password"; const salt = "salt"; - const kdf = KdfType.PBKDF2_SHA256; - const kdfConfig = new KdfConfig(600_000); + const kdfConfig = new PBKDF2KdfConfig(600_000); cryptoFunctionService.pbkdf2.mockResolvedValue(new Uint8Array(32)); - const key = await sut.deriveKeyFromPassword(password, salt, kdf, kdfConfig); + const key = await sut.deriveKeyFromPassword(password, salt, kdfConfig); expect(key.key.length).toEqual(32); }); @@ -88,13 +86,12 @@ describe("KeyGenerationService", () => { it("should derive a 32 byte key from a password using argon2id", async () => { const password = "password"; const salt = "salt"; - const kdf = KdfType.Argon2id; - const kdfConfig = new KdfConfig(600_000, 15); + const kdfConfig = new Argon2KdfConfig(3, 16, 4); cryptoFunctionService.hash.mockResolvedValue(new Uint8Array(32)); cryptoFunctionService.argon2.mockResolvedValue(new Uint8Array(32)); - const key = await sut.deriveKeyFromPassword(password, salt, kdf, kdfConfig); + const key = await sut.deriveKeyFromPassword(password, salt, kdfConfig); expect(key.key.length).toEqual(32); }); diff --git a/libs/common/src/platform/services/key-generation.service.ts b/libs/common/src/platform/services/key-generation.service.ts index c592f35e5f..9202b37100 100644 --- a/libs/common/src/platform/services/key-generation.service.ts +++ b/libs/common/src/platform/services/key-generation.service.ts @@ -46,17 +46,16 @@ export class KeyGenerationService implements KeyGenerationServiceAbstraction { async deriveKeyFromPassword( password: string | Uint8Array, salt: string | Uint8Array, - kdf: KdfType, kdfConfig: KdfConfig, ): Promise<SymmetricCryptoKey> { let key: Uint8Array = null; - if (kdf == null || kdf === KdfType.PBKDF2_SHA256) { + if (kdfConfig.kdfType == null || kdfConfig.kdfType === KdfType.PBKDF2_SHA256) { if (kdfConfig.iterations == null) { kdfConfig.iterations = PBKDF2_ITERATIONS.defaultValue; } key = await this.cryptoFunctionService.pbkdf2(password, salt, "sha256", kdfConfig.iterations); - } else if (kdf == KdfType.Argon2id) { + } else if (kdfConfig.kdfType == KdfType.Argon2id) { if (kdfConfig.iterations == null) { kdfConfig.iterations = ARGON2_ITERATIONS.defaultValue; } diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index 0c7cdd22d2..cab5768d2a 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -3,7 +3,6 @@ import { Jsonify, JsonValue } from "type-fest"; import { AccountService } from "../../auth/abstractions/account.service"; import { TokenService } from "../../auth/abstractions/token.service"; -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"; @@ -19,7 +18,7 @@ import { AbstractMemoryStorageService, AbstractStorageService, } from "../abstractions/storage.service"; -import { HtmlStorageLocation, KdfType, StorageLocation } from "../enums"; +import { HtmlStorageLocation, StorageLocation } from "../enums"; import { StateFactory } from "../factories/state-factory"; import { Utils } from "../misc/utils"; import { Account, AccountData, AccountSettings } from "../models/domain/account"; @@ -643,49 +642,6 @@ export class StateService< ); } - async getKdfConfig(options?: StorageOptions): Promise<KdfConfig> { - const iterations = ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.profile?.kdfIterations; - const memory = ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.profile?.kdfMemory; - const parallelism = ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.profile?.kdfParallelism; - return new KdfConfig(iterations, memory, parallelism); - } - - async setKdfConfig(config: KdfConfig, options?: StorageOptions): Promise<void> { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.profile.kdfIterations = config.iterations; - account.profile.kdfMemory = config.memory; - account.profile.kdfParallelism = config.parallelism; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getKdfType(options?: StorageOptions): Promise<KdfType> { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.profile?.kdfType; - } - - async setKdfType(value: KdfType, options?: StorageOptions): Promise<void> { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.profile.kdfType = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - async getLastActive(options?: StorageOptions): Promise<number> { options = this.reconcileOptions(options, await this.defaultOnDiskOptions()); diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index e04110f28b..8847f7fe51 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -35,6 +35,7 @@ export const BILLING_DISK = new StateDefinition("billing", "disk"); // Auth +export const KDF_CONFIG_DISK = new StateDefinition("kdfConfig", "disk"); 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"); diff --git a/libs/common/src/state-migrations/migrate.ts b/libs/common/src/state-migrations/migrate.ts index 2d8ef1619e..31bc5460b4 100644 --- a/libs/common/src/state-migrations/migrate.ts +++ b/libs/common/src/state-migrations/migrate.ts @@ -55,6 +55,7 @@ import { MoveMasterKeyStateToProviderMigrator } from "./migrations/55-move-maste 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 { KdfConfigMigrator } from "./migrations/59-move-kdf-config-to-state-provider"; 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"; @@ -62,7 +63,7 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting import { MinVersionMigrator } from "./migrations/min-version"; export const MIN_VERSION = 3; -export const CURRENT_VERSION = 58; +export const CURRENT_VERSION = 59; export type MinVersion = typeof MIN_VERSION; export function createMigrationBuilder() { @@ -122,7 +123,8 @@ export function createMigrationBuilder() { .with(MoveMasterKeyStateToProviderMigrator, 54, 55) .with(AuthRequestMigrator, 55, 56) .with(CipherServiceMigrator, 56, 57) - .with(RemoveRefreshTokenMigratedFlagMigrator, 57, CURRENT_VERSION); + .with(RemoveRefreshTokenMigratedFlagMigrator, 57, 58) + .with(KdfConfigMigrator, 58, CURRENT_VERSION); } export async function currentVersion( diff --git a/libs/common/src/state-migrations/migrations/59-move-kdf-config-to-state-provider.spec.ts b/libs/common/src/state-migrations/migrations/59-move-kdf-config-to-state-provider.spec.ts new file mode 100644 index 0000000000..dbce750a7e --- /dev/null +++ b/libs/common/src/state-migrations/migrations/59-move-kdf-config-to-state-provider.spec.ts @@ -0,0 +1,153 @@ +import { MockProxy } from "jest-mock-extended"; + +import { KeyDefinitionLike, MigrationHelper } from "../migration-helper"; +import { mockMigrationHelper } from "../migration-helper.spec"; + +import { KdfConfigMigrator } from "./59-move-kdf-config-to-state-provider"; + +function exampleJSON() { + return { + global: { + otherStuff: "otherStuff1", + }, + authenticatedAccounts: ["FirstAccount", "SecondAccount"], + FirstAccount: { + profile: { + kdfIterations: 3, + kdfMemory: 64, + kdfParallelism: 5, + kdfType: 1, + otherStuff: "otherStuff1", + }, + otherStuff: "otherStuff2", + }, + SecondAccount: { + profile: { + kdfIterations: 600_001, + kdfMemory: null as number, + kdfParallelism: null as number, + kdfType: 0, + otherStuff: "otherStuff3", + }, + otherStuff: "otherStuff4", + }, + }; +} + +function rollbackJSON() { + return { + user_FirstAccount_kdfConfig_kdfConfig: { + iterations: 3, + memory: 64, + parallelism: 5, + kdfType: 1, + }, + user_SecondAccount_kdfConfig_kdfConfig: { + iterations: 600_001, + memory: null as number, + parallelism: null as number, + kdfType: 0, + }, + global: { + otherStuff: "otherStuff1", + }, + authenticatedAccounts: ["FirstAccount", "SecondAccount"], + FirstAccount: { + profile: { + otherStuff: "otherStuff2", + }, + otherStuff: "otherStuff3", + }, + SecondAccount: { + profile: { + otherStuff: "otherStuff4", + }, + otherStuff: "otherStuff5", + }, + }; +} + +const kdfConfigKeyDefinition: KeyDefinitionLike = { + key: "kdfConfig", + stateDefinition: { + name: "kdfConfig", + }, +}; + +describe("KdfConfigMigrator", () => { + let helper: MockProxy<MigrationHelper>; + let sut: KdfConfigMigrator; + + describe("migrate", () => { + beforeEach(() => { + helper = mockMigrationHelper(exampleJSON(), 59); + sut = new KdfConfigMigrator(58, 59); + }); + + it("should remove kdfType and kdfConfig from Account.Profile", async () => { + await sut.migrate(helper); + + expect(helper.set).toHaveBeenCalledTimes(2); + expect(helper.set).toHaveBeenCalledWith("FirstAccount", { + profile: { + otherStuff: "otherStuff1", + }, + otherStuff: "otherStuff2", + }); + expect(helper.set).toHaveBeenCalledWith("SecondAccount", { + profile: { + otherStuff: "otherStuff3", + }, + otherStuff: "otherStuff4", + }); + expect(helper.setToUser).toHaveBeenCalledWith("FirstAccount", kdfConfigKeyDefinition, { + iterations: 3, + memory: 64, + parallelism: 5, + kdfType: 1, + }); + expect(helper.setToUser).toHaveBeenCalledWith("SecondAccount", kdfConfigKeyDefinition, { + iterations: 600_001, + memory: null as number, + parallelism: null as number, + kdfType: 0, + }); + }); + }); + + describe("rollback", () => { + beforeEach(() => { + helper = mockMigrationHelper(rollbackJSON(), 59); + sut = new KdfConfigMigrator(58, 59); + }); + + it("should null out new KdfConfig account value and set account.profile", async () => { + await sut.rollback(helper); + + expect(helper.setToUser).toHaveBeenCalledTimes(2); + expect(helper.setToUser).toHaveBeenCalledWith("FirstAccount", kdfConfigKeyDefinition, null); + expect(helper.setToUser).toHaveBeenCalledWith("SecondAccount", kdfConfigKeyDefinition, null); + expect(helper.set).toHaveBeenCalledTimes(2); + expect(helper.set).toHaveBeenCalledWith("FirstAccount", { + profile: { + kdfIterations: 3, + kdfMemory: 64, + kdfParallelism: 5, + kdfType: 1, + otherStuff: "otherStuff2", + }, + otherStuff: "otherStuff3", + }); + expect(helper.set).toHaveBeenCalledWith("SecondAccount", { + profile: { + kdfIterations: 600_001, + kdfMemory: null as number, + kdfParallelism: null as number, + kdfType: 0, + otherStuff: "otherStuff4", + }, + otherStuff: "otherStuff5", + }); + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/59-move-kdf-config-to-state-provider.ts b/libs/common/src/state-migrations/migrations/59-move-kdf-config-to-state-provider.ts new file mode 100644 index 0000000000..332306c6d4 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/59-move-kdf-config-to-state-provider.ts @@ -0,0 +1,78 @@ +import { KeyDefinitionLike, MigrationHelper } from "../migration-helper"; +import { Migrator } from "../migrator"; + +enum KdfType { + PBKDF2_SHA256 = 0, + Argon2id = 1, +} + +class KdfConfig { + iterations: number; + kdfType: KdfType; + memory?: number; + parallelism?: number; +} + +type ExpectedAccountType = { + profile?: { + kdfIterations: number; + kdfType: KdfType; + kdfMemory?: number; + kdfParallelism?: number; + }; +}; + +const kdfConfigKeyDefinition: KeyDefinitionLike = { + key: "kdfConfig", + stateDefinition: { + name: "kdfConfig", + }, +}; + +export class KdfConfigMigrator extends Migrator<58, 59> { + async migrate(helper: MigrationHelper): Promise<void> { + const accounts = await helper.getAccounts<ExpectedAccountType>(); + async function migrateAccount(userId: string, account: ExpectedAccountType): Promise<void> { + const iterations = account?.profile?.kdfIterations; + const kdfType = account?.profile?.kdfType; + const memory = account?.profile?.kdfMemory; + const parallelism = account?.profile?.kdfParallelism; + + const kdfConfig: KdfConfig = { + iterations: iterations, + kdfType: kdfType, + memory: memory, + parallelism: parallelism, + }; + + if (kdfConfig != null) { + await helper.setToUser(userId, kdfConfigKeyDefinition, kdfConfig); + delete account?.profile?.kdfIterations; + delete account?.profile?.kdfType; + delete account?.profile?.kdfMemory; + delete account?.profile?.kdfParallelism; + } + + await helper.set(userId, account); + } + await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]); + } + + async rollback(helper: MigrationHelper): Promise<void> { + const accounts = await helper.getAccounts<ExpectedAccountType>(); + async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise<void> { + const kdfConfig: KdfConfig = await helper.getFromUser(userId, kdfConfigKeyDefinition); + + if (kdfConfig != null) { + account.profile.kdfIterations = kdfConfig.iterations; + account.profile.kdfType = kdfConfig.kdfType; + account.profile.kdfMemory = kdfConfig.memory; + account.profile.kdfParallelism = kdfConfig.parallelism; + await helper.setToUser(userId, kdfConfigKeyDefinition, null); + } + await helper.set(userId, account); + } + + await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]); + } +} diff --git a/libs/common/src/tools/send/services/send.service.ts b/libs/common/src/tools/send/services/send.service.ts index 33b1f28be0..fb67de5501 100644 --- a/libs/common/src/tools/send/services/send.service.ts +++ b/libs/common/src/tools/send/services/send.service.ts @@ -1,10 +1,10 @@ import { Observable, concatMap, distinctUntilChanged, firstValueFrom, map } from "rxjs"; +import { PBKDF2KdfConfig } from "../../../auth/models/domain/kdf-config"; 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 { KdfType } from "../../../platform/enums"; import { Utils } from "../../../platform/misc/utils"; import { EncArrayBuffer } from "../../../platform/models/domain/enc-array-buffer"; import { EncString } from "../../../platform/models/domain/enc-string"; @@ -69,8 +69,7 @@ export class SendService implements InternalSendServiceAbstraction { const passwordKey = await this.keyGenerationService.deriveKeyFromPassword( password, model.key, - KdfType.PBKDF2_SHA256, - { iterations: SEND_KDF_ITERATIONS }, + new PBKDF2KdfConfig(SEND_KDF_ITERATIONS), ); send.password = passwordKey.keyB64; } diff --git a/libs/importer/src/importers/bitwarden/bitwarden-password-protected-importer.ts b/libs/importer/src/importers/bitwarden/bitwarden-password-protected-importer.ts index 9e047b063c..8f9e1abaf1 100644 --- a/libs/importer/src/importers/bitwarden/bitwarden-password-protected-importer.ts +++ b/libs/importer/src/importers/bitwarden/bitwarden-password-protected-importer.ts @@ -1,4 +1,8 @@ -import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; +import { + Argon2KdfConfig, + KdfConfig, + PBKDF2KdfConfig, +} from "@bitwarden/common/auth/models/domain/kdf-config"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { KdfType } from "@bitwarden/common/platform/enums"; @@ -69,12 +73,12 @@ export class BitwardenPasswordProtectedImporter extends BitwardenJsonImporter im return false; } - this.key = await this.cryptoService.makePinKey( - password, - jdoc.salt, - jdoc.kdfType, - new KdfConfig(jdoc.kdfIterations, jdoc.kdfMemory, jdoc.kdfParallelism), - ); + const kdfConfig: KdfConfig = + jdoc.kdfType === KdfType.PBKDF2_SHA256 + ? new PBKDF2KdfConfig(jdoc.kdfIterations) + : new Argon2KdfConfig(jdoc.kdfIterations, jdoc.kdfMemory, jdoc.kdfParallelism); + + this.key = await this.cryptoService.makePinKey(password, jdoc.salt, kdfConfig); const encKeyValidation = new EncString(jdoc.encKeyValidation_DO_NOT_EDIT); diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/base-vault-export.service.ts b/libs/tools/export/vault-export/vault-export-core/src/services/base-vault-export.service.ts index 1865c94c7d..dd5a210bf8 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/base-vault-export.service.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/base-vault-export.service.ts @@ -1,7 +1,7 @@ +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { KdfType } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CipherType } from "@bitwarden/common/vault/enums"; @@ -12,15 +12,14 @@ export class BaseVaultExportService { constructor( protected cryptoService: CryptoService, private cryptoFunctionService: CryptoFunctionService, - private stateService: StateService, + private kdfConfigService: KdfConfigService, ) {} protected async buildPasswordExport(clearText: string, password: string): Promise<string> { - const kdfType: KdfType = await this.stateService.getKdfType(); - const kdfConfig: KdfConfig = await this.stateService.getKdfConfig(); + const kdfConfig: KdfConfig = await this.kdfConfigService.getKdfConfig(); const salt = Utils.fromBufferToB64(await this.cryptoFunctionService.randomBytes(16)); - const key = await this.cryptoService.makePinKey(password, salt, kdfType, kdfConfig); + const key = await this.cryptoService.makePinKey(password, salt, kdfConfig); const encKeyValidation = await this.cryptoService.encrypt(Utils.newGuid(), key); const encText = await this.cryptoService.encrypt(clearText, key); @@ -29,14 +28,17 @@ export class BaseVaultExportService { encrypted: true, passwordProtected: true, salt: salt, - kdfType: kdfType, + kdfType: kdfConfig.kdfType, kdfIterations: kdfConfig.iterations, - kdfMemory: kdfConfig.memory, - kdfParallelism: kdfConfig.parallelism, encKeyValidation_DO_NOT_EDIT: encKeyValidation.encryptedString, data: encText.encryptedString, }; + if (kdfConfig.kdfType === KdfType.Argon2id) { + jsonDoc.kdfMemory = kdfConfig.memory; + jsonDoc.kdfParallelism = kdfConfig.parallelism; + } + return JSON.stringify(jsonDoc, null, " "); } diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts index fc8faa4b5b..b30384f9f4 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts @@ -1,13 +1,12 @@ import { mock, MockProxy } from "jest-mock-extended"; -import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { CipherWithIdExport } from "@bitwarden/common/models/export/cipher-with-ids.export"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; -import { KdfType, PBKDF2_ITERATIONS } from "@bitwarden/common/platform/enums"; +import { DEFAULT_KDF_CONFIG, KdfType, PBKDF2_ITERATIONS } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string"; -import { StateService } from "@bitwarden/common/platform/services/state.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { CipherType } from "@bitwarden/common/vault/enums"; @@ -110,10 +109,10 @@ function expectEqualCiphers(ciphers: CipherView[] | Cipher[], jsonResult: string expect(actual).toEqual(JSON.stringify(items)); } -function expectEqualFolderViews(folderviews: FolderView[] | Folder[], jsonResult: string) { +function expectEqualFolderViews(folderViews: FolderView[] | Folder[], jsonResult: string) { const actual = JSON.stringify(JSON.parse(jsonResult).folders); const folders: FolderResponse[] = []; - folderviews.forEach((c) => { + folderViews.forEach((c) => { const folder = new FolderResponse(); folder.id = c.id; folder.name = c.name.toString(); @@ -144,19 +143,18 @@ describe("VaultExportService", () => { let cipherService: MockProxy<CipherService>; let folderService: MockProxy<FolderService>; let cryptoService: MockProxy<CryptoService>; - let stateService: MockProxy<StateService>; + let kdfConfigService: MockProxy<KdfConfigService>; beforeEach(() => { cryptoFunctionService = mock<CryptoFunctionService>(); cipherService = mock<CipherService>(); folderService = mock<FolderService>(); cryptoService = mock<CryptoService>(); - stateService = mock<StateService>(); + kdfConfigService = mock<KdfConfigService>(); folderService.getAllDecryptedFromState.mockResolvedValue(UserFolderViews); folderService.getAllFromState.mockResolvedValue(UserFolders); - stateService.getKdfType.mockResolvedValue(KdfType.PBKDF2_SHA256); - stateService.getKdfConfig.mockResolvedValue(new KdfConfig(PBKDF2_ITERATIONS.defaultValue)); + kdfConfigService.getKdfConfig.mockResolvedValue(DEFAULT_KDF_CONFIG); cryptoService.encrypt.mockResolvedValue(new EncString("encrypted")); exportService = new IndividualVaultExportService( @@ -164,7 +162,7 @@ describe("VaultExportService", () => { cipherService, cryptoService, cryptoFunctionService, - stateService, + kdfConfigService, ); }); diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts index 5f3bd9de52..ee178767f4 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts @@ -1,9 +1,9 @@ import * as papa from "papaparse"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { CipherWithIdExport, FolderWithIdExport } from "@bitwarden/common/models/export"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; @@ -32,9 +32,9 @@ export class IndividualVaultExportService private cipherService: CipherService, cryptoService: CryptoService, cryptoFunctionService: CryptoFunctionService, - stateService: StateService, + kdfConfigService: KdfConfigService, ) { - super(cryptoService, cryptoFunctionService, stateService); + super(cryptoService, cryptoFunctionService, kdfConfigService); } async getExport(format: ExportFormat = "csv"): Promise<string> { diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts b/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts index 2346545231..98baf5dca3 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts @@ -1,10 +1,10 @@ import * as papa from "papaparse"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { CipherWithIdExport, CollectionWithIdExport } from "@bitwarden/common/models/export"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; @@ -36,10 +36,10 @@ export class OrganizationVaultExportService private apiService: ApiService, cryptoService: CryptoService, cryptoFunctionService: CryptoFunctionService, - stateService: StateService, private collectionService: CollectionService, + kdfConfigService: KdfConfigService, ) { - super(cryptoService, cryptoFunctionService, stateService); + super(cryptoService, cryptoFunctionService, kdfConfigService); } async getPasswordProtectedExport( diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.spec.ts b/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.spec.ts index fc8faa4b5b..b30384f9f4 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.spec.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.spec.ts @@ -1,13 +1,12 @@ import { mock, MockProxy } from "jest-mock-extended"; -import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { CipherWithIdExport } from "@bitwarden/common/models/export/cipher-with-ids.export"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; -import { KdfType, PBKDF2_ITERATIONS } from "@bitwarden/common/platform/enums"; +import { DEFAULT_KDF_CONFIG, KdfType, PBKDF2_ITERATIONS } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string"; -import { StateService } from "@bitwarden/common/platform/services/state.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { CipherType } from "@bitwarden/common/vault/enums"; @@ -110,10 +109,10 @@ function expectEqualCiphers(ciphers: CipherView[] | Cipher[], jsonResult: string expect(actual).toEqual(JSON.stringify(items)); } -function expectEqualFolderViews(folderviews: FolderView[] | Folder[], jsonResult: string) { +function expectEqualFolderViews(folderViews: FolderView[] | Folder[], jsonResult: string) { const actual = JSON.stringify(JSON.parse(jsonResult).folders); const folders: FolderResponse[] = []; - folderviews.forEach((c) => { + folderViews.forEach((c) => { const folder = new FolderResponse(); folder.id = c.id; folder.name = c.name.toString(); @@ -144,19 +143,18 @@ describe("VaultExportService", () => { let cipherService: MockProxy<CipherService>; let folderService: MockProxy<FolderService>; let cryptoService: MockProxy<CryptoService>; - let stateService: MockProxy<StateService>; + let kdfConfigService: MockProxy<KdfConfigService>; beforeEach(() => { cryptoFunctionService = mock<CryptoFunctionService>(); cipherService = mock<CipherService>(); folderService = mock<FolderService>(); cryptoService = mock<CryptoService>(); - stateService = mock<StateService>(); + kdfConfigService = mock<KdfConfigService>(); folderService.getAllDecryptedFromState.mockResolvedValue(UserFolderViews); folderService.getAllFromState.mockResolvedValue(UserFolders); - stateService.getKdfType.mockResolvedValue(KdfType.PBKDF2_SHA256); - stateService.getKdfConfig.mockResolvedValue(new KdfConfig(PBKDF2_ITERATIONS.defaultValue)); + kdfConfigService.getKdfConfig.mockResolvedValue(DEFAULT_KDF_CONFIG); cryptoService.encrypt.mockResolvedValue(new EncString("encrypted")); exportService = new IndividualVaultExportService( @@ -164,7 +162,7 @@ describe("VaultExportService", () => { cipherService, cryptoService, cryptoFunctionService, - stateService, + kdfConfigService, ); }); From e516eec200929f49b3710afc40efc361722da678 Mon Sep 17 00:00:00 2001 From: Matt Gibson <mgibson@bitwarden.com> Date: Thu, 25 Apr 2024 14:55:45 -0400 Subject: [PATCH 281/351] Reintroduce null object remove rerouting (#8920) * Reintroduce null object remove rerouting * Test remove redirect --- .../abstract-chrome-storage-api.service.ts | 5 +++++ .../abstractions/chrome-storage-api.service.spec.ts | 11 +++++++++++ 2 files changed, 16 insertions(+) diff --git a/apps/browser/src/platform/services/abstractions/abstract-chrome-storage-api.service.ts b/apps/browser/src/platform/services/abstractions/abstract-chrome-storage-api.service.ts index 64935ab591..259d6f154a 100644 --- a/apps/browser/src/platform/services/abstractions/abstract-chrome-storage-api.service.ts +++ b/apps/browser/src/platform/services/abstractions/abstract-chrome-storage-api.service.ts @@ -78,6 +78,11 @@ export default abstract class AbstractChromeStorageService async save(key: string, obj: any): Promise<void> { obj = objToStore(obj); + if (obj == null) { + // Safari does not support set of null values + return this.remove(key); + } + const keyedObj = { [key]: obj }; return new Promise<void>((resolve) => { this.chromeStorageApi.set(keyedObj, () => { diff --git a/apps/browser/src/platform/services/abstractions/chrome-storage-api.service.spec.ts b/apps/browser/src/platform/services/abstractions/chrome-storage-api.service.spec.ts index 812901879d..ceadc16a58 100644 --- a/apps/browser/src/platform/services/abstractions/chrome-storage-api.service.spec.ts +++ b/apps/browser/src/platform/services/abstractions/chrome-storage-api.service.spec.ts @@ -62,6 +62,17 @@ describe("ChromeStorageApiService", () => { expect.any(Function), ); }); + + it("removes the key when the value is null", async () => { + const removeMock = chrome.storage.local.remove as jest.Mock; + removeMock.mockImplementation((key, callback) => { + delete store[key]; + callback(); + }); + const key = "key"; + await service.save(key, null); + expect(removeMock).toHaveBeenCalledWith(key, expect.any(Function)); + }); }); describe("get", () => { From cbf7c292f33dc38e3872d4527a970e0b36a3f283 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Thu, 25 Apr 2024 15:27:06 -0400 Subject: [PATCH 282/351] [AC-2485] Add redirects to clients components based on FF and provider status (#8839) * Add provider clients redirects based on FF and provider status * Fixing broken test --- .../clients/base-clients.component.ts | 130 +++++++++++ .../providers/clients/clients.component.ts | 187 +++++---------- .../providers/providers-layout.component.html | 7 +- .../providers/providers-layout.component.ts | 3 + ...t-organization-subscription.component.html | 9 +- .../manage-client-organizations.component.ts | 213 ++++++------------ libs/common/src/admin-console/enums/index.ts | 1 + .../enums/provider-status-type.enum.ts | 5 + .../models/data/provider.data.ts | 4 +- .../admin-console/models/domain/provider.ts | 4 +- .../response/profile-provider.response.ts | 4 +- .../services/provider.service.spec.ts | 3 +- 12 files changed, 287 insertions(+), 283 deletions(-) create mode 100644 bitwarden_license/bit-web/src/app/admin-console/providers/clients/base-clients.component.ts create mode 100644 libs/common/src/admin-console/enums/provider-status-type.enum.ts diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/base-clients.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/base-clients.component.ts new file mode 100644 index 0000000000..604d61f3db --- /dev/null +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/base-clients.component.ts @@ -0,0 +1,130 @@ +import { SelectionModel } from "@angular/cdk/collections"; +import { Directive, OnDestroy, OnInit } from "@angular/core"; +import { ActivatedRoute } from "@angular/router"; +import { BehaviorSubject, from, Subject, switchMap } from "rxjs"; +import { first, takeUntil } from "rxjs/operators"; + +import { SearchService } from "@bitwarden/common/abstractions/search.service"; +import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; +import { DialogService, TableDataSource, ToastService } from "@bitwarden/components"; + +import { WebProviderService } from "../services/web-provider.service"; + +@Directive() +export abstract class BaseClientsComponent implements OnInit, OnDestroy { + protected destroy$ = new Subject<void>(); + + private searchText$ = new BehaviorSubject<string>(""); + + get searchText() { + return this.searchText$.value; + } + + set searchText(value: string) { + this.searchText$.next(value); + this.selection.clear(); + this.dataSource.filter = value; + } + + private searching = false; + protected scrolled = false; + protected pageSize = 100; + private pagedClientsCount = 0; + protected selection = new SelectionModel<string>(true, []); + + protected clients: ProviderOrganizationOrganizationDetailsResponse[]; + protected pagedClients: ProviderOrganizationOrganizationDetailsResponse[]; + protected dataSource = new TableDataSource<ProviderOrganizationOrganizationDetailsResponse>(); + + abstract providerId: string; + + protected constructor( + protected activatedRoute: ActivatedRoute, + protected dialogService: DialogService, + private i18nService: I18nService, + private searchService: SearchService, + private toastService: ToastService, + private validationService: ValidationService, + private webProviderService: WebProviderService, + ) {} + + abstract load(): Promise<void>; + + ngOnInit() { + this.activatedRoute.queryParams + .pipe(first(), takeUntil(this.destroy$)) + .subscribe((queryParams) => { + this.searchText = queryParams.search; + }); + + this.searchText$ + .pipe( + switchMap((searchText) => from(this.searchService.isSearchable(searchText))), + takeUntil(this.destroy$), + ) + .subscribe((isSearchable) => { + this.searching = isSearchable; + }); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + + isPaging() { + if (this.searching && this.scrolled) { + this.resetPaging(); + } + return !this.searching && this.clients && this.clients.length > this.pageSize; + } + + 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.scrolled = this.pagedClients.length > this.pageSize; + } + + async remove(organization: ProviderOrganizationOrganizationDetailsResponse) { + const confirmed = await this.dialogService.openSimpleDialog({ + title: organization.organizationName, + content: { key: "detachOrganizationConfirmation" }, + type: "warning", + }); + + if (!confirmed) { + return; + } + + try { + await this.webProviderService.detachOrganization(this.providerId, organization.id); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("detachedOrganization", organization.organizationName), + }); + await this.load(); + } catch (e) { + this.validationService.showError(e); + } + } +} diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts index abdfd6deff..54e264c666 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts @@ -1,29 +1,26 @@ -import { Component, OnInit } from "@angular/core"; +import { Component } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; -import { BehaviorSubject, Subject, firstValueFrom, from } from "rxjs"; -import { first, switchMap, takeUntil } from "rxjs/operators"; +import { combineLatest, firstValueFrom, from } from "rxjs"; +import { concatMap, switchMap, takeUntil } from "rxjs/operators"; -import { ModalService } from "@bitwarden/angular/services/modal.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { SearchService } from "@bitwarden/common/abstractions/search.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 { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; -import { ProviderUserType } from "@bitwarden/common/admin-console/enums"; +import { ProviderStatusType, ProviderUserType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response"; import { PlanType } from "@bitwarden/common/billing/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 { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { WebProviderService } from "../services/web-provider.service"; import { AddOrganizationComponent } from "./add-organization.component"; +import { BaseClientsComponent } from "./base-clients.component"; const DisallowedPlanTypes = [ PlanType.Free, @@ -36,90 +33,76 @@ const DisallowedPlanTypes = [ @Component({ templateUrl: "clients.component.html", }) -// eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class ClientsComponent implements OnInit { +export class ClientsComponent extends BaseClientsComponent { providerId: string; addableOrganizations: Organization[]; loading = true; manageOrganizations = false; showAddExisting = false; - clients: ProviderOrganizationOrganizationDetailsResponse[]; - pagedClients: ProviderOrganizationOrganizationDetailsResponse[]; - - protected didScroll = false; - protected pageSize = 100; - protected actionPromise: Promise<unknown>; - private pagedClientsCount = 0; - - protected enableConsolidatedBilling$ = this.configService.getFeatureFlag$( + protected consolidatedBillingEnabled$ = this.configService.getFeatureFlag$( FeatureFlag.EnableConsolidatedBilling, false, ); - private destroy$ = new Subject<void>(); - private _searchText$ = new BehaviorSubject<string>(""); - private isSearching: boolean = false; - - get searchText() { - return this._searchText$.value; - } - - set searchText(value: string) { - this._searchText$.next(value); - } constructor( - private route: ActivatedRoute, private router: Router, private providerService: ProviderService, private apiService: ApiService, - private searchService: SearchService, - private platformUtilsService: PlatformUtilsService, - private i18nService: I18nService, - private validationService: ValidationService, - private webProviderService: WebProviderService, - private logService: LogService, - private modalService: ModalService, private organizationService: OrganizationService, private organizationApiService: OrganizationApiServiceAbstraction, - private dialogService: DialogService, private configService: ConfigService, - ) {} + activatedRoute: ActivatedRoute, + dialogService: DialogService, + i18nService: I18nService, + searchService: SearchService, + toastService: ToastService, + validationService: ValidationService, + webProviderService: WebProviderService, + ) { + super( + activatedRoute, + dialogService, + i18nService, + searchService, + toastService, + validationService, + webProviderService, + ); + } - async ngOnInit() { - const enableConsolidatedBilling = await firstValueFrom(this.enableConsolidatedBilling$); - - if (enableConsolidatedBilling) { - await this.router.navigate(["../manage-client-organizations"], { relativeTo: this.route }); - } else { - this.route.parent.params - .pipe( - switchMap((params) => { - this.providerId = params.providerId; - return from(this.load()); - }), - takeUntil(this.destroy$), - ) - .subscribe(); - - this.route.queryParams.pipe(first(), takeUntil(this.destroy$)).subscribe((qParams) => { - this.searchText = qParams.search; - }); - - this._searchText$ - .pipe( - switchMap((searchText) => from(this.searchService.isSearchable(searchText))), - takeUntil(this.destroy$), - ) - .subscribe((isSearchable) => { - this.isSearching = isSearchable; - }); - } + ngOnInit() { + this.activatedRoute.parent.params + .pipe( + switchMap((params) => { + this.providerId = params.providerId; + return combineLatest([ + this.providerService.get(this.providerId), + this.consolidatedBillingEnabled$, + ]).pipe( + concatMap(([provider, consolidatedBillingEnabled]) => { + if ( + consolidatedBillingEnabled && + provider.providerStatus === ProviderStatusType.Billable + ) { + return from( + this.router.navigate(["../manage-client-organizations"], { + relativeTo: this.activatedRoute, + }), + ); + } else { + return from(this.load()); + } + }), + ); + }), + takeUntil(this.destroy$), + ) + .subscribe(); } ngOnDestroy() { - this.destroy$.next(); - this.destroy$.complete(); + super.ngOnDestroy(); } async load() { @@ -141,37 +124,6 @@ export class ClientsComponent implements OnInit { this.loading = false; } - isPaging() { - const searching = this.isSearching; - if (searching && this.didScroll) { - this.resetPaging(); - } - return !searching && this.clients && this.clients.length > this.pageSize; - } - - 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 addExistingOrganization() { const dialogRef = AddOrganizationComponent.open(this.dialogService, { providerId: this.providerId, @@ -182,33 +134,4 @@ export class ClientsComponent implements OnInit { 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/admin-console/providers/providers-layout.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html index ca2b1a3545..55efbe1386 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html @@ -7,7 +7,12 @@ <bit-nav-item icon="bwi-bank" [text]="'clients' | i18n" - [route]="(enableConsolidatedBilling$ | async) ? 'manage-client-organizations' : 'clients'" + [route]=" + (enableConsolidatedBilling$ | async) && + provider.providerStatus === ProviderStatusType.Billable + ? 'manage-client-organizations' + : 'clients' + " ></bit-nav-item> <bit-nav-group icon="bwi-sliders" [text]="'manage' | i18n" route="manage" *ngIf="showManageTab"> <bit-nav-item diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.ts index c60c9d3a03..c78bf476c0 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.ts @@ -4,6 +4,7 @@ import { ActivatedRoute, RouterModule } from "@angular/router"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; +import { ProviderStatusType } from "@bitwarden/common/admin-console/enums"; import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -83,4 +84,6 @@ export class ProvidersLayoutComponent { return "manage/events"; } } + + protected readonly ProviderStatusType = ProviderStatusType; } 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 index 18d6b3e63c..d1e4fe8b1f 100644 --- 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 @@ -11,14 +11,7 @@ <bit-label> {{ "assignedSeats" | i18n }} </bit-label> - <input - id="assignedSeats" - type="number" - appAutoFocus - bitInput - required - [(ngModel)]="assignedSeats" - /> + <input id="assignedSeats" type="number" bitInput required [(ngModel)]="assignedSeats" /> </bit-form-field> <ng-container *ngIf="remainingOpenSeats > 0"> <p> 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 index 2184a617cf..3cc96c4589 100644 --- 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 @@ -1,21 +1,23 @@ -import { SelectionModel } from "@angular/cdk/collections"; -import { Component, OnDestroy, OnInit } from "@angular/core"; -import { ActivatedRoute } from "@angular/router"; -import { BehaviorSubject, firstValueFrom, from, lastValueFrom, Subject } from "rxjs"; -import { first, switchMap, takeUntil } from "rxjs/operators"; +import { Component } from "@angular/core"; +import { ActivatedRoute, Router } from "@angular/router"; +import { combineLatest, firstValueFrom, from, lastValueFrom } from "rxjs"; +import { concatMap, 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 { ProviderStatusType, ProviderUserType } from "@bitwarden/common/admin-console/enums"; +import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; 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 { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response"; +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 { 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 { DialogService, ToastService } from "@bitwarden/components"; +import { BaseClientsComponent } from "../../../admin-console/providers/clients/base-clients.component"; import { WebProviderService } from "../../../admin-console/providers/services/web-provider.service"; import { @@ -27,127 +29,91 @@ import { ManageClientOrganizationSubscriptionComponent } from "./manage-client-o @Component({ templateUrl: "manage-client-organizations.component.html", }) - -// eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class ManageClientOrganizationsComponent implements OnInit, OnDestroy { +export class ManageClientOrganizationsComponent extends BaseClientsComponent { providerId: string; + provider: Provider; + loading = true; manageOrganizations = false; - private destroy$ = new Subject<void>(); - private _searchText$ = new BehaviorSubject<string>(""); - private isSearching: boolean = false; + private consolidatedBillingEnabled$ = this.configService.getFeatureFlag$( + FeatureFlag.EnableConsolidatedBilling, + 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<unknown>; - private pagedClientsCount = 0; - selection = new SelectionModel<string>(true, []); - protected dataSource = new TableDataSource<ProviderOrganizationOrganizationDetailsResponse>(); protected plans: PlanResponse[]; 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, private billingApiService: BillingApiService, - ) {} - - 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; - }); + private configService: ConfigService, + private providerService: ProviderService, + private router: Router, + activatedRoute: ActivatedRoute, + dialogService: DialogService, + i18nService: I18nService, + searchService: SearchService, + toastService: ToastService, + validationService: ValidationService, + webProviderService: WebProviderService, + ) { + super( + activatedRoute, + dialogService, + i18nService, + searchService, + toastService, + validationService, + webProviderService, + ); } - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); + ngOnInit() { + this.activatedRoute.parent.params + .pipe( + switchMap((params) => { + this.providerId = params.providerId; + return combineLatest([ + this.providerService.get(this.providerId), + this.consolidatedBillingEnabled$, + ]).pipe( + concatMap(([provider, consolidatedBillingEnabled]) => { + if ( + !consolidatedBillingEnabled || + provider.providerStatus !== ProviderStatusType.Billable + ) { + return from( + this.router.navigate(["../clients"], { + relativeTo: this.activatedRoute, + }), + ); + } else { + this.provider = provider; + this.manageOrganizations = this.provider.type === ProviderUserType.ProviderAdmin; + return from(this.load()); + } + }), + ); + }), + takeUntil(this.destroy$), + ) + .subscribe(); + } + + ngOnDestroy() { + super.ngOnDestroy(); } async load() { - const clientsResponse = await this.apiService.getProviderClients(this.providerId); - this.clients = - clientsResponse.data != null && clientsResponse.data.length > 0 ? clientsResponse.data : []; - this.dataSource.data = this.clients; - this.manageOrganizations = - (await this.providerService.get(this.providerId)).type === ProviderUserType.ProviderAdmin; + this.clients = (await this.apiService.getProviderClients(this.providerId)).data; - const plansResponse = await this.billingApiService.getPlans(); - this.plans = plansResponse.data; + this.dataSource.data = this.clients; + + this.plans = (await this.billingApiService.getPlans()).data; 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; @@ -161,35 +127,6 @@ export class ManageClientOrganizationsComponent implements OnInit, OnDestroy { 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; - } - createClientOrganization = async () => { const reference = openCreateClientOrganizationDialog(this.dialogService, { data: { diff --git a/libs/common/src/admin-console/enums/index.ts b/libs/common/src/admin-console/enums/index.ts index 0cbdf65805..83b8a941a0 100644 --- a/libs/common/src/admin-console/enums/index.ts +++ b/libs/common/src/admin-console/enums/index.ts @@ -7,3 +7,4 @@ export * from "./provider-type.enum"; export * from "./provider-user-status-type.enum"; export * from "./provider-user-type.enum"; export * from "./scim-provider-type.enum"; +export * from "./provider-status-type.enum"; diff --git a/libs/common/src/admin-console/enums/provider-status-type.enum.ts b/libs/common/src/admin-console/enums/provider-status-type.enum.ts new file mode 100644 index 0000000000..8da60af0eb --- /dev/null +++ b/libs/common/src/admin-console/enums/provider-status-type.enum.ts @@ -0,0 +1,5 @@ +export enum ProviderStatusType { + Pending = 0, + Created = 1, + Billable = 2, +} diff --git a/libs/common/src/admin-console/models/data/provider.data.ts b/libs/common/src/admin-console/models/data/provider.data.ts index a848888025..ff060ae270 100644 --- a/libs/common/src/admin-console/models/data/provider.data.ts +++ b/libs/common/src/admin-console/models/data/provider.data.ts @@ -1,4 +1,4 @@ -import { ProviderUserStatusType, ProviderUserType } from "../../enums"; +import { ProviderStatusType, ProviderUserStatusType, ProviderUserType } from "../../enums"; import { ProfileProviderResponse } from "../response/profile-provider.response"; export class ProviderData { @@ -9,6 +9,7 @@ export class ProviderData { enabled: boolean; userId: string; useEvents: boolean; + providerStatus: ProviderStatusType; constructor(response: ProfileProviderResponse) { this.id = response.id; @@ -18,5 +19,6 @@ export class ProviderData { this.enabled = response.enabled; this.userId = response.userId; this.useEvents = response.useEvents; + this.providerStatus = response.providerStatus; } } diff --git a/libs/common/src/admin-console/models/domain/provider.ts b/libs/common/src/admin-console/models/domain/provider.ts index d6d3d3c462..d51f698547 100644 --- a/libs/common/src/admin-console/models/domain/provider.ts +++ b/libs/common/src/admin-console/models/domain/provider.ts @@ -1,4 +1,4 @@ -import { ProviderUserStatusType, ProviderUserType } from "../../enums"; +import { ProviderStatusType, ProviderUserStatusType, ProviderUserType } from "../../enums"; import { ProviderData } from "../data/provider.data"; export class Provider { @@ -9,6 +9,7 @@ export class Provider { enabled: boolean; userId: string; useEvents: boolean; + providerStatus: ProviderStatusType; constructor(obj?: ProviderData) { if (obj == null) { @@ -22,6 +23,7 @@ export class Provider { this.enabled = obj.enabled; this.userId = obj.userId; this.useEvents = obj.useEvents; + this.providerStatus = obj.providerStatus; } get canAccess() { diff --git a/libs/common/src/admin-console/models/response/profile-provider.response.ts b/libs/common/src/admin-console/models/response/profile-provider.response.ts index eaecc9b847..701fe843de 100644 --- a/libs/common/src/admin-console/models/response/profile-provider.response.ts +++ b/libs/common/src/admin-console/models/response/profile-provider.response.ts @@ -1,5 +1,5 @@ import { BaseResponse } from "../../../models/response/base.response"; -import { ProviderUserStatusType, ProviderUserType } from "../../enums"; +import { ProviderStatusType, ProviderUserStatusType, ProviderUserType } from "../../enums"; import { PermissionsApi } from "../api/permissions.api"; export class ProfileProviderResponse extends BaseResponse { @@ -12,6 +12,7 @@ export class ProfileProviderResponse extends BaseResponse { permissions: PermissionsApi; userId: string; useEvents: boolean; + providerStatus: ProviderStatusType; constructor(response: any) { super(response); @@ -24,5 +25,6 @@ export class ProfileProviderResponse extends BaseResponse { this.permissions = new PermissionsApi(this.getResponseProperty("permissions")); this.userId = this.getResponseProperty("UserId"); this.useEvents = this.getResponseProperty("UseEvents"); + this.providerStatus = this.getResponseProperty("ProviderStatus"); } } diff --git a/libs/common/src/admin-console/services/provider.service.spec.ts b/libs/common/src/admin-console/services/provider.service.spec.ts index fcba9d5023..95da633f5c 100644 --- a/libs/common/src/admin-console/services/provider.service.spec.ts +++ b/libs/common/src/admin-console/services/provider.service.spec.ts @@ -2,7 +2,7 @@ import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from ". import { FakeActiveUserState, FakeSingleUserState } from "../../../spec/fake-state"; import { Utils } from "../../platform/misc/utils"; import { UserId } from "../../types/guid"; -import { ProviderUserStatusType, ProviderUserType } from "../enums"; +import { ProviderStatusType, ProviderUserStatusType, ProviderUserType } from "../enums"; import { ProviderData } from "../models/data/provider.data"; import { Provider } from "../models/domain/provider"; @@ -64,6 +64,7 @@ describe("PROVIDERS key definition", () => { enabled: true, userId: "string", useEvents: true, + providerStatus: ProviderStatusType.Pending, }, }; const result = sut.deserializer(JSON.parse(JSON.stringify(expectedResult))); From 8afe915be1480220d8bb4a327455c6a4c4015b10 Mon Sep 17 00:00:00 2001 From: Jake Fink <jfink@bitwarden.com> Date: Thu, 25 Apr 2024 16:45:23 -0400 Subject: [PATCH 283/351] [PM-7564] Move 2fa and login strategy service to popup and add state providers to 2fa service (#8820) * remove 2fa from main.background * remove login strategy service from main.background * move 2fa and login strategy service to popup, init in browser * add state providers to 2fa service - add deserializer helpers * use key definitions for global state * fix calls to 2fa service * remove extra await * add delay to wait for active account emission in popup * add and fix tests * fix cli * really fix cli * remove timeout and wait for active account * verify expected user is active account * fix tests * address feedback --- .../two-factor-service.factory.ts | 6 +- .../popup/two-factor-options.component.ts | 11 ++- .../browser/src/background/main.background.ts | 37 +------- .../src/popup/services/init.service.ts | 3 + .../src/popup/services/services.module.ts | 16 +--- apps/cli/src/auth/commands/login.command.ts | 4 +- apps/cli/src/bw.ts | 6 +- .../src/auth/components/sso.component.spec.ts | 2 +- .../two-factor-options.component.ts | 11 ++- .../auth/components/two-factor.component.ts | 22 +++-- .../src/services/jslib-services.module.ts | 2 +- .../auth-request-login.strategy.spec.ts | 4 +- .../login-strategies/login.strategy.spec.ts | 33 ++++--- .../common/login-strategies/login.strategy.ts | 34 ++++++-- .../password-login.strategy.spec.ts | 4 +- .../sso-login.strategy.spec.ts | 4 +- .../user-api-login.strategy.spec.ts | 4 +- .../webauthn-login.strategy.spec.ts | 10 ++- .../auth/abstractions/two-factor.service.ts | 14 +-- .../src/auth/models/domain/auth-result.ts | 2 +- .../response/identity-two-factor.response.ts | 14 +-- .../src/auth/services/two-factor.service.ts | 86 ++++++++++++------- .../state/deserialization-helpers.spec.ts | 25 ++++++ .../platform/state/deserialization-helpers.ts | 10 +-- .../src/platform/state/key-definition.ts | 2 +- .../src/platform/state/state-definitions.ts | 1 + .../src/platform/state/user-key-definition.ts | 2 +- 27 files changed, 217 insertions(+), 152 deletions(-) create mode 100644 libs/common/src/platform/state/deserialization-helpers.spec.ts diff --git a/apps/browser/src/auth/background/service-factories/two-factor-service.factory.ts b/apps/browser/src/auth/background/service-factories/two-factor-service.factory.ts index 1d79bbbaf1..5af5eb0017 100644 --- a/apps/browser/src/auth/background/service-factories/two-factor-service.factory.ts +++ b/apps/browser/src/auth/background/service-factories/two-factor-service.factory.ts @@ -1,11 +1,13 @@ import { TwoFactorService as AbstractTwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorService } from "@bitwarden/common/auth/services/two-factor.service"; +import { GlobalStateProvider } from "@bitwarden/common/platform/state"; import { FactoryOptions, CachedServices, factory, } from "../../../platform/background/service-factories/factory-options"; +import { globalStateProviderFactory } from "../../../platform/background/service-factories/global-state-provider.factory"; import { I18nServiceInitOptions, i18nServiceFactory, @@ -19,7 +21,8 @@ type TwoFactorServiceFactoryOptions = FactoryOptions; export type TwoFactorServiceInitOptions = TwoFactorServiceFactoryOptions & I18nServiceInitOptions & - PlatformUtilsServiceInitOptions; + PlatformUtilsServiceInitOptions & + GlobalStateProvider; export async function twoFactorServiceFactory( cache: { twoFactorService?: AbstractTwoFactorService } & CachedServices, @@ -33,6 +36,7 @@ export async function twoFactorServiceFactory( new TwoFactorService( await i18nServiceFactory(cache, opts), await platformUtilsServiceFactory(cache, opts), + await globalStateProviderFactory(cache, opts), ), ); service.init(); diff --git a/apps/browser/src/auth/popup/two-factor-options.component.ts b/apps/browser/src/auth/popup/two-factor-options.component.ts index bad2e4a9e7..6191d277ad 100644 --- a/apps/browser/src/auth/popup/two-factor-options.component.ts +++ b/apps/browser/src/auth/popup/two-factor-options.component.ts @@ -2,7 +2,10 @@ import { Component } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; import { TwoFactorOptionsComponent as BaseTwoFactorOptionsComponent } from "@bitwarden/angular/auth/components/two-factor-options.component"; -import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; +import { + TwoFactorProviderDetails, + TwoFactorService, +} from "@bitwarden/common/auth/abstractions/two-factor.service"; 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"; @@ -27,9 +30,9 @@ export class TwoFactorOptionsComponent extends BaseTwoFactorOptionsComponent { this.navigateTo2FA(); } - choose(p: any) { - super.choose(p); - this.twoFactorService.setSelectedProvider(p.type); + override async choose(p: TwoFactorProviderDetails) { + await super.choose(p); + await this.twoFactorService.setSelectedProvider(p.type); this.navigateTo2FA(); } diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index b4375df7d5..758c226bc3 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -3,8 +3,6 @@ import { Subject, firstValueFrom, merge } from "rxjs"; import { PinCryptoServiceAbstraction, PinCryptoService, - LoginStrategyServiceAbstraction, - LoginStrategyService, InternalUserDecryptionOptionsServiceAbstraction, UserDecryptionOptionsService, AuthRequestServiceAbstraction, @@ -38,7 +36,6 @@ import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarde import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service"; -import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { UserVerificationApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification-api.service.abstraction"; import { UserVerificationService as UserVerificationServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; @@ -54,7 +51,6 @@ import { KeyConnectorService } from "@bitwarden/common/auth/services/key-connect import { MasterPasswordService } from "@bitwarden/common/auth/services/master-password/master-password.service"; import { SsoLoginService } from "@bitwarden/common/auth/services/sso-login.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"; import { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.service"; import { @@ -277,7 +273,6 @@ export default class MainBackground { containerService: ContainerService; auditService: AuditServiceAbstraction; authService: AuthServiceAbstraction; - loginStrategyService: LoginStrategyServiceAbstraction; loginEmailService: LoginEmailServiceAbstraction; importApiService: ImportApiServiceAbstraction; importService: ImportServiceAbstraction; @@ -301,7 +296,6 @@ export default class MainBackground { providerService: ProviderServiceAbstraction; keyConnectorService: KeyConnectorServiceAbstraction; userVerificationService: UserVerificationServiceAbstraction; - twoFactorService: TwoFactorServiceAbstraction; vaultFilterService: VaultFilterService; usernameGenerationService: UsernameGenerationServiceAbstraction; encryptService: EncryptService; @@ -614,8 +608,6 @@ export default class MainBackground { this.stateService, ); - this.twoFactorService = new TwoFactorService(this.i18nService, this.platformUtilsService); - this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider); this.devicesApiService = new DevicesApiServiceImplementation(this.apiService); @@ -659,32 +651,6 @@ export default class MainBackground { this.loginEmailService = new LoginEmailService(this.stateProvider); - this.loginStrategyService = new LoginStrategyService( - this.accountService, - this.masterPasswordService, - this.cryptoService, - this.apiService, - this.tokenService, - this.appIdService, - this.platformUtilsService, - this.messagingService, - this.logService, - this.keyConnectorService, - this.environmentService, - this.stateService, - this.twoFactorService, - this.i18nService, - this.encryptService, - this.passwordStrengthService, - this.policyService, - this.deviceTrustService, - this.authRequestService, - this.userDecryptionOptionsService, - this.globalStateProvider, - this.billingAccountProfileStateService, - this.kdfConfigService, - ); - this.ssoLoginService = new SsoLoginService(this.stateProvider); this.userVerificationApiService = new UserVerificationApiService(this.apiService); @@ -1114,8 +1080,7 @@ export default class MainBackground { this.userKeyInitService.listenForActiveUserChangesToSetUserKey(); await (this.i18nService as I18nService).init(); - await (this.eventUploadService as EventUploadService).init(true); - this.twoFactorService.init(); + (this.eventUploadService as EventUploadService).init(true); if (this.popupOnlyContext) { return; diff --git a/apps/browser/src/popup/services/init.service.ts b/apps/browser/src/popup/services/init.service.ts index ee842565d7..63ce45c9b7 100644 --- a/apps/browser/src/popup/services/init.service.ts +++ b/apps/browser/src/popup/services/init.service.ts @@ -2,6 +2,7 @@ import { DOCUMENT } from "@angular/common"; import { Inject, Injectable } from "@angular/core"; import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction"; +import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService as LogServiceAbstraction } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -15,6 +16,7 @@ export class InitService { private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, private stateService: StateServiceAbstraction, + private twoFactorService: TwoFactorService, private logService: LogServiceAbstraction, private themingService: AbstractThemingService, @Inject(DOCUMENT) private document: Document, @@ -24,6 +26,7 @@ export class InitService { return async () => { await this.stateService.init({ runMigrations: false }); // Browser background is responsible for migrations await this.i18nService.init(); + this.twoFactorService.init(); if (!BrowserPopupUtils.inPopup(window)) { window.document.body.classList.add("body-full"); diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 38068d1849..052e341004 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -15,10 +15,7 @@ import { INTRAPROCESS_MESSAGING_SUBJECT, } from "@bitwarden/angular/services/injection-tokens"; import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module"; -import { - AuthRequestServiceAbstraction, - LoginStrategyServiceAbstraction, -} from "@bitwarden/auth/common"; +import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common"; import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service"; import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service"; import { SearchService as SearchServiceAbstraction } from "@bitwarden/common/abstractions/search.service"; @@ -33,7 +30,6 @@ import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/d import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; -import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { AuthService } from "@bitwarden/common/auth/services/auth.service"; import { @@ -168,21 +164,11 @@ const safeProviders: SafeProvider[] = [ useClass: UnauthGuardService, deps: [AuthServiceAbstraction, Router], }), - safeProvider({ - provide: TwoFactorService, - useFactory: getBgService<TwoFactorService>("twoFactorService"), - deps: [], - }), safeProvider({ provide: AuthServiceAbstraction, useFactory: getBgService<AuthService>("authService"), deps: [], }), - safeProvider({ - provide: LoginStrategyServiceAbstraction, - useFactory: getBgService<LoginStrategyServiceAbstraction>("loginStrategyService"), - deps: [], - }), safeProvider({ provide: SsoLoginServiceAbstraction, useFactory: getBgService<SsoLoginServiceAbstraction>("ssoLoginService"), diff --git a/apps/cli/src/auth/commands/login.command.ts b/apps/cli/src/auth/commands/login.command.ts index 3606285c72..bd61727a6c 100644 --- a/apps/cli/src/auth/commands/login.command.ts +++ b/apps/cli/src/auth/commands/login.command.ts @@ -231,7 +231,7 @@ export class LoginCommand { } } if (response.requiresTwoFactor) { - const twoFactorProviders = this.twoFactorService.getSupportedProviders(null); + const twoFactorProviders = await this.twoFactorService.getSupportedProviders(null); if (twoFactorProviders.length === 0) { return Response.badRequest("No providers available for this client."); } @@ -272,7 +272,7 @@ export class LoginCommand { if ( twoFactorToken == null && - response.twoFactorProviders.size > 1 && + Object.keys(response.twoFactorProviders).length > 1 && selectedProvider.type === TwoFactorProviderType.Email ) { const emailReq = new TwoFactorEmailRequest(); diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index ffe6c128b5..45c394e912 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -455,7 +455,11 @@ export class Main { this.stateProvider, ); - this.twoFactorService = new TwoFactorService(this.i18nService, this.platformUtilsService); + this.twoFactorService = new TwoFactorService( + this.i18nService, + this.platformUtilsService, + this.globalStateProvider, + ); this.passwordStrengthService = new PasswordStrengthService(); diff --git a/libs/angular/src/auth/components/sso.component.spec.ts b/libs/angular/src/auth/components/sso.component.spec.ts index 269ec51e30..ae644028f9 100644 --- a/libs/angular/src/auth/components/sso.component.spec.ts +++ b/libs/angular/src/auth/components/sso.component.spec.ts @@ -253,7 +253,7 @@ describe("SsoComponent", () => { describe("2FA scenarios", () => { beforeEach(() => { const authResult = new AuthResult(); - authResult.twoFactorProviders = new Map([[TwoFactorProviderType.Authenticator, {}]]); + authResult.twoFactorProviders = { [TwoFactorProviderType.Authenticator]: {} }; // use standard user with MP because this test is not concerned with password reset. selectedUserDecryptionOptions.next(mockUserDecryptionOpts.withMasterPassword); diff --git a/libs/angular/src/auth/components/two-factor-options.component.ts b/libs/angular/src/auth/components/two-factor-options.component.ts index 2808e41cc2..1bbf81fa34 100644 --- a/libs/angular/src/auth/components/two-factor-options.component.ts +++ b/libs/angular/src/auth/components/two-factor-options.component.ts @@ -2,7 +2,10 @@ import { Directive, EventEmitter, OnInit, Output } from "@angular/core"; import { Router } from "@angular/router"; import { firstValueFrom } from "rxjs"; -import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; +import { + TwoFactorProviderDetails, + TwoFactorService, +} from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -24,11 +27,11 @@ export class TwoFactorOptionsComponent implements OnInit { protected environmentService: EnvironmentService, ) {} - ngOnInit() { - this.providers = this.twoFactorService.getSupportedProviders(this.win); + async ngOnInit() { + this.providers = await this.twoFactorService.getSupportedProviders(this.win); } - choose(p: any) { + async choose(p: TwoFactorProviderDetails) { this.onProviderSelected.emit(p.type); } diff --git a/libs/angular/src/auth/components/two-factor.component.ts b/libs/angular/src/auth/components/two-factor.component.ts index f73f0483be..8e96c48ba0 100644 --- a/libs/angular/src/auth/components/two-factor.component.ts +++ b/libs/angular/src/auth/components/two-factor.component.ts @@ -102,7 +102,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI } async ngOnInit() { - if (!(await this.authing()) || this.twoFactorService.getProviders() == null) { + if (!(await this.authing()) || (await this.twoFactorService.getProviders()) == 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 this.router.navigate([this.loginRoute]); @@ -145,7 +145,9 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI ); } - this.selectedProviderType = this.twoFactorService.getDefaultProvider(this.webAuthnSupported); + this.selectedProviderType = await this.twoFactorService.getDefaultProvider( + this.webAuthnSupported, + ); await this.init(); } @@ -162,12 +164,14 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI this.cleanupWebAuthn(); this.title = (TwoFactorProviders as any)[this.selectedProviderType].name; - const providerData = this.twoFactorService.getProviders().get(this.selectedProviderType); + const providerData = await this.twoFactorService.getProviders().then((providers) => { + return providers.get(this.selectedProviderType); + }); switch (this.selectedProviderType) { case TwoFactorProviderType.WebAuthn: if (!this.webAuthnNewTab) { - setTimeout(() => { - this.authWebAuthn(); + setTimeout(async () => { + await this.authWebAuthn(); }, 500); } break; @@ -212,7 +216,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI break; case TwoFactorProviderType.Email: this.twoFactorEmail = providerData.Email; - if (this.twoFactorService.getProviders().size > 1) { + if ((await this.twoFactorService.getProviders()).size > 1) { await this.sendEmail(false); } break; @@ -474,8 +478,10 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI this.emailPromise = null; } - authWebAuthn() { - const providerData = this.twoFactorService.getProviders().get(this.selectedProviderType); + async authWebAuthn() { + const providerData = await this.twoFactorService.getProviders().then((providers) => { + return providers.get(this.selectedProviderType); + }); if (!this.webAuthnSupported || this.webAuthn == null) { return; diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 88494a1cbb..c7b27a25c2 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -874,7 +874,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: TwoFactorServiceAbstraction, useClass: TwoFactorService, - deps: [I18nServiceAbstraction, PlatformUtilsServiceAbstraction], + deps: [I18nServiceAbstraction, PlatformUtilsServiceAbstraction, GlobalStateProvider], }), safeProvider({ provide: FormValidationErrorsServiceAbstraction, 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 5e70c348f4..a123e30053 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 @@ -86,7 +86,9 @@ describe("AuthRequestLoginStrategy", () => { tokenService.getTwoFactorToken.mockResolvedValue(null); appIdService.getAppId.mockResolvedValue(deviceId); - tokenService.decodeAccessToken.mockResolvedValue({}); + tokenService.decodeAccessToken.mockResolvedValue({ + sub: mockUserId, + }); authRequestLoginStrategy = new AuthRequestLoginStrategy( cache, 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 7c022db23b..3284f6e947 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.spec.ts @@ -25,11 +25,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 { Utils } from "@bitwarden/common/platform/misc/utils"; -import { - Account, - AccountProfile, - AccountKeys, -} from "@bitwarden/common/platform/models/domain/account"; +import { Account, AccountProfile } 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"; @@ -214,7 +210,6 @@ describe("LoginStrategy", () => { email: email, }, }, - keys: new AccountKeys(), }), ); expect(userDecryptionOptionsService.setUserDecryptionOptions).toHaveBeenCalledWith( @@ -223,6 +218,21 @@ describe("LoginStrategy", () => { expect(messagingService.send).toHaveBeenCalledWith("loggedIn"); }); + it("throws if active account isn't found after being initialized", async () => { + const idTokenResponse = identityTokenResponseFactory(); + apiService.postIdentityToken.mockResolvedValue(idTokenResponse); + + const mockVaultTimeoutAction = VaultTimeoutAction.Lock; + const mockVaultTimeout = 1000; + + stateService.getVaultTimeoutAction.mockResolvedValue(mockVaultTimeoutAction); + stateService.getVaultTimeout.mockResolvedValue(mockVaultTimeout); + + accountService.activeAccountSubject.next(null); + + await expect(async () => await passwordLoginStrategy.logIn(credentials)).rejects.toThrow(); + }); + it("builds AuthResult", async () => { const tokenResponse = identityTokenResponseFactory(); tokenResponse.forcePasswordReset = true; @@ -306,8 +316,10 @@ describe("LoginStrategy", () => { expect(tokenService.clearTwoFactorToken).toHaveBeenCalled(); const expected = new AuthResult(); - expected.twoFactorProviders = new Map<TwoFactorProviderType, { [key: string]: string }>(); - expected.twoFactorProviders.set(0, null); + expected.twoFactorProviders = { 0: null } as Record< + TwoFactorProviderType, + Record<string, string> + >; expect(result).toEqual(expected); }); @@ -336,8 +348,9 @@ describe("LoginStrategy", () => { expect(messagingService.send).not.toHaveBeenCalled(); const expected = new AuthResult(); - expected.twoFactorProviders = new Map<TwoFactorProviderType, { [key: string]: string }>(); - expected.twoFactorProviders.set(1, { Email: "k***@bitwarden.com" }); + expected.twoFactorProviders = { + [TwoFactorProviderType.Email]: { Email: "k***@bitwarden.com" }, + }; expected.email = userEmail; expected.ssoEmail2FaSessionToken = ssoEmail2FaSessionToken; diff --git a/libs/auth/src/common/login-strategies/login.strategy.ts b/libs/auth/src/common/login-strategies/login.strategy.ts index 06fc98db13..fd268d955e 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.ts @@ -1,4 +1,4 @@ -import { BehaviorSubject } from "rxjs"; +import { BehaviorSubject, filter, firstValueFrom, timeout } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; @@ -101,7 +101,7 @@ export abstract class LoginStrategy { } protected async startLogIn(): Promise<[AuthResult, IdentityResponse]> { - this.twoFactorService.clearSelectedProvider(); + await this.twoFactorService.clearSelectedProvider(); const tokenRequest = this.cache.value.tokenRequest; const response = await this.apiService.postIdentityToken(tokenRequest); @@ -159,12 +159,12 @@ export abstract class LoginStrategy { * It also sets the access token and refresh token in the token service. * * @param {IdentityTokenResponse} tokenResponse - The response from the server containing the identity token. - * @returns {Promise<void>} - A promise that resolves when the account information has been successfully saved. + * @returns {Promise<UserId>} - A promise that resolves the the UserId when the account information has been successfully saved. */ protected async saveAccountInformation(tokenResponse: IdentityTokenResponse): Promise<UserId> { const accountInformation = await this.tokenService.decodeAccessToken(tokenResponse.accessToken); - const userId = accountInformation.sub; + const userId = accountInformation.sub as UserId; const vaultTimeoutAction = await this.stateService.getVaultTimeoutAction({ userId }); const vaultTimeout = await this.stateService.getVaultTimeout({ userId }); @@ -191,6 +191,8 @@ export abstract class LoginStrategy { }), ); + await this.verifyAccountAdded(userId); + await this.userDecryptionOptionsService.setUserDecryptionOptions( UserDecryptionOptions.fromResponse(tokenResponse), ); @@ -207,7 +209,7 @@ export abstract class LoginStrategy { ); await this.billingAccountProfileStateService.setHasPremium(accountInformation.premium, false); - return userId as UserId; + return userId; } protected async processTokenResponse(response: IdentityTokenResponse): Promise<AuthResult> { @@ -284,7 +286,7 @@ export abstract class LoginStrategy { const result = new AuthResult(); result.twoFactorProviders = response.twoFactorProviders2; - this.twoFactorService.setProviders(response); + await this.twoFactorService.setProviders(response); this.cache.next({ ...this.cache.value, captchaBypassToken: response.captchaToken ?? null }); result.ssoEmail2FaSessionToken = response.ssoEmail2faSessionToken; result.email = response.email; @@ -306,4 +308,24 @@ export abstract class LoginStrategy { result.captchaSiteKey = response.siteKey; return result; } + + /** + * Verifies that the active account is set after initialization. + * Note: In browser there is a slight delay between when active account emits in background, + * and when it emits in foreground. We're giving the foreground 1 second to catch up. + * If nothing is emitted, we throw an error. + */ + private async verifyAccountAdded(expectedUserId: UserId) { + await firstValueFrom( + this.accountService.activeAccount$.pipe( + filter((account) => account?.id === expectedUserId), + timeout({ + first: 1000, + with: () => { + throw new Error("Expected user never made active user after initialization."); + }, + }), + ), + ); + } } 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 be09448fdd..5c1fe9b1fe 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 @@ -99,7 +99,9 @@ describe("PasswordLoginStrategy", () => { kdfConfigService = mock<KdfConfigService>(); appIdService.getAppId.mockResolvedValue(deviceId); - tokenService.decodeAccessToken.mockResolvedValue({}); + tokenService.decodeAccessToken.mockResolvedValue({ + sub: userId, + }); loginStrategyService.makePreloginKey.mockResolvedValue(masterKey); 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 3439a1c199..416e910b47 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 @@ -92,7 +92,9 @@ describe("SsoLoginStrategy", () => { tokenService.getTwoFactorToken.mockResolvedValue(null); appIdService.getAppId.mockResolvedValue(deviceId); - tokenService.decodeAccessToken.mockResolvedValue({}); + tokenService.decodeAccessToken.mockResolvedValue({ + sub: userId, + }); ssoLoginStrategy = new SsoLoginStrategy( null, 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 5fce8b0b82..130e6c2d89 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 @@ -82,7 +82,9 @@ describe("UserApiLoginStrategy", () => { appIdService.getAppId.mockResolvedValue(deviceId); tokenService.getTwoFactorToken.mockResolvedValue(null); - tokenService.decodeAccessToken.mockResolvedValue({}); + tokenService.decodeAccessToken.mockResolvedValue({ + sub: userId, + }); apiLogInStrategy = new UserApiLoginStrategy( cache, 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 d75e194980..bbcd3bafdd 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 @@ -18,7 +18,8 @@ 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 { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; import { PrfKey, UserKey } from "@bitwarden/common/types/key"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; @@ -49,6 +50,7 @@ describe("WebAuthnLoginStrategy", () => { const token = "mockToken"; const deviceId = Utils.newGuid(); + const userId = Utils.newGuid() as UserId; let webAuthnCredentials!: WebAuthnLoginCredentials; @@ -69,7 +71,7 @@ describe("WebAuthnLoginStrategy", () => { beforeEach(() => { jest.clearAllMocks(); - accountService = new FakeAccountService(null); + accountService = mockAccountServiceWith(userId); masterPasswordService = new FakeMasterPasswordService(); cryptoService = mock<CryptoService>(); @@ -87,7 +89,9 @@ describe("WebAuthnLoginStrategy", () => { tokenService.getTwoFactorToken.mockResolvedValue(null); appIdService.getAppId.mockResolvedValue(deviceId); - tokenService.decodeAccessToken.mockResolvedValue({}); + tokenService.decodeAccessToken.mockResolvedValue({ + sub: userId, + }); webAuthnLoginStrategy = new WebAuthnLoginStrategy( cache, diff --git a/libs/common/src/auth/abstractions/two-factor.service.ts b/libs/common/src/auth/abstractions/two-factor.service.ts index 3ea7eb8db9..a0a9ecd2af 100644 --- a/libs/common/src/auth/abstractions/two-factor.service.ts +++ b/libs/common/src/auth/abstractions/two-factor.service.ts @@ -12,12 +12,12 @@ export interface TwoFactorProviderDetails { export abstract class TwoFactorService { init: () => void; - getSupportedProviders: (win: Window) => TwoFactorProviderDetails[]; - getDefaultProvider: (webAuthnSupported: boolean) => TwoFactorProviderType; - setSelectedProvider: (type: TwoFactorProviderType) => void; - clearSelectedProvider: () => void; + getSupportedProviders: (win: Window) => Promise<TwoFactorProviderDetails[]>; + getDefaultProvider: (webAuthnSupported: boolean) => Promise<TwoFactorProviderType>; + setSelectedProvider: (type: TwoFactorProviderType) => Promise<void>; + clearSelectedProvider: () => Promise<void>; - setProviders: (response: IdentityTwoFactorResponse) => void; - clearProviders: () => void; - getProviders: () => Map<TwoFactorProviderType, { [key: string]: string }>; + setProviders: (response: IdentityTwoFactorResponse) => Promise<void>; + clearProviders: () => Promise<void>; + getProviders: () => Promise<Map<TwoFactorProviderType, { [key: string]: string }>>; } diff --git a/libs/common/src/auth/models/domain/auth-result.ts b/libs/common/src/auth/models/domain/auth-result.ts index 993ce08d58..f45466777e 100644 --- a/libs/common/src/auth/models/domain/auth-result.ts +++ b/libs/common/src/auth/models/domain/auth-result.ts @@ -14,7 +14,7 @@ export class AuthResult { resetMasterPassword = false; forcePasswordReset: ForceSetPasswordReason = ForceSetPasswordReason.None; - twoFactorProviders: Map<TwoFactorProviderType, { [key: string]: string }> = null; + twoFactorProviders: Partial<Record<TwoFactorProviderType, Record<string, string>>> = null; ssoEmail2FaSessionToken?: string; email: string; requiresEncryptionKeyMigration: boolean; diff --git a/libs/common/src/auth/models/response/identity-two-factor.response.ts b/libs/common/src/auth/models/response/identity-two-factor.response.ts index bc5d2fbf85..dce64e8ef3 100644 --- a/libs/common/src/auth/models/response/identity-two-factor.response.ts +++ b/libs/common/src/auth/models/response/identity-two-factor.response.ts @@ -4,8 +4,10 @@ import { TwoFactorProviderType } from "../../enums/two-factor-provider-type"; import { MasterPasswordPolicyResponse } from "./master-password-policy.response"; export class IdentityTwoFactorResponse extends BaseResponse { + // contains available two-factor providers twoFactorProviders: TwoFactorProviderType[]; - twoFactorProviders2 = new Map<TwoFactorProviderType, { [key: string]: string }>(); + // a map of two-factor providers to necessary data for completion + twoFactorProviders2: Record<TwoFactorProviderType, Record<string, string>>; captchaToken: string; ssoEmail2faSessionToken: string; email?: string; @@ -15,15 +17,7 @@ export class IdentityTwoFactorResponse extends BaseResponse { super(response); this.captchaToken = this.getResponseProperty("CaptchaBypassToken"); this.twoFactorProviders = this.getResponseProperty("TwoFactorProviders"); - const twoFactorProviders2 = this.getResponseProperty("TwoFactorProviders2"); - if (twoFactorProviders2 != null) { - for (const prop in twoFactorProviders2) { - // eslint-disable-next-line - if (twoFactorProviders2.hasOwnProperty(prop)) { - this.twoFactorProviders2.set(parseInt(prop, null), twoFactorProviders2[prop]); - } - } - } + this.twoFactorProviders2 = this.getResponseProperty("TwoFactorProviders2"); this.masterPasswordPolicy = new MasterPasswordPolicyResponse( this.getResponseProperty("MasterPasswordPolicy"), ); diff --git a/libs/common/src/auth/services/two-factor.service.ts b/libs/common/src/auth/services/two-factor.service.ts index cd1e5ea122..50d2556157 100644 --- a/libs/common/src/auth/services/two-factor.service.ts +++ b/libs/common/src/auth/services/two-factor.service.ts @@ -1,5 +1,9 @@ +import { firstValueFrom, map } from "rxjs"; + import { I18nService } from "../../platform/abstractions/i18n.service"; import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service"; +import { Utils } from "../../platform/misc/utils"; +import { GlobalStateProvider, KeyDefinition, TWO_FACTOR_MEMORY } from "../../platform/state"; import { TwoFactorProviderDetails, TwoFactorService as TwoFactorServiceAbstraction, @@ -59,13 +63,36 @@ export const TwoFactorProviders: Partial<Record<TwoFactorProviderType, TwoFactor }, }; +// Memory storage as only required during authentication process +export const PROVIDERS = KeyDefinition.record<Record<string, string>, TwoFactorProviderType>( + TWO_FACTOR_MEMORY, + "providers", + { + deserializer: (obj) => obj, + }, +); + +// Memory storage as only required during authentication process +export const SELECTED_PROVIDER = new KeyDefinition<TwoFactorProviderType>( + TWO_FACTOR_MEMORY, + "selected", + { + deserializer: (obj) => obj, + }, +); + export class TwoFactorService implements TwoFactorServiceAbstraction { - private twoFactorProvidersData: Map<TwoFactorProviderType, { [key: string]: string }>; - private selectedTwoFactorProviderType: TwoFactorProviderType = null; + private providersState = this.globalStateProvider.get(PROVIDERS); + private selectedState = this.globalStateProvider.get(SELECTED_PROVIDER); + readonly providers$ = this.providersState.state$.pipe( + map((providers) => Utils.recordToMap(providers)), + ); + readonly selected$ = this.selectedState.state$; constructor( private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, + private globalStateProvider: GlobalStateProvider, ) {} init() { @@ -93,63 +120,60 @@ export class TwoFactorService implements TwoFactorServiceAbstraction { this.i18nService.t("yubiKeyDesc"); } - getSupportedProviders(win: Window): TwoFactorProviderDetails[] { + async getSupportedProviders(win: Window): Promise<TwoFactorProviderDetails[]> { + const data = await firstValueFrom(this.providers$); const providers: any[] = []; - if (this.twoFactorProvidersData == null) { + if (data == null) { return providers; } if ( - this.twoFactorProvidersData.has(TwoFactorProviderType.OrganizationDuo) && + data.has(TwoFactorProviderType.OrganizationDuo) && this.platformUtilsService.supportsDuo() ) { providers.push(TwoFactorProviders[TwoFactorProviderType.OrganizationDuo]); } - if (this.twoFactorProvidersData.has(TwoFactorProviderType.Authenticator)) { + if (data.has(TwoFactorProviderType.Authenticator)) { providers.push(TwoFactorProviders[TwoFactorProviderType.Authenticator]); } - if (this.twoFactorProvidersData.has(TwoFactorProviderType.Yubikey)) { + if (data.has(TwoFactorProviderType.Yubikey)) { providers.push(TwoFactorProviders[TwoFactorProviderType.Yubikey]); } - if ( - this.twoFactorProvidersData.has(TwoFactorProviderType.Duo) && - this.platformUtilsService.supportsDuo() - ) { + if (data.has(TwoFactorProviderType.Duo) && this.platformUtilsService.supportsDuo()) { providers.push(TwoFactorProviders[TwoFactorProviderType.Duo]); } if ( - this.twoFactorProvidersData.has(TwoFactorProviderType.WebAuthn) && + data.has(TwoFactorProviderType.WebAuthn) && this.platformUtilsService.supportsWebAuthn(win) ) { providers.push(TwoFactorProviders[TwoFactorProviderType.WebAuthn]); } - if (this.twoFactorProvidersData.has(TwoFactorProviderType.Email)) { + if (data.has(TwoFactorProviderType.Email)) { providers.push(TwoFactorProviders[TwoFactorProviderType.Email]); } return providers; } - getDefaultProvider(webAuthnSupported: boolean): TwoFactorProviderType { - if (this.twoFactorProvidersData == null) { + async getDefaultProvider(webAuthnSupported: boolean): Promise<TwoFactorProviderType> { + const data = await firstValueFrom(this.providers$); + const selected = await firstValueFrom(this.selected$); + if (data == null) { return null; } - if ( - this.selectedTwoFactorProviderType != null && - this.twoFactorProvidersData.has(this.selectedTwoFactorProviderType) - ) { - return this.selectedTwoFactorProviderType; + if (selected != null && data.has(selected)) { + return selected; } let providerType: TwoFactorProviderType = null; let providerPriority = -1; - this.twoFactorProvidersData.forEach((_value, type) => { + data.forEach((_value, type) => { const provider = (TwoFactorProviders as any)[type]; if (provider != null && provider.priority > providerPriority) { if (type === TwoFactorProviderType.WebAuthn && !webAuthnSupported) { @@ -164,23 +188,23 @@ export class TwoFactorService implements TwoFactorServiceAbstraction { return providerType; } - setSelectedProvider(type: TwoFactorProviderType) { - this.selectedTwoFactorProviderType = type; + async setSelectedProvider(type: TwoFactorProviderType): Promise<void> { + await this.selectedState.update(() => type); } - clearSelectedProvider() { - this.selectedTwoFactorProviderType = null; + async clearSelectedProvider(): Promise<void> { + await this.selectedState.update(() => null); } - setProviders(response: IdentityTwoFactorResponse) { - this.twoFactorProvidersData = response.twoFactorProviders2; + async setProviders(response: IdentityTwoFactorResponse): Promise<void> { + await this.providersState.update(() => response.twoFactorProviders2); } - clearProviders() { - this.twoFactorProvidersData = null; + async clearProviders(): Promise<void> { + await this.providersState.update(() => null); } - getProviders() { - return this.twoFactorProvidersData; + getProviders(): Promise<Map<TwoFactorProviderType, { [key: string]: string }>> { + return firstValueFrom(this.providers$); } } diff --git a/libs/common/src/platform/state/deserialization-helpers.spec.ts b/libs/common/src/platform/state/deserialization-helpers.spec.ts new file mode 100644 index 0000000000..b1ae447997 --- /dev/null +++ b/libs/common/src/platform/state/deserialization-helpers.spec.ts @@ -0,0 +1,25 @@ +import { record } from "./deserialization-helpers"; + +describe("deserialization helpers", () => { + describe("record", () => { + it("deserializes a record when keys are strings", () => { + const deserializer = record((value: number) => value); + const input = { + a: 1, + b: 2, + }; + const output = deserializer(input); + expect(output).toEqual(input); + }); + + it("deserializes a record when keys are numbers", () => { + const deserializer = record((value: number) => value); + const input = { + 1: 1, + 2: 2, + }; + const output = deserializer(input); + expect(output).toEqual(input); + }); + }); +}); diff --git a/libs/common/src/platform/state/deserialization-helpers.ts b/libs/common/src/platform/state/deserialization-helpers.ts index d68a3d0444..8fd5d2da19 100644 --- a/libs/common/src/platform/state/deserialization-helpers.ts +++ b/libs/common/src/platform/state/deserialization-helpers.ts @@ -21,7 +21,7 @@ export function array<T>( * * @param valueDeserializer */ -export function record<T, TKey extends string = string>( +export function record<T, TKey extends string | number = string>( valueDeserializer: (value: Jsonify<T>) => T, ): (record: Jsonify<Record<TKey, T>>) => Record<TKey, T> { return (jsonValue: Jsonify<Record<TKey, T> | null>) => { @@ -29,10 +29,10 @@ export function record<T, TKey extends string = string>( return null; } - const output: Record<string, T> = {}; - for (const key in jsonValue) { - output[key] = valueDeserializer((jsonValue as Record<string, Jsonify<T>>)[key]); - } + const output: Record<TKey, T> = {} as any; + Object.entries(jsonValue).forEach(([key, value]) => { + output[key as TKey] = valueDeserializer(value); + }); return output; }; } diff --git a/libs/common/src/platform/state/key-definition.ts b/libs/common/src/platform/state/key-definition.ts index b2a8ff8712..bdabd8df50 100644 --- a/libs/common/src/platform/state/key-definition.ts +++ b/libs/common/src/platform/state/key-definition.ts @@ -113,7 +113,7 @@ export class KeyDefinition<T> { * }); * ``` */ - static record<T, TKey extends string = string>( + static record<T, TKey extends string | number = string>( stateDefinition: StateDefinition, key: string, // We have them provide options for the value of the record, depending on future options we add, this could get a little weird. diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index 8847f7fe51..ee5005202f 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -40,6 +40,7 @@ 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 TWO_FACTOR_MEMORY = new StateDefinition("twoFactor", "memory"); 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", { diff --git a/libs/common/src/platform/state/user-key-definition.ts b/libs/common/src/platform/state/user-key-definition.ts index 3eb9369080..4c622e29f1 100644 --- a/libs/common/src/platform/state/user-key-definition.ts +++ b/libs/common/src/platform/state/user-key-definition.ts @@ -120,7 +120,7 @@ export class UserKeyDefinition<T> { * }); * ``` */ - static record<T, TKey extends string = string>( + static record<T, TKey extends string | number = string>( stateDefinition: StateDefinition, key: string, // We have them provide options for the value of the record, depending on future options we add, this could get a little weird. From d8749a0c56c5266ff0e64536ca859f9f87c95b28 Mon Sep 17 00:00:00 2001 From: SmithThe4th <gsmith@bitwarden.com> Date: Thu, 25 Apr 2024 16:58:25 -0400 Subject: [PATCH 284/351] [AC-2359] Ownership does not default to an organization when Remove Individual Vault policy is active (#8910) * fixed issue with clearing search index state * clear user index before account is totally cleaned up * added logout clear on option * removed redundant clear index from logout * fixed ownsership dropdown issu where async operations does bot complete early enough before the view is shown --- libs/angular/src/vault/components/add-edit.component.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts index d29c74b42d..d9b73a0e7f 100644 --- a/libs/angular/src/vault/components/add-edit.component.ts +++ b/libs/angular/src/vault/components/add-edit.component.ts @@ -184,8 +184,6 @@ export class AddEditComponent implements OnInit, OnDestroy { FeatureFlag.FlexibleCollectionsV1, false, ); - this.writeableCollections = await this.loadCollections(); - this.canUseReprompt = await this.passwordRepromptService.enabled(); this.policyService .policyAppliesToActiveUser$(PolicyType.PersonalOwnership) @@ -197,6 +195,9 @@ export class AddEditComponent implements OnInit, OnDestroy { takeUntil(this.destroy$), ) .subscribe(); + + this.writeableCollections = await this.loadCollections(); + this.canUseReprompt = await this.passwordRepromptService.enabled(); } ngOnDestroy() { From 2ff3fa92fb6c9f56a738c057c9d70272167b1d67 Mon Sep 17 00:00:00 2001 From: Jake Fink <jfink@bitwarden.com> Date: Thu, 25 Apr 2024 17:27:43 -0400 Subject: [PATCH 285/351] [PM-7702] Remove extra content script being injected (#8922) * remove extra content script being injected that results in multiple messages * add conditional logic for when to add script --- apps/browser/src/autofill/services/autofill.service.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/browser/src/autofill/services/autofill.service.ts b/apps/browser/src/autofill/services/autofill.service.ts index 8f85d65692..10e2d84361 100644 --- a/apps/browser/src/autofill/services/autofill.service.ts +++ b/apps/browser/src/autofill/services/autofill.service.ts @@ -130,7 +130,9 @@ export default class AutofillService implements AutofillServiceInterface { if (triggeringOnPageLoad && autoFillOnPageLoadIsEnabled) { injectedScripts.push("autofiller.js"); - } else { + } + + if (!triggeringOnPageLoad) { await this.scriptInjectorService.inject({ tabId: tab.id, injectDetails: { file: "content/content-message-handler.js", runAt: "document_start" }, From c3d4c7aa3d27e75a7e94a44f40fc9916850fa45f Mon Sep 17 00:00:00 2001 From: Cesar Gonzalez <cesar.a.gonzalezcs@gmail.com> Date: Thu, 25 Apr 2024 16:47:20 -0500 Subject: [PATCH 286/351] [PM-7710] Avoid re-indexing ciphers on current tab component and re-setting null storage values for popup components (#8908) * [PM-7710] Avoid re-indexing ciphers on current tab component and re-setting null storage values for popup components * [PM-7710] Avoid re-indexing ciphers on current tab component and re-setting null storage values for popup components --- .../tools/popup/services/browser-send-state.service.ts | 8 ++++++-- .../popup/components/vault/current-tab.component.ts | 2 ++ .../src/vault/services/vault-browser-state.service.ts | 8 ++++++-- libs/common/src/vault/abstractions/cipher.service.ts | 1 + libs/common/src/vault/services/cipher.service.ts | 9 +++++++-- 5 files changed, 22 insertions(+), 6 deletions(-) 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 index 52aeb01a92..11e71c9b20 100644 --- a/apps/browser/src/tools/popup/services/browser-send-state.service.ts +++ b/apps/browser/src/tools/popup/services/browser-send-state.service.ts @@ -46,7 +46,9 @@ export class BrowserSendStateService { * the send component on the browser */ async setBrowserSendComponentState(value: BrowserSendComponentState): Promise<void> { - await this.activeUserBrowserSendComponentState.update(() => value); + await this.activeUserBrowserSendComponentState.update(() => value, { + shouldUpdate: (current) => !(current == null && value == null), + }); } /** Get the active user's browser component state @@ -60,6 +62,8 @@ export class BrowserSendStateService { * @param { BrowserComponentState } value set the scroll position and search text for the send component on the browser */ async setBrowserSendTypeComponentState(value: BrowserComponentState): Promise<void> { - await this.activeUserBrowserSendTypeComponentState.update(() => value); + await this.activeUserBrowserSendTypeComponentState.update(() => value, { + shouldUpdate: (current) => !(current == null && value == null), + }); } } 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 4d2674fd70..d882dfd525 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 @@ -292,6 +292,8 @@ export class CurrentTabComponent implements OnInit, OnDestroy { const ciphers = await this.cipherService.getAllDecryptedForUrl( this.url, otherTypes.length > 0 ? otherTypes : null, + null, + false, ); this.loginCiphers = []; diff --git a/apps/browser/src/vault/services/vault-browser-state.service.ts b/apps/browser/src/vault/services/vault-browser-state.service.ts index a0d55a9d55..43a28928da 100644 --- a/apps/browser/src/vault/services/vault-browser-state.service.ts +++ b/apps/browser/src/vault/services/vault-browser-state.service.ts @@ -52,7 +52,9 @@ export class VaultBrowserStateService { } async setBrowserGroupingsComponentState(value: BrowserGroupingsComponentState): Promise<void> { - await this.activeUserVaultBrowserGroupingsComponentState.update(() => value); + await this.activeUserVaultBrowserGroupingsComponentState.update(() => value, { + shouldUpdate: (current) => !(current == null && value == null), + }); } async getBrowserVaultItemsComponentState(): Promise<BrowserComponentState> { @@ -60,6 +62,8 @@ export class VaultBrowserStateService { } async setBrowserVaultItemsComponentState(value: BrowserComponentState): Promise<void> { - await this.activeUserVaultBrowserComponentState.update(() => value); + await this.activeUserVaultBrowserComponentState.update(() => value, { + shouldUpdate: (current) => !(current == null && value == null), + }); } } diff --git a/libs/common/src/vault/abstractions/cipher.service.ts b/libs/common/src/vault/abstractions/cipher.service.ts index 22e2c54a59..4337043cdf 100644 --- a/libs/common/src/vault/abstractions/cipher.service.ts +++ b/libs/common/src/vault/abstractions/cipher.service.ts @@ -33,6 +33,7 @@ export abstract class CipherService { url: string, includeOtherTypes?: CipherType[], defaultMatch?: UriMatchStrategySetting, + reindexCiphers?: boolean, ) => Promise<CipherView[]>; getAllFromApiForOrganization: (organizationId: string) => Promise<CipherView[]>; /** diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 0b44636ea6..fd484ee902 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -441,6 +441,7 @@ export class CipherService implements CipherServiceAbstraction { url: string, includeOtherTypes?: CipherType[], defaultMatch: UriMatchStrategySetting = null, + reindexCiphers = true, ): Promise<CipherView[]> { if (url == null && includeOtherTypes == null) { return Promise.resolve([]); @@ -449,7 +450,9 @@ export class CipherService implements CipherServiceAbstraction { const equivalentDomains = await firstValueFrom( this.domainSettingsService.getUrlEquivalentDomains(url), ); - const ciphers = await this.getAllDecrypted(); + const ciphers = reindexCiphers + ? await this.getAllDecrypted() + : await this.getDecryptedCiphers(); defaultMatch ??= await firstValueFrom(this.domainSettingsService.defaultUriMatchStrategy$); return ciphers.filter((cipher) => { @@ -1135,7 +1138,9 @@ export class CipherService implements CipherServiceAbstraction { } async setAddEditCipherInfo(value: AddEditCipherInfo) { - await this.addEditCipherInfoState.update(() => value); + await this.addEditCipherInfoState.update(() => value, { + shouldUpdate: (current) => !(current == null && value == null), + }); } // Helpers From c21a58f2fb5f7ca69eb6c49556d2f1151dde3b0e Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Fri, 26 Apr 2024 08:36:57 +1000 Subject: [PATCH 287/351] Use refCount: true to avoid potential memory leak (#8796) --- .../organizations/manage/group-add-edit.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts index dea6f4999b..b18effac86 100644 --- a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts @@ -194,7 +194,7 @@ export class GroupAddEditComponent implements OnInit, OnDestroy { }), ); }), - shareReplay({ refCount: false }), + shareReplay({ refCount: true, bufferSize: 1 }), ); restrictGroupAccess$ = combineLatest([ From 788bef6b7a35f0a1e24fa086fddcac94d293f06e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 26 Apr 2024 07:04:21 +0000 Subject: [PATCH 288/351] Autosync the updated translations (#8924) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/desktop/src/locales/fr/messages.json | 4 ++-- apps/desktop/src/locales/hu/messages.json | 2 +- apps/desktop/src/locales/lv/messages.json | 2 +- apps/desktop/src/locales/nl/messages.json | 2 +- apps/desktop/src/locales/zh_CN/messages.json | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/locales/fr/messages.json b/apps/desktop/src/locales/fr/messages.json index 86550b736f..e82420a1f5 100644 --- a/apps/desktop/src/locales/fr/messages.json +++ b/apps/desktop/src/locales/fr/messages.json @@ -1636,7 +1636,7 @@ "message": "Error enabling browser integration" }, "browserIntegrationErrorDesc": { - "message": "An error has occurred while enabling browser integration." + "message": "Une erreur s'est produite lors de l'action de l'intégration du navigateur." }, "browserIntegrationMasOnlyDesc": { "message": "Malheureusement l'intégration avec le navigateur est uniquement supportée dans la version Mac App Store pour le moment." @@ -2698,7 +2698,7 @@ "description": "Label indicating the most common import formats" }, "success": { - "message": "Success" + "message": "Succès" }, "troubleshooting": { "message": "Résolution de problèmes" diff --git a/apps/desktop/src/locales/hu/messages.json b/apps/desktop/src/locales/hu/messages.json index 5c91fb4b94..838b3fc7c8 100644 --- a/apps/desktop/src/locales/hu/messages.json +++ b/apps/desktop/src/locales/hu/messages.json @@ -2698,7 +2698,7 @@ "description": "Label indicating the most common import formats" }, "success": { - "message": "Success" + "message": "Sikeres" }, "troubleshooting": { "message": "Hibaelhárítás" diff --git a/apps/desktop/src/locales/lv/messages.json b/apps/desktop/src/locales/lv/messages.json index aa057f54ab..e2e068362e 100644 --- a/apps/desktop/src/locales/lv/messages.json +++ b/apps/desktop/src/locales/lv/messages.json @@ -2698,7 +2698,7 @@ "description": "Label indicating the most common import formats" }, "success": { - "message": "Success" + "message": "Izdevās" }, "troubleshooting": { "message": "Sarežģījumu novēršana" diff --git a/apps/desktop/src/locales/nl/messages.json b/apps/desktop/src/locales/nl/messages.json index b5f2a413d6..f56572259b 100644 --- a/apps/desktop/src/locales/nl/messages.json +++ b/apps/desktop/src/locales/nl/messages.json @@ -2698,7 +2698,7 @@ "description": "Label indicating the most common import formats" }, "success": { - "message": "Success" + "message": "Succes" }, "troubleshooting": { "message": "Probleemoplossing" diff --git a/apps/desktop/src/locales/zh_CN/messages.json b/apps/desktop/src/locales/zh_CN/messages.json index 9837be29e3..aad13e06ef 100644 --- a/apps/desktop/src/locales/zh_CN/messages.json +++ b/apps/desktop/src/locales/zh_CN/messages.json @@ -2698,7 +2698,7 @@ "description": "Label indicating the most common import formats" }, "success": { - "message": "Success" + "message": "成功" }, "troubleshooting": { "message": "故障排除" From c7fa376be36a865b5ed9a6d758e9a80ce07a0ca8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 26 Apr 2024 07:05:43 +0000 Subject: [PATCH 289/351] Autosync the updated translations (#8926) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/web/src/locales/ca/messages.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/web/src/locales/ca/messages.json b/apps/web/src/locales/ca/messages.json index bf3071b506..0f958846cd 100644 --- a/apps/web/src/locales/ca/messages.json +++ b/apps/web/src/locales/ca/messages.json @@ -7748,31 +7748,31 @@ "description": "A machine user which can be used to automate processes and access secrets in the system." }, "machineAccounts": { - "message": "Machine accounts", + "message": "Compte de màquina", "description": "The title for the section that deals with machine accounts." }, "newMachineAccount": { - "message": "New machine account", + "message": "Compte nou de màquina", "description": "Title for creating a new machine account." }, "machineAccountsNoItemsMessage": { - "message": "Create a new machine account to get started automating secret access.", + "message": "Crea un compte de màquina nou per començar a automatitzar l'accés secret.", "description": "Message to encourage the user to start creating machine accounts." }, "machineAccountsNoItemsTitle": { - "message": "Nothing to show yet", + "message": "Encara no hi ha res a mostrar", "description": "Title to indicate that there are no machine accounts to display." }, "deleteMachineAccounts": { - "message": "Delete machine accounts", + "message": "Suprimeix els comptes de màquina", "description": "Title for the action to delete one or multiple machine accounts." }, "deleteMachineAccount": { - "message": "Delete machine account", + "message": "Suprimeix comptes de màquina", "description": "Title for the action to delete a single machine account." }, "viewMachineAccount": { - "message": "View machine account", + "message": "Veure el compte de màquina", "description": "Action to view the details of a machine account." }, "deleteMachineAccountDialogMessage": { From 14b2eb99a2baa74279036b412ad174b31bdc8ef8 Mon Sep 17 00:00:00 2001 From: Oscar Hinton <Hinton@users.noreply.github.com> Date: Fri, 26 Apr 2024 12:57:26 +0200 Subject: [PATCH 290/351] [PM-2282] Make feature flags type safe (#8612) Refactors the feature flags in ConfigService to be type safe. It also moves the default value to a centralized location rather than the caller defining it. This ensures consistency across the various places they are used. --- .../fileless-importer.background.ts | 2 +- .../layouts/organization-layout.component.ts | 1 - .../member-dialog/member-dialog.component.ts | 1 - .../settings/account.component.ts | 2 -- .../key-rotation/user-key-rotation.service.ts | 2 +- ...ganization-subscription-cloud.component.ts | 1 - .../src/app/layouts/user-layout.component.ts | 1 - .../collection-dialog.component.ts | 2 +- .../bulk-delete-dialog.component.ts | 1 - .../vault-onboarding.component.ts | 3 +- .../vault/individual-vault/vault.component.ts | 1 - .../vault/org-vault/attachments.component.ts | 2 +- ...-collection-assignment-dialog.component.ts | 5 +-- .../app/vault/org-vault/vault.component.ts | 1 - .../providers/clients/clients.component.ts | 1 - .../providers/providers-layout.component.ts | 2 -- .../providers/settings/account.component.ts | 1 - .../providers/setup/setup.component.ts | 2 -- .../directives/if-feature.directive.spec.ts | 8 ++--- .../src/directives/if-feature.directive.ts | 4 +-- .../platform/guard/feature-flag.guard.spec.ts | 8 ++--- .../vault/components/add-edit.component.ts | 1 - libs/common/src/enums/feature-flag.enum.ts | 36 +++++++++++++++++-- .../abstractions/config/config.service.ts | 14 ++------ .../abstractions/config/server-config.ts | 3 +- .../models/data/server-config.data.ts | 3 +- .../services/config/default-config.service.ts | 17 +++++---- 27 files changed, 67 insertions(+), 58 deletions(-) diff --git a/apps/browser/src/tools/background/fileless-importer.background.ts b/apps/browser/src/tools/background/fileless-importer.background.ts index 07c6408e8d..fed5541f52 100644 --- a/apps/browser/src/tools/background/fileless-importer.background.ts +++ b/apps/browser/src/tools/background/fileless-importer.background.ts @@ -183,7 +183,7 @@ class FilelessImporterBackground implements FilelessImporterBackgroundInterface return; } - const filelessImportFeatureFlagEnabled = await this.configService.getFeatureFlag<boolean>( + const filelessImportFeatureFlagEnabled = await this.configService.getFeatureFlag( FeatureFlag.BrowserFilelessImport, ); const userAuthStatus = await this.authService.getAuthStatus(); 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 b1a84c22f3..7de0c98cd5 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 @@ -58,7 +58,6 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy { protected showPaymentMethodWarningBanners$ = this.configService.getFeatureFlag$( FeatureFlag.ShowPaymentMethodWarningBanners, - false, ); constructor( 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 f1af950650..3cccd6e28f 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 @@ -218,7 +218,6 @@ export class MemberDialogComponent implements OnDestroy { groups: groups$, flexibleCollectionsV1Enabled: this.configService.getFeatureFlag$( FeatureFlag.FlexibleCollectionsV1, - false, ), }) .pipe(takeUntil(this.destroy$)) 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 1ce05f7a30..d8091e46ae 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 @@ -44,12 +44,10 @@ export class AccountComponent { protected flexibleCollectionsMigrationEnabled$ = this.configService.getFeatureFlag$( FeatureFlag.FlexibleCollectionsMigration, - false, ); flexibleCollectionsV1Enabled$ = this.configService.getFeatureFlag$( FeatureFlag.FlexibleCollectionsV1, - false, ); // FormGroup validators taken from server Organization domain object 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 94c6208115..dc5f933724 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 @@ -90,7 +90,7 @@ export class UserKeyRotationService { request.emergencyAccessKeys = await this.emergencyAccessService.getRotatedKeys(newUserKey); request.resetPasswordKeys = await this.resetPasswordService.getRotatedKeys(newUserKey); - if (await this.configService.getFeatureFlag<boolean>(FeatureFlag.KeyRotationImprovements)) { + if (await this.configService.getFeatureFlag(FeatureFlag.KeyRotationImprovements)) { await this.apiService.postUserKeyUpdate(request); } else { await this.rotateUserKeyAndEncryptedDataLegacy(request); 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 9326359bd8..477032deba 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 @@ -84,7 +84,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy this.showUpdatedSubscriptionStatusSection$ = this.configService.getFeatureFlag$( FeatureFlag.AC1795_UpdatedSubscriptionStatusSection, - false, ); } diff --git a/apps/web/src/app/layouts/user-layout.component.ts b/apps/web/src/app/layouts/user-layout.component.ts index fea0352867..eb507bd997 100644 --- a/apps/web/src/app/layouts/user-layout.component.ts +++ b/apps/web/src/app/layouts/user-layout.component.ts @@ -40,7 +40,6 @@ export class UserLayoutComponent implements OnInit { protected showPaymentMethodWarningBanners$ = this.configService.getFeatureFlag$( FeatureFlag.ShowPaymentMethodWarningBanners, - false, ); constructor( diff --git a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts index 8e0d610c93..4e95bb4bcc 100644 --- a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts +++ b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts @@ -76,7 +76,7 @@ export enum CollectionDialogAction { }) export class CollectionDialogComponent implements OnInit, OnDestroy { protected flexibleCollectionsV1Enabled$ = this.configService - .getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1, false) + .getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1) .pipe(first()); private destroy$ = new Subject<void>(); diff --git a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts index 4050823a6d..a678a05ae3 100644 --- a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts +++ b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts @@ -54,7 +54,6 @@ export class BulkDeleteDialogComponent { private flexibleCollectionsV1Enabled$ = this.configService.getFeatureFlag$( FeatureFlag.FlexibleCollectionsV1, - false, ); constructor( diff --git a/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts b/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts index dc3a41cf15..90af89e60e 100644 --- a/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts @@ -60,9 +60,8 @@ export class VaultOnboardingComponent implements OnInit, OnChanges, OnDestroy { ) {} async ngOnInit() { - this.showOnboardingAccess$ = await this.configService.getFeatureFlag$<boolean>( + this.showOnboardingAccess$ = await this.configService.getFeatureFlag$( FeatureFlag.VaultOnboarding, - false, ); this.onboardingTasks$ = this.vaultOnboardingService.vaultOnboardingState$; await this.setOnboardingTasks(); diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index c97dd93d76..b956a90445 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -148,7 +148,6 @@ export class VaultComponent implements OnInit, OnDestroy { protected currentSearchText$: Observable<string>; protected flexibleCollectionsV1Enabled$ = this.configService.getFeatureFlag$( FeatureFlag.FlexibleCollectionsV1, - false, ); private searchText$ = new Subject<string>(); diff --git a/apps/web/src/app/vault/org-vault/attachments.component.ts b/apps/web/src/app/vault/org-vault/attachments.component.ts index cf1f0796ec..2aecf277e6 100644 --- a/apps/web/src/app/vault/org-vault/attachments.component.ts +++ b/apps/web/src/app/vault/org-vault/attachments.component.ts @@ -60,7 +60,7 @@ export class AttachmentsComponent extends BaseAttachmentsComponent implements On async ngOnInit() { await super.ngOnInit(); this.flexibleCollectionsV1Enabled = await firstValueFrom( - this.configService.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1, false), + this.configService.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1), ); } diff --git a/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/bulk-collection-assignment-dialog.component.ts b/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/bulk-collection-assignment-dialog.component.ts index 091c646178..e9f8401d73 100644 --- a/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/bulk-collection-assignment-dialog.component.ts +++ b/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/bulk-collection-assignment-dialog.component.ts @@ -70,10 +70,7 @@ export class BulkCollectionAssignmentDialogComponent implements OnDestroy, OnIni ) {} async ngOnInit() { - const v1FCEnabled = await this.configService.getFeatureFlag( - FeatureFlag.FlexibleCollectionsV1, - false, - ); + const v1FCEnabled = await this.configService.getFeatureFlag(FeatureFlag.FlexibleCollectionsV1); const org = await this.organizationService.get(this.params.organizationId); if (org.canEditAllCiphers(v1FCEnabled)) { diff --git a/apps/web/src/app/vault/org-vault/vault.component.ts b/apps/web/src/app/vault/org-vault/vault.component.ts index 9de404e969..243dedef93 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.ts +++ b/apps/web/src/app/vault/org-vault/vault.component.ts @@ -193,7 +193,6 @@ export class VaultComponent implements OnInit, OnDestroy { this._flexibleCollectionsV1FlagEnabled = await this.configService.getFeatureFlag( FeatureFlag.FlexibleCollectionsV1, - false, ); const filter$ = this.routedVaultFilterService.filter$; diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts index 54e264c666..6875c3816b 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts @@ -42,7 +42,6 @@ export class ClientsComponent extends BaseClientsComponent { protected consolidatedBillingEnabled$ = this.configService.getFeatureFlag$( FeatureFlag.EnableConsolidatedBilling, - false, ); constructor( diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.ts index c78bf476c0..8dbb653401 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.ts @@ -37,12 +37,10 @@ export class ProvidersLayoutComponent { protected showPaymentMethodWarningBanners$ = this.configService.getFeatureFlag$( FeatureFlag.ShowPaymentMethodWarningBanners, - false, ); protected enableConsolidatedBilling$ = this.configService.getFeatureFlag$( FeatureFlag.EnableConsolidatedBilling, - false, ); constructor( diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts index 83038d1bfc..70eb8af7ba 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts @@ -30,7 +30,6 @@ export class AccountComponent { protected enableDeleteProvider$ = this.configService.getFeatureFlag$( FeatureFlag.EnableDeleteProvider, - false, ); constructor( 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 cf9af4f68a..258088257d 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 @@ -36,12 +36,10 @@ export class SetupComponent implements OnInit { protected showPaymentMethodWarningBanners$ = this.configService.getFeatureFlag$( FeatureFlag.ShowPaymentMethodWarningBanners, - false, ); protected enableConsolidatedBilling$ = this.configService.getFeatureFlag$( FeatureFlag.EnableConsolidatedBilling, - false, ); constructor( diff --git a/libs/angular/src/directives/if-feature.directive.spec.ts b/libs/angular/src/directives/if-feature.directive.spec.ts index 944410be7d..456220b791 100644 --- a/libs/angular/src/directives/if-feature.directive.spec.ts +++ b/libs/angular/src/directives/if-feature.directive.spec.ts @@ -3,7 +3,7 @@ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { By } from "@angular/platform-browser"; import { mock, MockProxy } from "jest-mock-extended"; -import { FeatureFlag, FeatureFlagValue } from "@bitwarden/common/enums/feature-flag.enum"; +import { AllowedFeatureFlagTypes, FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -41,10 +41,8 @@ describe("IfFeatureDirective", () => { let content: HTMLElement; let mockConfigService: MockProxy<ConfigService>; - const mockConfigFlagValue = (flag: FeatureFlag, flagValue: FeatureFlagValue) => { - mockConfigService.getFeatureFlag.mockImplementation((f, defaultValue) => - flag == f ? Promise.resolve(flagValue) : Promise.resolve(defaultValue), - ); + const mockConfigFlagValue = (flag: FeatureFlag, flagValue: AllowedFeatureFlagTypes) => { + mockConfigService.getFeatureFlag.mockImplementation((f) => Promise.resolve(flagValue as any)); }; const queryContent = (testId: string) => diff --git a/libs/angular/src/directives/if-feature.directive.ts b/libs/angular/src/directives/if-feature.directive.ts index 069f306a89..838bd264ad 100644 --- a/libs/angular/src/directives/if-feature.directive.ts +++ b/libs/angular/src/directives/if-feature.directive.ts @@ -1,6 +1,6 @@ import { Directive, Input, OnInit, TemplateRef, ViewContainerRef } from "@angular/core"; -import { FeatureFlag, FeatureFlagValue } from "@bitwarden/common/enums/feature-flag.enum"; +import { AllowedFeatureFlagTypes, FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -23,7 +23,7 @@ export class IfFeatureDirective implements OnInit { * Optional value to compare against the value of the feature flag in the config service. * @default true */ - @Input() appIfFeatureValue: FeatureFlagValue = true; + @Input() appIfFeatureValue: AllowedFeatureFlagTypes = true; private hasView = false; 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 88637dff97..323e8c2659 100644 --- a/libs/angular/src/platform/guard/feature-flag.guard.spec.ts +++ b/libs/angular/src/platform/guard/feature-flag.guard.spec.ts @@ -34,12 +34,12 @@ describe("canAccessFeature", () => { flag == testFlag ? Promise.resolve(flagValue) : Promise.resolve(defaultValue), ); } else if (typeof flagValue === "string") { - mockConfigService.getFeatureFlag.mockImplementation((flag, defaultValue = "") => - flag == testFlag ? Promise.resolve(flagValue) : Promise.resolve(defaultValue), + mockConfigService.getFeatureFlag.mockImplementation((flag) => + flag == testFlag ? Promise.resolve(flagValue as any) : Promise.resolve(""), ); } else if (typeof flagValue === "number") { - mockConfigService.getFeatureFlag.mockImplementation((flag, defaultValue = 0) => - flag == testFlag ? Promise.resolve(flagValue) : Promise.resolve(defaultValue), + mockConfigService.getFeatureFlag.mockImplementation((flag) => + flag == testFlag ? Promise.resolve(flagValue as any) : Promise.resolve(0), ); } diff --git a/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts index d9b73a0e7f..177b4289f4 100644 --- a/libs/angular/src/vault/components/add-edit.component.ts +++ b/libs/angular/src/vault/components/add-edit.component.ts @@ -182,7 +182,6 @@ export class AddEditComponent implements OnInit, OnDestroy { async ngOnInit() { this.flexibleCollectionsV1Enabled = await this.configService.getFeatureFlag( FeatureFlag.FlexibleCollectionsV1, - false, ); this.policyService diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 636e9bc4ce..d84494362e 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -1,3 +1,8 @@ +/** + * Feature flags. + * + * Flags MUST be short lived and SHALL be removed once enabled. + */ export enum FeatureFlag { BrowserFilelessImport = "browser-fileless-import", ItemShare = "item-share", @@ -13,5 +18,32 @@ export enum FeatureFlag { EnableDeleteProvider = "AC-1218-delete-provider", } -// Replace this with a type safe lookup of the feature flag values in PM-2282 -export type FeatureFlagValue = number | string | boolean; +export type AllowedFeatureFlagTypes = boolean | number | string; + +// Helper to ensure the value is treated as a boolean. +const FALSE = false as boolean; + +/** + * Default value for feature flags. + * + * DO NOT enable previously disabled flags, REMOVE them instead. + * We support true as a value as we prefer flags to "enable" not "disable". + */ +export const DefaultFeatureFlagValue = { + [FeatureFlag.BrowserFilelessImport]: FALSE, + [FeatureFlag.ItemShare]: FALSE, + [FeatureFlag.FlexibleCollectionsV1]: FALSE, + [FeatureFlag.VaultOnboarding]: FALSE, + [FeatureFlag.GeneratorToolsModernization]: FALSE, + [FeatureFlag.KeyRotationImprovements]: FALSE, + [FeatureFlag.FlexibleCollectionsMigration]: FALSE, + [FeatureFlag.ShowPaymentMethodWarningBanners]: FALSE, + [FeatureFlag.EnableConsolidatedBilling]: FALSE, + [FeatureFlag.AC1795_UpdatedSubscriptionStatusSection]: FALSE, + [FeatureFlag.UnassignedItemsBanner]: FALSE, + [FeatureFlag.EnableDeleteProvider]: FALSE, +} satisfies Record<FeatureFlag, AllowedFeatureFlagTypes>; + +export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue; + +export type FeatureFlagValueType<Flag extends FeatureFlag> = DefaultFeatureFlagValueType[Flag]; diff --git a/libs/common/src/platform/abstractions/config/config.service.ts b/libs/common/src/platform/abstractions/config/config.service.ts index 9eca5891ac..6985430acc 100644 --- a/libs/common/src/platform/abstractions/config/config.service.ts +++ b/libs/common/src/platform/abstractions/config/config.service.ts @@ -1,7 +1,7 @@ import { Observable } from "rxjs"; import { SemVer } from "semver"; -import { FeatureFlag } from "../../../enums/feature-flag.enum"; +import { FeatureFlag, FeatureFlagValueType } from "../../../enums/feature-flag.enum"; import { Region } from "../environment.service"; import { ServerConfig } from "./server-config"; @@ -14,23 +14,15 @@ export abstract class ConfigService { /** * 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$: <T extends boolean | number | string>( - key: FeatureFlag, - defaultValue?: T, - ) => Observable<T>; + getFeatureFlag$: <Flag extends FeatureFlag>(key: Flag) => Observable<FeatureFlagValueType<Flag>>; /** * 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: <T extends boolean | number | string>( - key: FeatureFlag, - defaultValue?: T, - ) => Promise<T>; + getFeatureFlag: <Flag extends FeatureFlag>(key: Flag) => Promise<FeatureFlagValueType<Flag>>; /** * Verifies whether the server version meets the minimum required version * @param minimumRequiredServerVersion The minimum version required diff --git a/libs/common/src/platform/abstractions/config/server-config.ts b/libs/common/src/platform/abstractions/config/server-config.ts index 287e359f18..bb18605964 100644 --- a/libs/common/src/platform/abstractions/config/server-config.ts +++ b/libs/common/src/platform/abstractions/config/server-config.ts @@ -1,5 +1,6 @@ import { Jsonify } from "type-fest"; +import { AllowedFeatureFlagTypes } from "../../../enums/feature-flag.enum"; import { ServerConfigData, ThirdPartyServerConfigData, @@ -14,7 +15,7 @@ export class ServerConfig { server?: ThirdPartyServerConfigData; environment?: EnvironmentServerConfigData; utcDate: Date; - featureStates: { [key: string]: string } = {}; + featureStates: { [key: string]: AllowedFeatureFlagTypes } = {}; constructor(serverConfigData: ServerConfigData) { this.version = serverConfigData.version; diff --git a/libs/common/src/platform/models/data/server-config.data.ts b/libs/common/src/platform/models/data/server-config.data.ts index a4819f7567..57e8fbc628 100644 --- a/libs/common/src/platform/models/data/server-config.data.ts +++ b/libs/common/src/platform/models/data/server-config.data.ts @@ -1,5 +1,6 @@ import { Jsonify } from "type-fest"; +import { AllowedFeatureFlagTypes } from "../../../enums/feature-flag.enum"; import { Region } from "../../abstractions/environment.service"; import { ServerConfigResponse, @@ -13,7 +14,7 @@ export class ServerConfigData { server?: ThirdPartyServerConfigData; environment?: EnvironmentServerConfigData; utcDate: string; - featureStates: { [key: string]: string } = {}; + featureStates: { [key: string]: AllowedFeatureFlagTypes } = {}; constructor(serverConfigResponse: Partial<ServerConfigResponse>) { this.version = serverConfigResponse?.version; diff --git a/libs/common/src/platform/services/config/default-config.service.ts b/libs/common/src/platform/services/config/default-config.service.ts index e124deccf8..71b76363a3 100644 --- a/libs/common/src/platform/services/config/default-config.service.ts +++ b/libs/common/src/platform/services/config/default-config.service.ts @@ -13,7 +13,11 @@ import { } from "rxjs"; import { SemVer } from "semver"; -import { FeatureFlag, FeatureFlagValue } from "../../../enums/feature-flag.enum"; +import { + DefaultFeatureFlagValue, + FeatureFlag, + FeatureFlagValueType, +} 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"; @@ -89,20 +93,21 @@ export class DefaultConfigService implements ConfigService { map((config) => config?.environment?.cloudRegion ?? Region.US), ); } - getFeatureFlag$<T extends FeatureFlagValue>(key: FeatureFlag, defaultValue?: T) { + + getFeatureFlag$<Flag extends FeatureFlag>(key: Flag) { return this.serverConfig$.pipe( map((serverConfig) => { if (serverConfig?.featureStates == null || serverConfig.featureStates[key] == null) { - return defaultValue; + return DefaultFeatureFlagValue[key]; } - return serverConfig.featureStates[key] as T; + return serverConfig.featureStates[key] as FeatureFlagValueType<Flag>; }), ); } - async getFeatureFlag<T extends FeatureFlagValue>(key: FeatureFlag, defaultValue?: T) { - return await firstValueFrom(this.getFeatureFlag$(key, defaultValue)); + async getFeatureFlag<Flag extends FeatureFlag>(key: Flag) { + return await firstValueFrom(this.getFeatureFlag$(key)); } checkServerMeetsVersionRequirement$(minimumRequiredServerVersion: SemVer) { From 11ba8d188deb8efd52b1b8ff04c2d2e82f168be3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 26 Apr 2024 11:06:19 +0000 Subject: [PATCH 291/351] Autosync the updated translations (#8925) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/browser/src/_locales/pl/messages.json | 2 +- apps/browser/src/_locales/pt_BR/messages.json | 2 +- apps/browser/src/_locales/vi/messages.json | 24 +++++++++---------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index d3d9106c15..1a56d32a35 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -7,7 +7,7 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "message": "W domu, w pracy, lub w ruchu, Bitwarden z łatwością zabezpiecza Twoje hasła, passkeys i poufne informacje", "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index 417bc977eb..0f40bc63bb 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -7,7 +7,7 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "message": "Em qual lugar for, o Bitwarden protege suas senhas, chaves de acesso, e informações confidenciais", "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index 4eba4ffaea..6e530412db 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden Password Manager", + "message": "Bitwarden - Trình Quản lý Mật khẩu", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "message": "Ở nhà, ở cơ quan, hay trên đường đi, Bitwarden sẽ bảo mật tất cả mật khẩu, passkey, và thông tin cá nhân của bạn", "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { @@ -650,7 +650,7 @@ "message": "'Thông báo Thêm đăng nhập' sẽ tự động nhắc bạn lưu các đăng nhập mới vào hầm an toàn của bạn bất cứ khi nào bạn đăng nhập trang web lần đầu tiên." }, "addLoginNotificationDescAlt": { - "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." + "message": "Đưa ra lựa chọn để thêm một mục nếu không tìm thấy mục đó trong hòm của bạn. Áp dụng với mọi tài khoản đăng nhập trên thiết bị." }, "showCardsCurrentTab": { "message": "Hiển thị thẻ trên trang Tab" @@ -685,13 +685,13 @@ "message": "Yêu cầu cập nhật mật khẩu đăng nhập khi phát hiện thay đổi trên trang web." }, "changedPasswordNotificationDescAlt": { - "message": "Ask to update a login's password when a change is detected on a website. Applies to all logged in accounts." + "message": "Đưa ra lựa chọn để cập nhật mật khẩu khi phát hiện có sự thay đổi trên trang web. Áp dụng với mọi tài khoản đăng nhập trên thiết bị." }, "enableUsePasskeys": { - "message": "Ask to save and use passkeys" + "message": "Đưa ra lựa chọn để lưu và sử dụng passkey" }, "usePasskeysDesc": { - "message": "Ask to save new passkeys or log in with passkeys stored in your vault. Applies to all logged in accounts." + "message": "Đưa ra lựa chọn để lưu passkey mới hoặc đăng nhập bằng passkey đã lưu trong hòm. Áp dụng với mọi tài khoản đăng nhập trên thiết bị." }, "notificationChangeDesc": { "message": "Bạn có muốn cập nhật mật khẩu này trên Bitwarden không?" @@ -712,7 +712,7 @@ "message": "Sử dụng một đúp chuột để truy cập vào việc tạo mật khẩu và thông tin đăng nhập phù hợp cho trang web. " }, "contextMenuItemDescAlt": { - "message": "Use a secondary click to access password generation and matching logins for the website. Applies to all logged in accounts." + "message": "Truy cập trình khởi tạo mật khẩu và các mục đăng nhập đã lưu của trang web bằng cách nhấn đúp chuột. Áp dụng với mọi tài khoản đăng nhập trên thiết bị." }, "defaultUriMatchDetection": { "message": "Phương thức kiểm tra URI mặc định", @@ -728,7 +728,7 @@ "message": "Thay đổi màu sắc ứng dụng." }, "themeDescAlt": { - "message": "Change the application's color theme. Applies to all logged in accounts." + "message": "Thay đổi tông màu giao diện của ứng dụng. Áp dụng với mọi tài khoản đăng nhập trên thiết bị." }, "dark": { "message": "Tối", @@ -1061,10 +1061,10 @@ "message": "Tắt cài đặt trình quản lý mật khẩu tích hợp trong trình duyệt của bạn để tránh xung đột." }, "turnOffBrowserBuiltInPasswordManagerSettingsLink": { - "message": "Edit browser settings." + "message": "Thay đổi cài đặt của trình duyệt." }, "autofillOverlayVisibilityOff": { - "message": "Off", + "message": "Tắt", "description": "Overlay setting select option for disabling autofill overlay" }, "autofillOverlayVisibilityOnFieldFocus": { @@ -1168,7 +1168,7 @@ "message": "Hiển thị một ảnh nhận dạng bên cạnh mỗi lần đăng nhập." }, "faviconDescAlt": { - "message": "Show a recognizable image next to each login. Applies to all logged in accounts." + "message": "Hiển thị một biểu tượng dễ nhận dạng bên cạnh mỗi mục đăng nhập. Áp dụng với mọi tài khoản đăng nhập trên thiết bị." }, "enableBadgeCounter": { "message": "Hiển thị biểu tượng bộ đếm" @@ -1500,7 +1500,7 @@ "message": "Mã PIN không hợp lệ." }, "tooManyInvalidPinEntryAttemptsLoggingOut": { - "message": "Too many invalid PIN entry attempts. Logging out." + "message": "Mã PIN bị gõ sai quá nhiều lần. Đang đăng xuất." }, "unlockWithBiometrics": { "message": "Mở khóa bằng sinh trắc học" From 2fa4c6e4f930d6f543ac799efe339e95368b36d7 Mon Sep 17 00:00:00 2001 From: KiruthigaManivannan <162679756+KiruthigaManivannan@users.noreply.github.com> Date: Fri, 26 Apr 2024 18:24:48 +0530 Subject: [PATCH 292/351] PM-4945 Update Two Factor verify dialog (#8580) * PM-4945 Update Two Factor verify dialog * PM-4945 Addressed review comments * PM-4945 Removed legacy User verification component and used new one --- .../settings/two-factor-setup.component.ts | 3 + .../two-factor-authenticator.component.html | 7 -- .../settings/two-factor-duo.component.html | 7 -- .../settings/two-factor-email.component.html | 7 -- .../two-factor-recovery.component.html | 2 - .../settings/two-factor-setup.component.ts | 73 ++++++++++++++---- .../settings/two-factor-verify.component.html | 36 +++++---- .../settings/two-factor-verify.component.ts | 74 ++++++++++++++----- .../two-factor-webauthn.component.html | 7 -- .../two-factor-yubikey.component.html | 7 -- .../src/app/shared/loose-components.module.ts | 6 +- 11 files changed, 142 insertions(+), 87 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts b/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts index abf1d249e1..80d77968f2 100644 --- a/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts +++ b/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts @@ -10,6 +10,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { DialogService } from "@bitwarden/components"; import { TwoFactorDuoComponent } from "../../../auth/settings/two-factor-duo.component"; import { TwoFactorSetupComponent as BaseTwoFactorSetupComponent } from "../../../auth/settings/two-factor-setup.component"; @@ -22,6 +23,7 @@ import { TwoFactorSetupComponent as BaseTwoFactorSetupComponent } from "../../.. export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent { tabbedHeader = false; constructor( + dialogService: DialogService, apiService: ApiService, modalService: ModalService, messagingService: MessagingService, @@ -31,6 +33,7 @@ export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent { billingAccountProfileStateService: BillingAccountProfileStateService, ) { super( + dialogService, apiService, modalService, messagingService, diff --git a/apps/web/src/app/auth/settings/two-factor-authenticator.component.html b/apps/web/src/app/auth/settings/two-factor-authenticator.component.html index 33bf4fb130..e17714cca7 100644 --- a/apps/web/src/app/auth/settings/two-factor-authenticator.component.html +++ b/apps/web/src/app/auth/settings/two-factor-authenticator.component.html @@ -15,13 +15,6 @@ <span aria-hidden="true">&times;</span> </button> </div> - <app-two-factor-verify - [organizationId]="organizationId" - [type]="type" - (onAuthed)="auth($any($event))" - *ngIf="!authed" - > - </app-two-factor-verify> <form #form (ngSubmit)="submit()" diff --git a/apps/web/src/app/auth/settings/two-factor-duo.component.html b/apps/web/src/app/auth/settings/two-factor-duo.component.html index fa76cf362c..bf93e668fb 100644 --- a/apps/web/src/app/auth/settings/two-factor-duo.component.html +++ b/apps/web/src/app/auth/settings/two-factor-duo.component.html @@ -15,13 +15,6 @@ <span aria-hidden="true">&times;</span> </button> </div> - <app-two-factor-verify - [organizationId]="organizationId" - [type]="type" - (onAuthed)="auth($any($event))" - *ngIf="!authed" - > - </app-two-factor-verify> <form #form (ngSubmit)="submit()" diff --git a/apps/web/src/app/auth/settings/two-factor-email.component.html b/apps/web/src/app/auth/settings/two-factor-email.component.html index a885aa9624..93a6b0bb18 100644 --- a/apps/web/src/app/auth/settings/two-factor-email.component.html +++ b/apps/web/src/app/auth/settings/two-factor-email.component.html @@ -15,13 +15,6 @@ <span aria-hidden="true">&times;</span> </button> </div> - <app-two-factor-verify - [organizationId]="organizationId" - [type]="type" - (onAuthed)="auth($any($event))" - *ngIf="!authed" - > - </app-two-factor-verify> <form #form (ngSubmit)="submit()" diff --git a/apps/web/src/app/auth/settings/two-factor-recovery.component.html b/apps/web/src/app/auth/settings/two-factor-recovery.component.html index 6fdf5591c1..93cbab8b92 100644 --- a/apps/web/src/app/auth/settings/two-factor-recovery.component.html +++ b/apps/web/src/app/auth/settings/two-factor-recovery.component.html @@ -15,8 +15,6 @@ <span aria-hidden="true">&times;</span> </button> </div> - <app-two-factor-verify [type]="type" (onAuthed)="auth($event)" *ngIf="!authed"> - </app-two-factor-verify> <ng-container *ngIf="authed"> <div class="modal-body text-center"> <ng-container *ngIf="code"> diff --git a/apps/web/src/app/auth/settings/two-factor-setup.component.ts b/apps/web/src/app/auth/settings/two-factor-setup.component.ts index 53892214e4..8e2e721576 100644 --- a/apps/web/src/app/auth/settings/two-factor-setup.component.ts +++ b/apps/web/src/app/auth/settings/two-factor-setup.component.ts @@ -1,5 +1,5 @@ import { Component, OnDestroy, OnInit, Type, ViewChild, ViewContainerRef } from "@angular/core"; -import { firstValueFrom, Observable, Subject, takeUntil } from "rxjs"; +import { firstValueFrom, lastValueFrom, Observable, Subject, takeUntil } from "rxjs"; import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref"; import { ModalService } from "@bitwarden/angular/services/modal.service"; @@ -8,15 +8,23 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; +import { TwoFactorAuthenticatorResponse } from "@bitwarden/common/auth/models/response/two-factor-authenticator.response"; +import { TwoFactorDuoResponse } from "@bitwarden/common/auth/models/response/two-factor-duo.response"; +import { TwoFactorEmailResponse } from "@bitwarden/common/auth/models/response/two-factor-email.response"; +import { TwoFactorWebAuthnResponse } from "@bitwarden/common/auth/models/response/two-factor-web-authn.response"; +import { TwoFactorYubiKeyResponse } from "@bitwarden/common/auth/models/response/two-factor-yubi-key.response"; import { TwoFactorProviders } from "@bitwarden/common/auth/services/two-factor.service"; +import { AuthResponse } from "@bitwarden/common/auth/types/auth-response"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { ProductType } from "@bitwarden/common/enums"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { DialogService } from "@bitwarden/components"; import { TwoFactorAuthenticatorComponent } from "./two-factor-authenticator.component"; import { TwoFactorDuoComponent } from "./two-factor-duo.component"; import { TwoFactorEmailComponent } from "./two-factor-email.component"; import { TwoFactorRecoveryComponent } from "./two-factor-recovery.component"; +import { TwoFactorVerifyComponent } from "./two-factor-verify.component"; import { TwoFactorWebAuthnComponent } from "./two-factor-webauthn.component"; import { TwoFactorYubiKeyComponent } from "./two-factor-yubikey.component"; @@ -52,6 +60,7 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy { private twoFactorAuthPolicyAppliesToActiveUser: boolean; constructor( + protected dialogService: DialogService, protected apiService: ApiService, protected modalService: ModalService, protected messagingService: MessagingService, @@ -114,50 +123,82 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy { this.loading = false; } + async callTwoFactorVerifyDialog(type?: TwoFactorProviderType) { + const twoFactorVerifyDialogRef = TwoFactorVerifyComponent.open(this.dialogService, { + data: { type: type, organizationId: this.organizationId }, + }); + return await lastValueFrom(twoFactorVerifyDialogRef.closed); + } + async manage(type: TwoFactorProviderType) { switch (type) { case TwoFactorProviderType.Authenticator: { + const result: AuthResponse<TwoFactorAuthenticatorResponse> = + await this.callTwoFactorVerifyDialog(type); + if (!result) { + return; + } const authComp = await this.openModal( this.authenticatorModalRef, TwoFactorAuthenticatorComponent, ); - // eslint-disable-next-line rxjs-angular/prefer-takeuntil - authComp.onUpdated.subscribe((enabled: boolean) => { + await authComp.auth(result); + authComp.onUpdated.pipe(takeUntil(this.destroy$)).subscribe((enabled: boolean) => { this.updateStatus(enabled, TwoFactorProviderType.Authenticator); }); break; } case TwoFactorProviderType.Yubikey: { + const result: AuthResponse<TwoFactorYubiKeyResponse> = + await this.callTwoFactorVerifyDialog(type); + if (!result) { + return; + } const yubiComp = await this.openModal(this.yubikeyModalRef, TwoFactorYubiKeyComponent); - // eslint-disable-next-line rxjs-angular/prefer-takeuntil - yubiComp.onUpdated.subscribe((enabled: boolean) => { + yubiComp.auth(result); + yubiComp.onUpdated.pipe(takeUntil(this.destroy$)).subscribe((enabled: boolean) => { this.updateStatus(enabled, TwoFactorProviderType.Yubikey); }); break; } case TwoFactorProviderType.Duo: { + const result: AuthResponse<TwoFactorDuoResponse> = + await this.callTwoFactorVerifyDialog(type); + if (!result) { + return; + } const duoComp = await this.openModal(this.duoModalRef, TwoFactorDuoComponent); - // eslint-disable-next-line rxjs-angular/prefer-takeuntil - duoComp.onUpdated.subscribe((enabled: boolean) => { + duoComp.auth(result); + duoComp.onUpdated.pipe(takeUntil(this.destroy$)).subscribe((enabled: boolean) => { this.updateStatus(enabled, TwoFactorProviderType.Duo); }); break; } case TwoFactorProviderType.Email: { + const result: AuthResponse<TwoFactorEmailResponse> = + await this.callTwoFactorVerifyDialog(type); + if (!result) { + return; + } const emailComp = await this.openModal(this.emailModalRef, TwoFactorEmailComponent); - // eslint-disable-next-line rxjs-angular/prefer-takeuntil - emailComp.onUpdated.subscribe((enabled: boolean) => { + await emailComp.auth(result); + emailComp.onUpdated.pipe(takeUntil(this.destroy$)).subscribe((enabled: boolean) => { this.updateStatus(enabled, TwoFactorProviderType.Email); }); break; } case TwoFactorProviderType.WebAuthn: { + const result: AuthResponse<TwoFactorWebAuthnResponse> = + await this.callTwoFactorVerifyDialog(type); + if (!result) { + return; + } const webAuthnComp = await this.openModal( this.webAuthnModalRef, TwoFactorWebAuthnComponent, ); - // eslint-disable-next-line rxjs-angular/prefer-takeuntil - webAuthnComp.onUpdated.subscribe((enabled: boolean) => { + webAuthnComp.auth(result); + webAuthnComp.onUpdated.pipe(takeUntil(this.destroy$)).subscribe((enabled: boolean) => { this.updateStatus(enabled, TwoFactorProviderType.WebAuthn); }); break; @@ -167,10 +208,12 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy { } } - recoveryCode() { - // 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.openModal(this.recoveryModalRef, TwoFactorRecoveryComponent); + async recoveryCode() { + const result = await this.callTwoFactorVerifyDialog(-1 as TwoFactorProviderType); + if (result) { + const recoverComp = await this.openModal(this.recoveryModalRef, TwoFactorRecoveryComponent); + recoverComp.auth(result); + } } async premiumRequired() { diff --git a/apps/web/src/app/auth/settings/two-factor-verify.component.html b/apps/web/src/app/auth/settings/two-factor-verify.component.html index 87d6b09984..283282ccb8 100644 --- a/apps/web/src/app/auth/settings/two-factor-verify.component.html +++ b/apps/web/src/app/auth/settings/two-factor-verify.component.html @@ -1,15 +1,23 @@ -<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate> - <div class="modal-body"> - <app-user-verification [(ngModel)]="secret" ngDefaultControl name="secret"> - </app-user-verification> - </div> - <div class="modal-footer"> - <button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading"> - <i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i> - <span>{{ "continue" | i18n }}</span> - </button> - <button type="button" class="btn btn-outline-secondary" data-dismiss="modal"> - {{ "close" | i18n }} - </button> - </div> +<form [formGroup]="formGroup" [bitSubmit]="submit"> + <bit-dialog dialogSize="default"> + <span bitDialogTitle> + {{ "twoStepLogin" | i18n }} + <small class="tw-text-muted">{{ dialogTitle }}</small> + </span> + <ng-container bitDialogContent> + <app-user-verification-form-input + formControlName="secret" + ngDefaultControl + name="secret" + ></app-user-verification-form-input> + </ng-container> + <ng-container bitDialogFooter> + <button bitButton bitFormButton type="submit" buttonType="primary"> + {{ "continue" | i18n }} + </button> + <button bitButton type="button" buttonType="secondary" bitDialogClose> + {{ "close" | i18n }} + </button> + </ng-container> + </bit-dialog> </form> diff --git a/apps/web/src/app/auth/settings/two-factor-verify.component.ts b/apps/web/src/app/auth/settings/two-factor-verify.component.ts index 145573f668..7dc2847b82 100644 --- a/apps/web/src/app/auth/settings/two-factor-verify.component.ts +++ b/apps/web/src/app/auth/settings/two-factor-verify.component.ts @@ -1,4 +1,6 @@ -import { Component, EventEmitter, Input, Output } from "@angular/core"; +import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; +import { Component, EventEmitter, Inject, Output } from "@angular/core"; +import { FormControl, FormGroup } from "@angular/forms"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; @@ -8,46 +10,74 @@ import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request import { AuthResponse } from "@bitwarden/common/auth/types/auth-response"; import { TwoFactorResponse } from "@bitwarden/common/auth/types/two-factor-response"; import { Verification } from "@bitwarden/common/auth/types/verification"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { DialogService } from "@bitwarden/components"; + +type TwoFactorVerifyDialogData = { + type: TwoFactorProviderType; + organizationId: string; +}; @Component({ selector: "app-two-factor-verify", templateUrl: "two-factor-verify.component.html", }) export class TwoFactorVerifyComponent { - @Input() type: TwoFactorProviderType; - @Input() organizationId: string; + type: TwoFactorProviderType; + organizationId: string; @Output() onAuthed = new EventEmitter<AuthResponse<TwoFactorResponse>>(); - secret: Verification; formPromise: Promise<TwoFactorResponse>; + protected formGroup = new FormGroup({ + secret: new FormControl<Verification | null>(null), + }); + constructor( + @Inject(DIALOG_DATA) protected data: TwoFactorVerifyDialogData, + private dialogRef: DialogRef, private apiService: ApiService, - private logService: LogService, + private i18nService: I18nService, private userVerificationService: UserVerificationService, - ) {} + ) { + this.type = data.type; + this.organizationId = data.organizationId; + } - async submit() { + submit = async () => { let hashedSecret: string; - - try { - this.formPromise = this.userVerificationService.buildRequest(this.secret).then((request) => { + this.formPromise = this.userVerificationService + .buildRequest(this.formGroup.value.secret) + .then((request) => { hashedSecret = - this.secret.type === VerificationType.MasterPassword + this.formGroup.value.secret.type === VerificationType.MasterPassword ? request.masterPasswordHash : request.otp; return this.apiCall(request); }); - const response = await this.formPromise; - this.onAuthed.emit({ - response: response, - secret: hashedSecret, - verificationType: this.secret.type, - }); - } catch (e) { - this.logService.error(e); + const response = await this.formPromise; + this.dialogRef.close({ + response: response, + secret: hashedSecret, + verificationType: this.formGroup.value.secret.type, + }); + }; + + get dialogTitle(): string { + switch (this.type) { + case -1 as TwoFactorProviderType: + return this.i18nService.t("recoveryCodeTitle"); + case TwoFactorProviderType.Duo: + return "Duo"; + case TwoFactorProviderType.Email: + return this.i18nService.t("emailTitle"); + case TwoFactorProviderType.WebAuthn: + return this.i18nService.t("webAuthnTitle"); + case TwoFactorProviderType.Authenticator: + return this.i18nService.t("authenticatorAppTitle"); + case TwoFactorProviderType.Yubikey: + return "Yubikey"; } } @@ -72,4 +102,8 @@ export class TwoFactorVerifyComponent { return this.apiService.getTwoFactorYubiKey(request); } } + + static open(dialogService: DialogService, config: DialogConfig<TwoFactorVerifyDialogData>) { + return dialogService.open<AuthResponse<any>>(TwoFactorVerifyComponent, config); + } } diff --git a/apps/web/src/app/auth/settings/two-factor-webauthn.component.html b/apps/web/src/app/auth/settings/two-factor-webauthn.component.html index 72f9d66c2c..f5a84397c8 100644 --- a/apps/web/src/app/auth/settings/two-factor-webauthn.component.html +++ b/apps/web/src/app/auth/settings/two-factor-webauthn.component.html @@ -15,13 +15,6 @@ <span aria-hidden="true">&times;</span> </button> </div> - <app-two-factor-verify - [organizationId]="organizationId" - [type]="type" - (onAuthed)="auth($any($event))" - *ngIf="!authed" - > - </app-two-factor-verify> <form #form (ngSubmit)="submit()" diff --git a/apps/web/src/app/auth/settings/two-factor-yubikey.component.html b/apps/web/src/app/auth/settings/two-factor-yubikey.component.html index d377b303ef..47482c07a3 100644 --- a/apps/web/src/app/auth/settings/two-factor-yubikey.component.html +++ b/apps/web/src/app/auth/settings/two-factor-yubikey.component.html @@ -15,13 +15,6 @@ <span aria-hidden="true">&times;</span> </button> </div> - <app-two-factor-verify - [organizationId]="organizationId" - [type]="type" - (onAuthed)="auth($any($event))" - *ngIf="!authed" - > - </app-two-factor-verify> <form #form (ngSubmit)="submit()" diff --git a/apps/web/src/app/shared/loose-components.module.ts b/apps/web/src/app/shared/loose-components.module.ts index 8f6a1eaedc..b511f5d766 100644 --- a/apps/web/src/app/shared/loose-components.module.ts +++ b/apps/web/src/app/shared/loose-components.module.ts @@ -1,6 +1,9 @@ import { NgModule } from "@angular/core"; -import { PasswordCalloutComponent } from "@bitwarden/auth/angular"; +import { + PasswordCalloutComponent, + UserVerificationFormInputComponent, +} from "@bitwarden/auth/angular"; import { LayoutComponent, NavigationModule } from "@bitwarden/components"; import { OrganizationLayoutComponent } from "../admin-console/organizations/layouts/organization-layout.component"; @@ -106,6 +109,7 @@ import { SharedModule } from "./shared.module"; OrganizationBadgeModule, PipesModule, PasswordCalloutComponent, + UserVerificationFormInputComponent, DangerZoneComponent, LayoutComponent, NavigationModule, From ec37e5e4d3650079318a2a07ed94947d87feccf3 Mon Sep 17 00:00:00 2001 From: Victoria League <vleague@bitwarden.com> Date: Fri, 26 Apr 2024 09:35:32 -0400 Subject: [PATCH 293/351] [CL-219][CL-218][CL-217] Add new extension layout components (#8728) --- .storybook/main.ts | 2 + .storybook/tsconfig.json | 4 +- .storybook/typings.d.ts | 4 - .../popup/layout/popup-footer.component.html | 9 + .../popup/layout/popup-footer.component.ts | 9 + .../popup/layout/popup-header.component.html | 19 + .../popup/layout/popup-header.component.ts | 34 ++ .../platform/popup/layout/popup-layout.mdx | 138 +++++++ .../popup/layout/popup-layout.stories.ts | 367 ++++++++++++++++++ .../popup/layout/popup-page.component.html | 7 + .../popup/layout/popup-page.component.ts | 11 + .../popup-tab-navigation.component.html | 32 ++ .../layout/popup-tab-navigation.component.ts | 43 ++ apps/browser/src/popup/app.module.ts | 8 + apps/browser/src/popup/scss/base.scss | 6 +- apps/browser/tsconfig.json | 1 + libs/components/src/tw-theme.css | 8 + libs/components/tailwind.config.base.js | 5 + libs/components/tailwind.config.js | 1 + tsconfig.json | 16 +- 20 files changed, 711 insertions(+), 13 deletions(-) delete mode 100644 .storybook/typings.d.ts create mode 100644 apps/browser/src/platform/popup/layout/popup-footer.component.html create mode 100644 apps/browser/src/platform/popup/layout/popup-footer.component.ts create mode 100644 apps/browser/src/platform/popup/layout/popup-header.component.html create mode 100644 apps/browser/src/platform/popup/layout/popup-header.component.ts create mode 100644 apps/browser/src/platform/popup/layout/popup-layout.mdx create mode 100644 apps/browser/src/platform/popup/layout/popup-layout.stories.ts create mode 100644 apps/browser/src/platform/popup/layout/popup-page.component.html create mode 100644 apps/browser/src/platform/popup/layout/popup-page.component.ts create mode 100644 apps/browser/src/platform/popup/layout/popup-tab-navigation.component.html create mode 100644 apps/browser/src/platform/popup/layout/popup-tab-navigation.component.ts diff --git a/.storybook/main.ts b/.storybook/main.ts index c71a74c2a7..cb63ada550 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -9,6 +9,8 @@ const config: StorybookConfig = { "../libs/components/src/**/*.stories.@(js|jsx|ts|tsx)", "../apps/web/src/**/*.mdx", "../apps/web/src/**/*.stories.@(js|jsx|ts|tsx)", + "../apps/browser/src/**/*.mdx", + "../apps/browser/src/**/*.stories.@(js|jsx|ts|tsx)", "../bitwarden_license/bit-web/src/**/*.mdx", "../bitwarden_license/bit-web/src/**/*.stories.@(js|jsx|ts|tsx)", ], diff --git a/.storybook/tsconfig.json b/.storybook/tsconfig.json index 113cc5bcde..34acc9a740 100644 --- a/.storybook/tsconfig.json +++ b/.storybook/tsconfig.json @@ -1,12 +1,10 @@ { "extends": "../tsconfig", "compilerOptions": { - "types": ["node", "jest", "chrome"], "allowSyntheticDefaultImports": true }, - "exclude": ["../src/test.setup.ts", "../apps/src/**/*.spec.ts", "../libs/**/*.spec.ts"], + "exclude": ["../src/test.setup.ts", "../apps/**/*.spec.ts", "../libs/**/*.spec.ts"], "files": [ - "./typings.d.ts", "./preview.tsx", "../libs/components/src/main.ts", "../libs/components/src/polyfills.ts" diff --git a/.storybook/typings.d.ts b/.storybook/typings.d.ts deleted file mode 100644 index c94d67b1a2..0000000000 --- a/.storybook/typings.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module "*.md" { - const content: string; - export default content; -} diff --git a/apps/browser/src/platform/popup/layout/popup-footer.component.html b/apps/browser/src/platform/popup/layout/popup-footer.component.html new file mode 100644 index 0000000000..2cbbca79c0 --- /dev/null +++ b/apps/browser/src/platform/popup/layout/popup-footer.component.html @@ -0,0 +1,9 @@ +<footer + class="tw-p-3 tw-border-0 tw-border-solid tw-border-t tw-border-secondary-300 tw-bg-background" +> + <div class="tw-max-w-screen-sm tw-mx-auto"> + <div class="tw-flex tw-justify-start tw-gap-2"> + <ng-content></ng-content> + </div> + </div> +</footer> diff --git a/apps/browser/src/platform/popup/layout/popup-footer.component.ts b/apps/browser/src/platform/popup/layout/popup-footer.component.ts new file mode 100644 index 0000000000..826a1d1c60 --- /dev/null +++ b/apps/browser/src/platform/popup/layout/popup-footer.component.ts @@ -0,0 +1,9 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "popup-footer", + templateUrl: "popup-footer.component.html", + standalone: true, + imports: [], +}) +export class PopupFooterComponent {} diff --git a/apps/browser/src/platform/popup/layout/popup-header.component.html b/apps/browser/src/platform/popup/layout/popup-header.component.html new file mode 100644 index 0000000000..c0894f8168 --- /dev/null +++ b/apps/browser/src/platform/popup/layout/popup-header.component.html @@ -0,0 +1,19 @@ +<header + class="tw-p-4 tw-border-0 tw-border-solid tw-border-b tw-border-secondary-300 tw-bg-background" +> + <div class="tw-max-w-screen-sm tw-mx-auto tw-flex tw-justify-between tw-w-full"> + <div class="tw-inline-flex tw-items-center tw-gap-2 tw-h-9"> + <button + bitIconButton="bwi-back" + type="button" + *ngIf="showBackButton" + [title]="'back' | i18n" + [ariaLabel]="'back' | i18n" + ></button> + <h1 bitTypography="h3" class="!tw-mb-0.5 tw-text-headers">{{ pageTitle }}</h1> + </div> + <div class="tw-inline-flex tw-items-center tw-gap-2 tw-h-9"> + <ng-content select="[slot=end]"></ng-content> + </div> + </div> +</header> diff --git a/apps/browser/src/platform/popup/layout/popup-header.component.ts b/apps/browser/src/platform/popup/layout/popup-header.component.ts new file mode 100644 index 0000000000..f2f8eb95af --- /dev/null +++ b/apps/browser/src/platform/popup/layout/popup-header.component.ts @@ -0,0 +1,34 @@ +import { BooleanInput, coerceBooleanProperty } from "@angular/cdk/coercion"; +import { CommonModule, Location } from "@angular/common"; +import { Component, Input } from "@angular/core"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { IconButtonModule, TypographyModule } from "@bitwarden/components"; + +@Component({ + selector: "popup-header", + templateUrl: "popup-header.component.html", + standalone: true, + imports: [TypographyModule, CommonModule, IconButtonModule, JslibModule], +}) +export class PopupHeaderComponent { + /** Display the back button, which uses Location.back() to go back one page in history */ + @Input() + get showBackButton() { + return this._showBackButton; + } + set showBackButton(value: BooleanInput) { + this._showBackButton = coerceBooleanProperty(value); + } + + private _showBackButton = false; + + /** Title string that will be inserted as an h1 */ + @Input({ required: true }) pageTitle: string; + + constructor(private location: Location) {} + + back() { + this.location.back(); + } +} diff --git a/apps/browser/src/platform/popup/layout/popup-layout.mdx b/apps/browser/src/platform/popup/layout/popup-layout.mdx new file mode 100644 index 0000000000..91f7dab277 --- /dev/null +++ b/apps/browser/src/platform/popup/layout/popup-layout.mdx @@ -0,0 +1,138 @@ +import { Meta, Story, Canvas } from "@storybook/addon-docs"; + +import * as stories from "./popup-layout.stories"; + +<Meta of={stories} /> + +Please note that because these stories use `router-outlet`, there are issues with rendering content +when Light & Dark mode is selected. The stories are best viewed by selecting one color mode. + +# Popup Tab Navigation + +The popup tab navigation component composes together the popup page and the bottom tab navigation +footer. This component is intended to be used a level _above_ each extension tab's page code. + +The navigation footer contains the 4 main page links for the browser extension. It uses the Angular +router to determine which page is currently active, and style the button appropriately. Clicking on +the buttons will navigate to the correct route. The navigation footer has a max-width built in so +that the page looks nice when the extension is popped out. + +Long button names will be ellipsed. + +Usage example: + +```html +<popup-tab-navigation> + <popup-page></popup-page> +</popup-tab-navigation> +``` + +# Popup Page + +The popup page handles positioning a page's `header` and `footer` elements, and inserting the rest +of the content into the `main` element with scroll. There is also a max-width built in so that the +page looks nice when the extension is popped out. + +**Slots** + +- `header` + - Use `popup-header` component. + - Every page should have a header. +- `footer` + - Use the `popup-footer` component. + - Not every page will have a footer. +- default + - Whatever content you want in `main`. + +Basic usage example: + +```html +<popup-page> + <popup-header slot="header"></popup-header> + <div>This is content</div> + <popup-footer slot="footer"></popup-footer> +</popup-page> +``` + +## Popup header + +**Args** + +- `pageTitle`: required + - Inserts title as an `h1`. +- `showBackButton`: optional, defaults to `false` + - Toggles the back button to appear. The back button uses `Location.back()` to navigate back one + page in history. + +**Slots** + +- `end` + - Use to insert one or more interactive elements. + - The header handles the spacing between elements passed to the `end` slot. + +Usage example: + +```html +<popup-header pageTitle="Test" showBackButton> + <ng-container slot="end"> + <button></button> + <button></button> + </ng-container> +</popup-header> +``` + +Common interactive elements to insert into the `end` slot are: + +- `app-current-account`: shows current account and switcher +- `app-pop-out`: shows popout button when the extension is not already popped out +- "Add" button: this can be accomplished with the Button component and any custom functionality for + that particular page + +## Popup footer + +Popup footer should be used when the page displays action buttons. It functions similarly to the +Dialog footer in that the calling code is responsible for passing in the different buttons that need +to be rendered. + +Usage example: + +```html +<popup-footer> + <button bitButton buttonType="primary">Save</button> + <button bitButton buttonType="secondary">Cancel</button> +</popup-footer> +``` + +# Page types + +There are a few types of pages that are used in the browser extension. + +View the story source code to see examples of how to construct these types of pages. + +## Extension Tab + +Example of wrapping an extension page in the `popup-tab-navigation` component. + +<Canvas> + <Story of={stories.PopupTabNavigation} /> +</Canvas> + +## Extension Page + +Examples of using just the `popup-page` component, without and with a footer. + +<Canvas> + <Story of={stories.PopupPage} /> +</Canvas> + +<Canvas> + <Story of={stories.PopupPageWithFooter} /> +</Canvas> + +## Popped out + +When the browser extension is popped out, the "popout" button should not be passed to the header. + +<Canvas> + <Story of={stories.PoppedOut} /> +</Canvas> diff --git a/apps/browser/src/platform/popup/layout/popup-layout.stories.ts b/apps/browser/src/platform/popup/layout/popup-layout.stories.ts new file mode 100644 index 0000000000..1b10e50c0c --- /dev/null +++ b/apps/browser/src/platform/popup/layout/popup-layout.stories.ts @@ -0,0 +1,367 @@ +import { CommonModule } from "@angular/common"; +import { Component, importProvidersFrom } from "@angular/core"; +import { RouterModule } from "@angular/router"; +import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { + AvatarModule, + ButtonModule, + I18nMockService, + IconButtonModule, +} from "@bitwarden/components"; + +import { PopupFooterComponent } from "./popup-footer.component"; +import { PopupHeaderComponent } from "./popup-header.component"; +import { PopupPageComponent } from "./popup-page.component"; +import { PopupTabNavigationComponent } from "./popup-tab-navigation.component"; + +@Component({ + selector: "extension-container", + template: ` + <div class="tw-h-[640px] tw-w-[380px] tw-border tw-border-solid tw-border-secondary-300"> + <ng-content></ng-content> + </div> + `, + standalone: true, +}) +class ExtensionContainerComponent {} + +@Component({ + selector: "vault-placeholder", + template: ` + <div class="tw-mb-8 tw-text-main">vault item</div> + <div class="tw-my-8 tw-text-main">vault item</div> + <div class="tw-my-8 tw-text-main">vault item</div> + <div class="tw-my-8 tw-text-main">vault item</div> + <div class="tw-my-8 tw-text-main">vault item</div> + <div class="tw-my-8 tw-text-main">vault item</div> + <div class="tw-my-8 tw-text-main">vault item</div> + <div class="tw-my-8 tw-text-main">vault item</div> + <div class="tw-my-8 tw-text-main">vault item</div> + <div class="tw-my-8 tw-text-main">vault item</div> + <div class="tw-my-8 tw-text-main">vault item</div> + <div class="tw-my-8 tw-text-main">vault item</div> + <div class="tw-my-8 tw-text-main">vault item last item</div> + `, + standalone: true, +}) +class VaultComponent {} + +@Component({ + selector: "generator-placeholder", + template: ` <div class="tw-text-main">generator stuff here</div> `, + standalone: true, +}) +class GeneratorComponent {} + +@Component({ + selector: "send-placeholder", + template: ` <div class="tw-text-main">send some stuff</div> `, + standalone: true, +}) +class SendComponent {} + +@Component({ + selector: "settings-placeholder", + template: ` <div class="tw-text-main">change your settings</div> `, + standalone: true, +}) +class SettingsComponent {} + +@Component({ + selector: "mock-add-button", + template: ` + <button bitButton buttonType="primary" type="button"> + <i class="bwi bwi-plus-f" aria-hidden="true"></i> + Add + </button> + `, + standalone: true, + imports: [ButtonModule], +}) +class MockAddButtonComponent {} + +@Component({ + selector: "mock-popout-button", + template: ` + <button + bitIconButton="bwi-popout" + size="small" + type="button" + title="Pop out" + aria-label="Pop out" + ></button> + `, + standalone: true, + imports: [IconButtonModule], +}) +class MockPopoutButtonComponent {} + +@Component({ + selector: "mock-current-account", + template: ` + <button class="tw-bg-transparent tw-border-none" type="button"> + <bit-avatar text="Ash Ketchum" size="small"></bit-avatar> + </button> + `, + standalone: true, + imports: [AvatarModule], +}) +class MockCurrentAccountComponent {} + +@Component({ + selector: "mock-vault-page", + template: ` + <popup-page> + <popup-header slot="header" pageTitle="Test"> + <ng-container slot="end"> + <mock-add-button></mock-add-button> + <mock-popout-button></mock-popout-button> + <mock-current-account></mock-current-account> + </ng-container> + </popup-header> + <vault-placeholder></vault-placeholder> + </popup-page> + `, + standalone: true, + imports: [ + PopupPageComponent, + PopupHeaderComponent, + MockAddButtonComponent, + MockPopoutButtonComponent, + MockCurrentAccountComponent, + VaultComponent, + ], +}) +class MockVaultPageComponent {} + +@Component({ + selector: "mock-vault-page-popped", + template: ` + <popup-page> + <popup-header slot="header" pageTitle="Test"> + <ng-container slot="end"> + <mock-add-button></mock-add-button> + <mock-current-account></mock-current-account> + </ng-container> + </popup-header> + <vault-placeholder></vault-placeholder> + </popup-page> + `, + standalone: true, + imports: [ + PopupPageComponent, + PopupHeaderComponent, + MockAddButtonComponent, + MockPopoutButtonComponent, + MockCurrentAccountComponent, + VaultComponent, + ], +}) +class MockVaultPagePoppedComponent {} + +@Component({ + selector: "mock-generator-page", + template: ` + <popup-page> + <popup-header slot="header" pageTitle="Test"> + <ng-container slot="end"> + <mock-add-button></mock-add-button> + <mock-popout-button></mock-popout-button> + <mock-current-account></mock-current-account> + </ng-container> + </popup-header> + <generator-placeholder></generator-placeholder> + </popup-page> + `, + standalone: true, + imports: [ + PopupPageComponent, + PopupHeaderComponent, + MockAddButtonComponent, + MockPopoutButtonComponent, + MockCurrentAccountComponent, + GeneratorComponent, + ], +}) +class MockGeneratorPageComponent {} + +@Component({ + selector: "mock-send-page", + template: ` + <popup-page> + <popup-header slot="header" pageTitle="Test"> + <ng-container slot="end"> + <mock-add-button></mock-add-button> + <mock-popout-button></mock-popout-button> + <mock-current-account></mock-current-account> + </ng-container> + </popup-header> + <send-placeholder></send-placeholder> + </popup-page> + `, + standalone: true, + imports: [ + PopupPageComponent, + PopupHeaderComponent, + MockAddButtonComponent, + MockPopoutButtonComponent, + MockCurrentAccountComponent, + SendComponent, + ], +}) +class MockSendPageComponent {} + +@Component({ + selector: "mock-settings-page", + template: ` + <popup-page> + <popup-header slot="header" pageTitle="Test"> + <ng-container slot="end"> + <mock-add-button></mock-add-button> + <mock-popout-button></mock-popout-button> + <mock-current-account></mock-current-account> + </ng-container> + </popup-header> + <settings-placeholder></settings-placeholder> + </popup-page> + `, + standalone: true, + imports: [ + PopupPageComponent, + PopupHeaderComponent, + MockAddButtonComponent, + MockPopoutButtonComponent, + MockCurrentAccountComponent, + SettingsComponent, + ], +}) +class MockSettingsPageComponent {} + +@Component({ + selector: "mock-vault-subpage", + template: ` + <popup-page> + <popup-header slot="header" pageTitle="Test" showBackButton> + <ng-container slot="end"> + <mock-popout-button></mock-popout-button> + </ng-container> + </popup-header> + <vault-placeholder></vault-placeholder> + <popup-footer slot="footer"> + <button bitButton buttonType="primary">Save</button> + <button bitButton buttonType="secondary">Cancel</button> + </popup-footer> + </popup-page> + `, + standalone: true, + imports: [ + PopupPageComponent, + PopupHeaderComponent, + PopupFooterComponent, + ButtonModule, + MockAddButtonComponent, + MockPopoutButtonComponent, + MockCurrentAccountComponent, + VaultComponent, + ], +}) +class MockVaultSubpageComponent {} + +export default { + title: "Browser/Popup Layout", + component: PopupPageComponent, + decorators: [ + moduleMetadata({ + imports: [ + PopupTabNavigationComponent, + CommonModule, + RouterModule, + ExtensionContainerComponent, + MockVaultSubpageComponent, + MockVaultPageComponent, + MockSendPageComponent, + MockGeneratorPageComponent, + MockSettingsPageComponent, + MockVaultPagePoppedComponent, + ], + providers: [ + { + provide: I18nService, + useFactory: () => { + return new I18nMockService({ + back: "Back", + }); + }, + }, + ], + }), + applicationConfig({ + providers: [ + importProvidersFrom( + RouterModule.forRoot( + [ + { path: "", redirectTo: "vault", pathMatch: "full" }, + { path: "vault", component: MockVaultPageComponent }, + { path: "generator", component: MockGeneratorPageComponent }, + { path: "send", component: MockSendPageComponent }, + { path: "settings", component: MockSettingsPageComponent }, + // in case you are coming from a story that also uses the router + { path: "**", redirectTo: "vault" }, + ], + { useHash: true }, + ), + ), + ], + }), + ], +} as Meta; + +type Story = StoryObj<PopupPageComponent>; + +export const PopupTabNavigation: Story = { + render: (args) => ({ + props: args, + template: /* HTML */ ` + <extension-container> + <popup-tab-navigation> + <router-outlet></router-outlet> + </popup-tab-navigation> + </extension-container> + `, + }), +}; + +export const PopupPage: Story = { + render: (args) => ({ + props: args, + template: /* HTML */ ` + <extension-container> + <mock-vault-page></mock-vault-page> + </extension-container> + `, + }), +}; + +export const PopupPageWithFooter: Story = { + render: (args) => ({ + props: args, + template: /* HTML */ ` + <extension-container> + <mock-vault-subpage></mock-vault-subpage> + </extension-container> + `, + }), +}; + +export const PoppedOut: Story = { + render: (args) => ({ + props: args, + template: /* HTML */ ` + <div class="tw-h-[640px] tw-w-[900px] tw-border tw-border-solid tw-border-secondary-300"> + <mock-vault-page-popped></mock-vault-page-popped> + </div> + `, + }), +}; diff --git a/apps/browser/src/platform/popup/layout/popup-page.component.html b/apps/browser/src/platform/popup/layout/popup-page.component.html new file mode 100644 index 0000000000..ba871d6319 --- /dev/null +++ b/apps/browser/src/platform/popup/layout/popup-page.component.html @@ -0,0 +1,7 @@ +<ng-content select="[slot=header]"></ng-content> +<main class="tw-bg-background-alt tw-p-3 tw-flex-1 tw-overflow-y-auto"> + <div class="tw-max-w-screen-sm tw-mx-auto"> + <ng-content></ng-content> + </div> +</main> +<ng-content select="[slot=footer]"></ng-content> diff --git a/apps/browser/src/platform/popup/layout/popup-page.component.ts b/apps/browser/src/platform/popup/layout/popup-page.component.ts new file mode 100644 index 0000000000..1223a6f418 --- /dev/null +++ b/apps/browser/src/platform/popup/layout/popup-page.component.ts @@ -0,0 +1,11 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "popup-page", + templateUrl: "popup-page.component.html", + standalone: true, + host: { + class: "tw-h-full tw-flex tw-flex-col tw-flex-1 tw-overflow-y-auto", + }, +}) +export class PopupPageComponent {} diff --git a/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.html b/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.html new file mode 100644 index 0000000000..a0ff252c6c --- /dev/null +++ b/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.html @@ -0,0 +1,32 @@ +<div class="tw-h-full tw-overflow-y-auto [&>*]:tw-h-full [&>*]:tw-overflow-y-auto"> + <ng-content></ng-content> +</div> +<footer class="tw-bg-background tw-border-0 tw-border-t tw-border-secondary-300 tw-border-solid"> + <div class="tw-max-w-screen-sm tw-mx-auto"> + <div class="tw-flex tw-flex-1"> + <a + *ngFor="let button of navButtons" + class="tw-group tw-flex tw-flex-col tw-items-center tw-gap-1 tw-px-0.5 tw-pb-2 tw-pt-3 tw-w-1/4 tw-no-underline hover:tw-no-underline hover:tw-text-primary-600 hover:tw-bg-primary-100 tw-border-2 tw-border-solid tw-border-transparent focus-visible:tw-rounded-lg focus-visible:tw-border-primary-500" + [ngClass]="rla.isActive ? 'tw-font-bold tw-text-primary-600' : 'tw-text-muted'" + [title]="button.label" + [routerLink]="button.page" + routerLinkActive + #rla="routerLinkActive" + ariaCurrentWhenActive="page" + > + <i *ngIf="!rla.isActive" class="bwi bwi-lg bwi-{{ button.iconKey }}" aria-hidden="true"></i> + <i + *ngIf="rla.isActive" + class="bwi bwi-lg bwi-{{ button.iconKeyActive }}" + aria-hidden="true" + ></i> + <span + class="tw-truncate tw-max-w-full" + [ngClass]="!rla.isActive && 'group-hover:tw-underline'" + > + {{ button.label }} + </span> + </a> + </div> + </div> +</footer> diff --git a/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.ts b/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.ts new file mode 100644 index 0000000000..3a275454d9 --- /dev/null +++ b/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.ts @@ -0,0 +1,43 @@ +import { CommonModule } from "@angular/common"; +import { Component } from "@angular/core"; +import { RouterModule } from "@angular/router"; + +import { LinkModule } from "@bitwarden/components"; + +@Component({ + selector: "popup-tab-navigation", + templateUrl: "popup-tab-navigation.component.html", + standalone: true, + imports: [CommonModule, LinkModule, RouterModule], + host: { + class: "tw-block tw-h-full tw-w-full tw-flex tw-flex-col", + }, +}) +export class PopupTabNavigationComponent { + navButtons = [ + { + label: "Vault", + page: "/vault", + iconKey: "lock", + iconKeyActive: "lock-f", + }, + { + label: "Generator", + page: "/generator", + iconKey: "generate", + iconKeyActive: "generate-f", + }, + { + label: "Send", + page: "/send", + iconKey: "send", + iconKeyActive: "send-f", + }, + { + label: "Settings", + page: "/settings", + iconKey: "cog", + iconKeyActive: "cog-f", + }, + ]; +} diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index 5718542b01..2fb582d693 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -36,6 +36,10 @@ import { TwoFactorComponent } from "../auth/popup/two-factor.component"; import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component"; import { AutofillComponent } from "../autofill/popup/settings/autofill.component"; import { HeaderComponent } from "../platform/popup/header.component"; +import { PopupFooterComponent } from "../platform/popup/layout/popup-footer.component"; +import { PopupHeaderComponent } from "../platform/popup/layout/popup-header.component"; +import { PopupPageComponent } from "../platform/popup/layout/popup-page.component"; +import { PopupTabNavigationComponent } from "../platform/popup/layout/popup-tab-navigation.component"; import { FilePopoutCalloutComponent } from "../tools/popup/components/file-popout-callout.component"; import { GeneratorComponent } from "../tools/popup/generator/generator.component"; import { PasswordGeneratorHistoryComponent } from "../tools/popup/generator/password-generator-history.component"; @@ -108,6 +112,10 @@ import "../platform/popup/locales"; AccountComponent, ButtonModule, ExportScopeCalloutComponent, + PopupPageComponent, + PopupTabNavigationComponent, + PopupFooterComponent, + PopupHeaderComponent, ], declarations: [ ActionButtonsComponent, diff --git a/apps/browser/src/popup/scss/base.scss b/apps/browser/src/popup/scss/base.scss index 73da455941..80c7536087 100644 --- a/apps/browser/src/popup/scss/base.scss +++ b/apps/browser/src/popup/scss/base.scss @@ -68,7 +68,7 @@ img { border: none; } -a { +a:not(popup-page a, popup-tab-navigation a) { text-decoration: none; @include themify($themes) { @@ -171,7 +171,7 @@ cdk-virtual-scroll-viewport::-webkit-scrollbar-thumb, } } -header:not(bit-callout header, bit-dialog header) { +header:not(bit-callout header, bit-dialog header, popup-page header) { height: 44px; display: flex; @@ -448,7 +448,7 @@ app-root { } } -main { +main:not(popup-page main) { position: absolute; top: 44px; bottom: 0; diff --git a/apps/browser/tsconfig.json b/apps/browser/tsconfig.json index 505f1533ae..e1bf2b7211 100644 --- a/apps/browser/tsconfig.json +++ b/apps/browser/tsconfig.json @@ -2,6 +2,7 @@ "compilerOptions": { "moduleResolution": "node", "noImplicitAny": true, + "allowSyntheticDefaultImports": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, "module": "ES2020", diff --git a/libs/components/src/tw-theme.css b/libs/components/src/tw-theme.css index 72e8e1e5e8..00ab2ff717 100644 --- a/libs/components/src/tw-theme.css +++ b/libs/components/src/tw-theme.css @@ -17,7 +17,11 @@ --color-background-alt3: 18 82 163; --color-background-alt4: 13 60 119; + /* Can only be used behind the extension refresh flag */ + --color-primary-100: 200 217 249; --color-primary-300: 103 149 232; + /* Can only be used behind the extension refresh flag */ + --color-primary-500: 23 93 220; --color-primary-600: 23 93 220; --color-primary-700: 18 82 163; @@ -43,6 +47,7 @@ --color-text-contrast: 255 255 255; --color-text-alt2: 255 255 255; --color-text-code: 192 17 118; + --color-text-headers: 2 15 102; --tw-ring-offset-color: #ffffff; } @@ -60,7 +65,9 @@ --color-background-alt3: 47 52 61; --color-background-alt4: 16 18 21; + --color-primary-100: 8 31 73; --color-primary-300: 23 93 220; + --color-primary-500: 54 117 232; --color-primary-600: 106 153 240; --color-primary-700: 180 204 249; @@ -86,6 +93,7 @@ --color-text-contrast: 25 30 38; --color-text-alt2: 255 255 255; --color-text-code: 240 141 199; + --color-text-headers: 226 227 228; --tw-ring-offset-color: #1f242e; } diff --git a/libs/components/tailwind.config.base.js b/libs/components/tailwind.config.base.js index b76f25eae7..12af316b38 100644 --- a/libs/components/tailwind.config.base.js +++ b/libs/components/tailwind.config.base.js @@ -24,7 +24,11 @@ module.exports = { current: colors.current, black: colors.black, primary: { + // Can only be used behind the extension refresh flag + 100: rgba("--color-primary-100"), 300: rgba("--color-primary-300"), + // Can only be used behind the extension refresh flag + 500: rgba("--color-primary-500"), 600: rgba("--color-primary-600"), 700: rgba("--color-primary-700"), }, @@ -69,6 +73,7 @@ module.exports = { main: rgba("--color-text-main"), muted: rgba("--color-text-muted"), contrast: rgba("--color-text-contrast"), + headers: rgba("--color-text-headers"), alt2: rgba("--color-text-alt2"), code: rgba("--color-text-code"), success: rgba("--color-success-600"), diff --git a/libs/components/tailwind.config.js b/libs/components/tailwind.config.js index 987b969e8f..7a53c82ec5 100644 --- a/libs/components/tailwind.config.js +++ b/libs/components/tailwind.config.js @@ -5,6 +5,7 @@ config.content = [ "libs/components/src/**/*.{html,ts,mdx}", "libs/auth/src/**/*.{html,ts,mdx}", "apps/web/src/**/*.{html,ts,mdx}", + "apps/browser/src/**/*.{html,ts,mdx}", "bitwarden_license/bit-web/src/**/*.{html,ts,mdx}", ".storybook/preview.tsx", ]; diff --git a/tsconfig.json b/tsconfig.json index ab3f8861a9..60dc9d223e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,7 +5,7 @@ "noImplicitAny": true, "target": "ES2016", "module": "ES2020", - "lib": ["es5", "es6", "es7", "dom"], + "lib": ["es5", "es6", "es7", "dom", "ES2021"], "sourceMap": true, "allowSyntheticDefaultImports": true, "experimentalDecorators": true, @@ -38,6 +38,16 @@ ], "useDefineForClassFields": false }, - "include": ["apps/web/src/**/*", "libs/*/src/**/*", "bitwarden_license/bit-web/src/**/*"], - "exclude": ["apps/web/src/**/*.spec.ts", "libs/*/src/**/*.spec.ts", "**/*.spec-util.ts"] + "include": [ + "apps/web/src/**/*", + "apps/browser/src/**/*", + "libs/*/src/**/*", + "bitwarden_license/bit-web/src/**/*" + ], + "exclude": [ + "apps/web/src/**/*.spec.ts", + "apps/browser/src/**/*.spec.ts", + "libs/*/src/**/*.spec.ts", + "**/*.spec-util.ts" + ] } From acea273f972087e5410c42d2d08f5192c788022f Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Fri, 26 Apr 2024 09:39:46 -0400 Subject: [PATCH 294/351] Fix ConfigService Merge Conflict (#8932) --- .../providers/clients/manage-client-organizations.component.ts | 1 - 1 file changed, 1 deletion(-) 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 index 3cc96c4589..c10fdf0bc7 100644 --- 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 @@ -38,7 +38,6 @@ export class ManageClientOrganizationsComponent extends BaseClientsComponent { private consolidatedBillingEnabled$ = this.configService.getFeatureFlag$( FeatureFlag.EnableConsolidatedBilling, - false, ); protected plans: PlanResponse[]; From 7f5efcc18cea94af4998f18afaecdc5aae9de580 Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Fri, 26 Apr 2024 10:18:05 -0400 Subject: [PATCH 295/351] PM-7745 - SSO Login Strategy - trySetUserKeyWithDeviceKey should use the user id from the IdTokenResponse and not StateService as I suspect it's not working as expected. Thinking there might be a race condition where the user id is null or maybe incorrect. (#8927) --- .../src/common/login-strategies/sso-login.strategy.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) 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 c7cd9052f8..ad56d1ae51 100644 --- a/libs/auth/src/common/login-strategies/sso-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/sso-login.strategy.ts @@ -244,7 +244,7 @@ export class SsoLoginStrategy extends LoginStrategy { // Only try to set user key with device key if admin approval request was not successful if (!hasUserKey) { - await this.trySetUserKeyWithDeviceKey(tokenResponse); + await this.trySetUserKeyWithDeviceKey(tokenResponse, userId); } } else if ( masterKeyEncryptedUserKey != null && @@ -312,11 +312,12 @@ export class SsoLoginStrategy extends LoginStrategy { } } - private async trySetUserKeyWithDeviceKey(tokenResponse: IdentityTokenResponse): Promise<void> { + private async trySetUserKeyWithDeviceKey( + tokenResponse: IdentityTokenResponse, + userId: UserId, + ): Promise<void> { const trustedDeviceOption = tokenResponse.userDecryptionOptions?.trustedDeviceOption; - const userId = (await this.stateService.getUserId()) as UserId; - const deviceKey = await this.deviceTrustService.getDeviceKey(userId); const encDevicePrivateKey = trustedDeviceOption?.encryptedPrivateKey; const encUserKey = trustedDeviceOption?.encryptedUserKey; From a7958c1a569612a6fdee0b6aefd8ca36bd55fb8d Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Fri, 26 Apr 2024 10:23:11 -0400 Subject: [PATCH 296/351] Display `version_name` in AboutComponent (#8931) --- apps/browser/src/platform/browser/browser-api.ts | 4 ---- .../platform-utils/browser-platform-utils.service.ts | 8 +++++--- apps/browser/src/popup/settings/about.component.html | 2 +- apps/browser/src/popup/settings/about.component.ts | 12 +++++++----- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/apps/browser/src/platform/browser/browser-api.ts b/apps/browser/src/platform/browser/browser-api.ts index b793777d8b..f536eb8312 100644 --- a/apps/browser/src/platform/browser/browser-api.ts +++ b/apps/browser/src/platform/browser/browser-api.ts @@ -238,10 +238,6 @@ export class BrowserApi { return typeof window !== "undefined" && window === BrowserApi.getBackgroundPage(); } - static getApplicationVersion(): string { - return chrome.runtime.getManifest().version; - } - /** * Gets the extension views that match the given properties. This method is not * available within background service worker. As a result, it will return an diff --git a/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.ts b/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.ts index e9f7f17d9b..6e3b3aa403 100644 --- a/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.ts +++ b/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.ts @@ -175,11 +175,13 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic } getApplicationVersion(): Promise<string> { - return Promise.resolve(BrowserApi.getApplicationVersion()); + const manifest = chrome.runtime.getManifest(); + return Promise.resolve(manifest.version_name ?? manifest.version); } - async getApplicationVersionNumber(): Promise<string> { - return (await this.getApplicationVersion()).split(RegExp("[+|-]"))[0].trim(); + getApplicationVersionNumber(): Promise<string> { + const manifest = chrome.runtime.getManifest(); + return Promise.resolve(manifest.version.split(RegExp("[+|-]"))[0].trim()); } supportsWebAuthn(win: Window): boolean { diff --git a/apps/browser/src/popup/settings/about.component.html b/apps/browser/src/popup/settings/about.component.html index a4ad0ba801..e68a664ba7 100644 --- a/apps/browser/src/popup/settings/about.component.html +++ b/apps/browser/src/popup/settings/about.component.html @@ -5,7 +5,7 @@ <div bitDialogTitle>Bitwarden</div> <div bitDialogContent> <p>&copy; Bitwarden Inc. 2015-{{ year }}</p> - <p>{{ "version" | i18n }}: {{ version }}</p> + <p>{{ "version" | i18n }}: {{ version$ | async }}</p> <ng-container *ngIf="data$ | async as data"> <p *ngIf="data.isCloud"> {{ "serverVersion" | i18n }}: {{ data.serverConfig?.version }} diff --git a/apps/browser/src/popup/settings/about.component.ts b/apps/browser/src/popup/settings/about.component.ts index 61b5749b51..d7f98c1e7f 100644 --- a/apps/browser/src/popup/settings/about.component.ts +++ b/apps/browser/src/popup/settings/about.component.ts @@ -1,14 +1,13 @@ import { CommonModule } from "@angular/common"; import { Component } from "@angular/core"; -import { combineLatest, map } from "rxjs"; +import { Observable, combineLatest, defer, map } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ButtonModule, DialogModule } from "@bitwarden/components"; -import { BrowserApi } from "../../platform/browser/browser-api"; - @Component({ templateUrl: "about.component.html", standalone: true, @@ -16,7 +15,7 @@ import { BrowserApi } from "../../platform/browser/browser-api"; }) export class AboutComponent { protected year = new Date().getFullYear(); - protected version = BrowserApi.getApplicationVersion(); + protected version$: Observable<string>; protected data$ = combineLatest([ this.configService.serverConfig$, @@ -26,5 +25,8 @@ export class AboutComponent { constructor( private configService: ConfigService, private environmentService: EnvironmentService, - ) {} + private platformUtilsService: PlatformUtilsService, + ) { + this.version$ = defer(() => this.platformUtilsService.getApplicationVersion()); + } } From 67280f48dd86c4f50c36fea108bc8468528c0491 Mon Sep 17 00:00:00 2001 From: Robyn MacCallum <robyntmaccallum@gmail.com> Date: Fri, 26 Apr 2024 07:53:11 -0700 Subject: [PATCH 297/351] Make integrations links open in a new page (#8933) * Make integrations links open in a new page * Fix target warning --- .../integration-card/integration-card.component.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-card/integration-card.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-card/integration-card.component.html index 15b2519dae..5bb9ed425f 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-card/integration-card.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integration-card/integration-card.component.html @@ -18,7 +18,8 @@ <a class="tw-block tw-mb-0 tw-font-bold hover:tw-no-underline focus:tw-outline-none after:tw-content-[''] after:tw-block after:tw-absolute after:tw-w-full after:tw-h-full after:tw-left-0 after:tw-top-0" [href]="linkURL" - [rel]="[externalURL ? 'noopener noreferrer' : null]" + rel="noopener noreferrer" + target="_blank" > {{ linkText }} </a> From 4c1c09f07f3fd9955b918378907bd7803168d52a Mon Sep 17 00:00:00 2001 From: Matt Gibson <mgibson@bitwarden.com> Date: Fri, 26 Apr 2024 11:21:42 -0400 Subject: [PATCH 298/351] Use unique port names for derived states (#8937) --- .../platform/state/foreground-derived-state.provider.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/browser/src/platform/state/foreground-derived-state.provider.ts b/apps/browser/src/platform/state/foreground-derived-state.provider.ts index d9262e3b6e..f8b7b2e708 100644 --- a/apps/browser/src/platform/state/foreground-derived-state.provider.ts +++ b/apps/browser/src/platform/state/foreground-derived-state.provider.ts @@ -26,7 +26,12 @@ export class ForegroundDerivedStateProvider extends DefaultDerivedStateProvider _dependencies: TDeps, storageLocation: [string, AbstractStorageService & ObservableStorageService], ): DerivedState<TTo> { - const [cacheKey, storageService] = storageLocation; - return new ForegroundDerivedState(deriveDefinition, storageService, cacheKey, this.ngZone); + const [location, storageService] = storageLocation; + return new ForegroundDerivedState( + deriveDefinition, + storageService, + deriveDefinition.buildCacheKey(location), + this.ngZone, + ); } } From a4f1a3f13db4eb8e4491d91d104a08d872163d61 Mon Sep 17 00:00:00 2001 From: Matt Gibson <mgibson@bitwarden.com> Date: Fri, 26 Apr 2024 12:17:34 -0400 Subject: [PATCH 299/351] Use unique port names for derived states (#8938) --- .../src/platform/state/background-derived-state.provider.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/browser/src/platform/state/background-derived-state.provider.ts b/apps/browser/src/platform/state/background-derived-state.provider.ts index f3d217789e..834ae59249 100644 --- a/apps/browser/src/platform/state/background-derived-state.provider.ts +++ b/apps/browser/src/platform/state/background-derived-state.provider.ts @@ -18,12 +18,12 @@ export class BackgroundDerivedStateProvider extends DefaultDerivedStateProvider dependencies: TDeps, storageLocation: [string, AbstractStorageService & ObservableStorageService], ): DerivedState<TTo> { - const [cacheKey, storageService] = storageLocation; + const [location, storageService] = storageLocation; return new BackgroundDerivedState( parentState$, deriveDefinition, storageService, - cacheKey, + deriveDefinition.buildCacheKey(location), dependencies, ); } From b482a15d34661b05c8fbb5f9f7205a39bc937599 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Fri, 26 Apr 2024 14:41:57 -0400 Subject: [PATCH 300/351] Bandaid Folders Not Emitting (#8934) * Bandaid Folders Not Emitting * Remove VaultFilterComponent Change --- apps/browser/src/platform/state/foreground-derived-state.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/browser/src/platform/state/foreground-derived-state.ts b/apps/browser/src/platform/state/foreground-derived-state.ts index b9dda763df..b59f7bb889 100644 --- a/apps/browser/src/platform/state/foreground-derived-state.ts +++ b/apps/browser/src/platform/state/foreground-derived-state.ts @@ -56,7 +56,7 @@ export class ForegroundDerivedState<TTo> implements DerivedState<TTo> { return await this.getStoredValue(); }), - filter((s) => s.derived), + filter((s) => s?.derived === true), // A "remove" storage update will return us null map((s) => s.value), ); From b3242145f9812b11b3fd29e6017a1f27653d929d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 26 Apr 2024 14:59:15 -0400 Subject: [PATCH 301/351] [deps] Platform (CL): Update autoprefixer to v10.4.19 (#8735) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 10 +++++----- package.json | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index ad27ada66b..ba2f4d9f6f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -120,7 +120,7 @@ "@typescript-eslint/eslint-plugin": "7.4.0", "@typescript-eslint/parser": "7.4.0", "@webcomponents/custom-elements": "1.6.0", - "autoprefixer": "10.4.18", + "autoprefixer": "10.4.19", "base64-loader": "1.0.0", "chromatic": "10.9.6", "concurrently": "8.2.2", @@ -12930,9 +12930,9 @@ } }, "node_modules/autoprefixer": { - "version": "10.4.18", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.18.tgz", - "integrity": "sha512-1DKbDfsr6KUElM6wg+0zRNkB/Q7WcKYAaK+pzXn+Xqmszm/5Xa9coeNdtP88Vi+dPzZnMjhge8GIV49ZQkDa+g==", + "version": "10.4.19", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", + "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==", "dev": true, "funding": [ { @@ -12950,7 +12950,7 @@ ], "dependencies": { "browserslist": "^4.23.0", - "caniuse-lite": "^1.0.30001591", + "caniuse-lite": "^1.0.30001599", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.0.0", diff --git a/package.json b/package.json index 09065b234e..1a547e0a85 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ "@typescript-eslint/eslint-plugin": "7.4.0", "@typescript-eslint/parser": "7.4.0", "@webcomponents/custom-elements": "1.6.0", - "autoprefixer": "10.4.18", + "autoprefixer": "10.4.19", "base64-loader": "1.0.0", "chromatic": "10.9.6", "concurrently": "8.2.2", From 089f251a0c4e966a642451f19641ae6f5ab942b6 Mon Sep 17 00:00:00 2001 From: Matt Gibson <mgibson@bitwarden.com> Date: Fri, 26 Apr 2024 15:08:39 -0400 Subject: [PATCH 302/351] Remove memory storage cache from derived state. Use observable cache and port messaging (#8939) --- .../browser/src/background/main.background.ts | 2 +- .../derived-state-provider.factory.ts | 10 +- .../background-derived-state.provider.ts | 9 +- .../state/background-derived-state.ts | 25 ++-- .../state/derived-state-interactions.spec.ts | 34 ++--- .../foreground-derived-state.provider.ts | 17 +-- .../state/foreground-derived-state.spec.ts | 27 +--- .../state/foreground-derived-state.ts | 66 ++-------- .../src/popup/services/services.module.ts | 2 +- apps/cli/src/bw.ts | 2 +- apps/desktop/src/main.ts | 2 +- .../src/services/jslib-services.module.ts | 2 +- libs/common/spec/fake-state-provider.ts | 4 +- libs/common/spec/index.ts | 1 + libs/common/spec/observable-tracker.ts | 6 +- .../src/platform/state/derive-definition.ts | 4 +- .../default-derived-state.provider.ts | 26 +--- .../default-derived-state.spec.ts | 117 +++--------------- .../implementations/default-derived-state.ts | 21 +--- 19 files changed, 83 insertions(+), 294 deletions(-) diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 758c226bc3..82fda90489 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -490,7 +490,7 @@ export default class MainBackground { this.accountService, this.singleUserStateProvider, ); - this.derivedStateProvider = new BackgroundDerivedStateProvider(storageServiceProvider); + this.derivedStateProvider = new BackgroundDerivedStateProvider(); this.stateProvider = new DefaultStateProvider( this.activeUserStateProvider, this.singleUserStateProvider, diff --git a/apps/browser/src/platform/background/service-factories/derived-state-provider.factory.ts b/apps/browser/src/platform/background/service-factories/derived-state-provider.factory.ts index 4025d01950..3c3900144b 100644 --- a/apps/browser/src/platform/background/service-factories/derived-state-provider.factory.ts +++ b/apps/browser/src/platform/background/service-factories/derived-state-provider.factory.ts @@ -3,15 +3,10 @@ import { DerivedStateProvider } from "@bitwarden/common/platform/state"; import { BackgroundDerivedStateProvider } from "../../state/background-derived-state.provider"; import { CachedServices, FactoryOptions, factory } from "./factory-options"; -import { - StorageServiceProviderInitOptions, - storageServiceProviderFactory, -} from "./storage-service-provider.factory"; type DerivedStateProviderFactoryOptions = FactoryOptions; -export type DerivedStateProviderInitOptions = DerivedStateProviderFactoryOptions & - StorageServiceProviderInitOptions; +export type DerivedStateProviderInitOptions = DerivedStateProviderFactoryOptions; export async function derivedStateProviderFactory( cache: { derivedStateProvider?: DerivedStateProvider } & CachedServices, @@ -21,7 +16,6 @@ export async function derivedStateProviderFactory( cache, "derivedStateProvider", opts, - async () => - new BackgroundDerivedStateProvider(await storageServiceProviderFactory(cache, opts)), + async () => new BackgroundDerivedStateProvider(), ); } diff --git a/apps/browser/src/platform/state/background-derived-state.provider.ts b/apps/browser/src/platform/state/background-derived-state.provider.ts index 834ae59249..cbc5a34b37 100644 --- a/apps/browser/src/platform/state/background-derived-state.provider.ts +++ b/apps/browser/src/platform/state/background-derived-state.provider.ts @@ -1,9 +1,5 @@ import { Observable } from "rxjs"; -import { - AbstractStorageService, - ObservableStorageService, -} from "@bitwarden/common/platform/abstractions/storage.service"; import { DeriveDefinition, DerivedState } from "@bitwarden/common/platform/state"; // eslint-disable-next-line import/no-restricted-paths -- extending this class for this client import { DefaultDerivedStateProvider } from "@bitwarden/common/platform/state/implementations/default-derived-state.provider"; @@ -16,14 +12,11 @@ export class BackgroundDerivedStateProvider extends DefaultDerivedStateProvider parentState$: Observable<TFrom>, deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>, dependencies: TDeps, - storageLocation: [string, AbstractStorageService & ObservableStorageService], ): DerivedState<TTo> { - const [location, storageService] = storageLocation; return new BackgroundDerivedState( parentState$, deriveDefinition, - storageService, - deriveDefinition.buildCacheKey(location), + deriveDefinition.buildCacheKey(), dependencies, ); } diff --git a/apps/browser/src/platform/state/background-derived-state.ts b/apps/browser/src/platform/state/background-derived-state.ts index c62795acdc..61768cb970 100644 --- a/apps/browser/src/platform/state/background-derived-state.ts +++ b/apps/browser/src/platform/state/background-derived-state.ts @@ -1,10 +1,7 @@ -import { Observable, Subscription } from "rxjs"; +import { Observable, Subscription, concatMap } from "rxjs"; import { Jsonify } from "type-fest"; -import { - AbstractStorageService, - ObservableStorageService, -} from "@bitwarden/common/platform/abstractions/storage.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; import { DeriveDefinition } from "@bitwarden/common/platform/state"; // eslint-disable-next-line import/no-restricted-paths -- extending this class for this client import { DefaultDerivedState } from "@bitwarden/common/platform/state/implementations/default-derived-state"; @@ -22,11 +19,10 @@ export class BackgroundDerivedState< constructor( parentState$: Observable<TFrom>, deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>, - memoryStorage: AbstractStorageService & ObservableStorageService, portName: string, dependencies: TDeps, ) { - super(parentState$, deriveDefinition, memoryStorage, dependencies); + super(parentState$, deriveDefinition, dependencies); // listen for foreground derived states to connect BrowserApi.addListener(chrome.runtime.onConnect, (port) => { @@ -42,7 +38,20 @@ export class BackgroundDerivedState< }); port.onMessage.addListener(listenerCallback); - const stateSubscription = this.state$.subscribe(); + const stateSubscription = this.state$ + .pipe( + concatMap(async (state) => { + await this.sendMessage( + { + action: "nextState", + data: JSON.stringify(state), + id: Utils.newGuid(), + }, + port, + ); + }), + ) + .subscribe(); this.portSubscriptions.set(port, stateSubscription); }); diff --git a/apps/browser/src/platform/state/derived-state-interactions.spec.ts b/apps/browser/src/platform/state/derived-state-interactions.spec.ts index a5df01bc98..823c071a4c 100644 --- a/apps/browser/src/platform/state/derived-state-interactions.spec.ts +++ b/apps/browser/src/platform/state/derived-state-interactions.spec.ts @@ -4,14 +4,13 @@ */ import { NgZone } from "@angular/core"; -import { FakeStorageService } from "@bitwarden/common/../spec/fake-storage.service"; -import { awaitAsync, trackEmissions } from "@bitwarden/common/../spec/utils"; import { mock } from "jest-mock-extended"; import { Subject, firstValueFrom } from "rxjs"; import { DeriveDefinition } from "@bitwarden/common/platform/state"; // eslint-disable-next-line import/no-restricted-paths -- needed to define a derive definition import { StateDefinition } from "@bitwarden/common/platform/state/state-definition"; +import { awaitAsync, trackEmissions, ObservableTracker } from "@bitwarden/common/spec"; import { mockPorts } from "../../../spec/mock-port.spec-util"; @@ -22,6 +21,7 @@ const stateDefinition = new StateDefinition("test", "memory"); const deriveDefinition = new DeriveDefinition(stateDefinition, "test", { derive: (dateString: string) => (dateString == null ? null : new Date(dateString)), deserializer: (dateString: string) => (dateString == null ? null : new Date(dateString)), + cleanupDelayMs: 1000, }); // Mock out the runInsideAngular operator so we don't have to deal with zone.js @@ -35,7 +35,6 @@ describe("foreground background derived state interactions", () => { let foreground: ForegroundDerivedState<Date>; let background: BackgroundDerivedState<string, Date, Record<string, unknown>>; let parentState$: Subject<string>; - let memoryStorage: FakeStorageService; const initialParent = "2020-01-01"; const ngZone = mock<NgZone>(); const portName = "testPort"; @@ -43,16 +42,9 @@ describe("foreground background derived state interactions", () => { beforeEach(() => { mockPorts(); parentState$ = new Subject<string>(); - memoryStorage = new FakeStorageService(); - background = new BackgroundDerivedState( - parentState$, - deriveDefinition, - memoryStorage, - portName, - {}, - ); - foreground = new ForegroundDerivedState(deriveDefinition, memoryStorage, portName, ngZone); + background = new BackgroundDerivedState(parentState$, deriveDefinition, portName, {}); + foreground = new ForegroundDerivedState(deriveDefinition, portName, ngZone); }); afterEach(() => { @@ -72,21 +64,13 @@ describe("foreground background derived state interactions", () => { }); it("should initialize a late-connected foreground", async () => { - const newForeground = new ForegroundDerivedState( - deriveDefinition, - memoryStorage, - portName, - ngZone, - ); - const backgroundEmissions = trackEmissions(background.state$); + const newForeground = new ForegroundDerivedState(deriveDefinition, portName, ngZone); + const backgroundTracker = new ObservableTracker(background.state$); parentState$.next(initialParent); - await awaitAsync(); + const foregroundTracker = new ObservableTracker(newForeground.state$); - const foregroundEmissions = trackEmissions(newForeground.state$); - await awaitAsync(10); - - expect(backgroundEmissions).toEqual([new Date(initialParent)]); - expect(foregroundEmissions).toEqual([new Date(initialParent)]); + expect(await backgroundTracker.expectEmission()).toEqual(new Date(initialParent)); + expect(await foregroundTracker.expectEmission()).toEqual(new Date(initialParent)); }); describe("forceValue", () => { diff --git a/apps/browser/src/platform/state/foreground-derived-state.provider.ts b/apps/browser/src/platform/state/foreground-derived-state.provider.ts index f8b7b2e708..8b8d82b914 100644 --- a/apps/browser/src/platform/state/foreground-derived-state.provider.ts +++ b/apps/browser/src/platform/state/foreground-derived-state.provider.ts @@ -1,11 +1,6 @@ import { NgZone } from "@angular/core"; import { Observable } from "rxjs"; -import { - AbstractStorageService, - ObservableStorageService, -} from "@bitwarden/common/platform/abstractions/storage.service"; -import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider"; import { DeriveDefinition, DerivedState } from "@bitwarden/common/platform/state"; // eslint-disable-next-line import/no-restricted-paths -- extending this class for this client import { DefaultDerivedStateProvider } from "@bitwarden/common/platform/state/implementations/default-derived-state.provider"; @@ -14,23 +9,17 @@ import { DerivedStateDependencies } from "@bitwarden/common/src/types/state"; import { ForegroundDerivedState } from "./foreground-derived-state"; export class ForegroundDerivedStateProvider extends DefaultDerivedStateProvider { - constructor( - storageServiceProvider: StorageServiceProvider, - private ngZone: NgZone, - ) { - super(storageServiceProvider); + constructor(private ngZone: NgZone) { + super(); } override buildDerivedState<TFrom, TTo, TDeps extends DerivedStateDependencies>( _parentState$: Observable<TFrom>, deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>, _dependencies: TDeps, - storageLocation: [string, AbstractStorageService & ObservableStorageService], ): DerivedState<TTo> { - const [location, storageService] = storageLocation; return new ForegroundDerivedState( deriveDefinition, - storageService, - deriveDefinition.buildCacheKey(location), + deriveDefinition.buildCacheKey(), this.ngZone, ); } diff --git a/apps/browser/src/platform/state/foreground-derived-state.spec.ts b/apps/browser/src/platform/state/foreground-derived-state.spec.ts index 2c29f39bc1..ee224540c1 100644 --- a/apps/browser/src/platform/state/foreground-derived-state.spec.ts +++ b/apps/browser/src/platform/state/foreground-derived-state.spec.ts @@ -1,11 +1,5 @@ -/** - * need to update test environment so structuredClone works appropriately - * @jest-environment ../../libs/shared/test.environment.ts - */ - import { NgZone } from "@angular/core"; -import { awaitAsync, trackEmissions } from "@bitwarden/common/../spec"; -import { FakeStorageService } from "@bitwarden/common/../spec/fake-storage.service"; +import { awaitAsync } from "@bitwarden/common/../spec"; import { mock } from "jest-mock-extended"; import { DeriveDefinition } from "@bitwarden/common/platform/state"; @@ -32,15 +26,12 @@ jest.mock("../browser/run-inside-angular.operator", () => { describe("ForegroundDerivedState", () => { let sut: ForegroundDerivedState<Date>; - let memoryStorage: FakeStorageService; const portName = "testPort"; const ngZone = mock<NgZone>(); beforeEach(() => { - memoryStorage = new FakeStorageService(); - memoryStorage.internalUpdateValuesRequireDeserialization(true); mockPorts(); - sut = new ForegroundDerivedState(deriveDefinition, memoryStorage, portName, ngZone); + sut = new ForegroundDerivedState(deriveDefinition, portName, ngZone); }); afterEach(() => { @@ -67,18 +58,4 @@ describe("ForegroundDerivedState", () => { expect(disconnectSpy).toHaveBeenCalled(); expect(sut["port"]).toBeNull(); }); - - it("should emit when the memory storage updates", async () => { - const dateString = "2020-01-01"; - const emissions = trackEmissions(sut.state$); - - await memoryStorage.save(deriveDefinition.storageKey, { - derived: true, - value: new Date(dateString), - }); - - await awaitAsync(); - - expect(emissions).toEqual([new Date(dateString)]); - }); }); diff --git a/apps/browser/src/platform/state/foreground-derived-state.ts b/apps/browser/src/platform/state/foreground-derived-state.ts index b59f7bb889..6abe363876 100644 --- a/apps/browser/src/platform/state/foreground-derived-state.ts +++ b/apps/browser/src/platform/state/foreground-derived-state.ts @@ -6,19 +6,14 @@ import { filter, firstValueFrom, map, - merge, of, share, switchMap, tap, timer, } from "rxjs"; -import { Jsonify, JsonObject } from "type-fest"; +import { Jsonify } from "type-fest"; -import { - AbstractStorageService, - ObservableStorageService, -} from "@bitwarden/common/platform/abstractions/storage.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { DeriveDefinition, DerivedState } from "@bitwarden/common/platform/state"; import { DerivedStateDependencies } from "@bitwarden/common/types/state"; @@ -27,41 +22,28 @@ import { fromChromeEvent } from "../browser/from-chrome-event"; import { runInsideAngular } from "../browser/run-inside-angular.operator"; export class ForegroundDerivedState<TTo> implements DerivedState<TTo> { - private storageKey: string; private port: chrome.runtime.Port; private backgroundResponses$: Observable<DerivedStateMessage>; state$: Observable<TTo>; constructor( private deriveDefinition: DeriveDefinition<unknown, TTo, DerivedStateDependencies>, - private memoryStorage: AbstractStorageService & ObservableStorageService, private portName: string, private ngZone: NgZone, ) { - this.storageKey = deriveDefinition.storageKey; - - const initialStorageGet$ = defer(() => { - return this.getStoredValue(); - }).pipe( - filter((s) => s.derived), - map((s) => s.value), - ); - - const latestStorage$ = this.memoryStorage.updates$.pipe( - filter((s) => s.key === this.storageKey), - switchMap(async (storageUpdate) => { - if (storageUpdate.updateType === "remove") { - return null; - } - - return await this.getStoredValue(); - }), - filter((s) => s?.derived === true), // A "remove" storage update will return us null - map((s) => s.value), - ); + const latestValueFromPort$ = (port: chrome.runtime.Port) => { + return fromChromeEvent(port.onMessage).pipe( + map(([message]) => message as DerivedStateMessage), + filter((message) => message.originator === "background" && message.action === "nextState"), + map((message) => { + const json = JSON.parse(message.data) as Jsonify<TTo>; + return this.deriveDefinition.deserialize(json); + }), + ); + }; this.state$ = defer(() => of(this.initializePort())).pipe( - switchMap(() => merge(initialStorageGet$, latestStorage$)), + switchMap(() => latestValueFromPort$(this.port)), share({ connector: () => new ReplaySubject<TTo>(1), resetOnRefCountZero: () => @@ -130,28 +112,4 @@ export class ForegroundDerivedState<TTo> implements DerivedState<TTo> { this.port = null; this.backgroundResponses$ = null; } - - protected async getStoredValue(): Promise<{ derived: boolean; value: TTo | null }> { - if (this.memoryStorage.valuesRequireDeserialization) { - const storedJson = await this.memoryStorage.get< - Jsonify<{ derived: true; value: JsonObject }> - >(this.storageKey); - - if (!storedJson?.derived) { - return { derived: false, value: null }; - } - - const value = this.deriveDefinition.deserialize(storedJson.value as any); - - return { derived: true, value }; - } else { - const stored = await this.memoryStorage.get<{ derived: true; value: TTo }>(this.storageKey); - - if (!stored?.derived) { - return { derived: false, value: null }; - } - - return { derived: true, value: stored.value }; - } - } } diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 052e341004..5944783232 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -473,7 +473,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: DerivedStateProvider, useClass: ForegroundDerivedStateProvider, - deps: [StorageServiceProvider, NgZone], + deps: [NgZone], }), safeProvider({ provide: AutofillSettingsServiceAbstraction, diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index 45c394e912..85fba27089 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -314,7 +314,7 @@ export class Main { this.singleUserStateProvider, ); - this.derivedStateProvider = new DefaultDerivedStateProvider(storageServiceProvider); + this.derivedStateProvider = new DefaultDerivedStateProvider(); this.stateProvider = new DefaultStateProvider( this.activeUserStateProvider, diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index da4c14b4aa..0766af90b6 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -157,7 +157,7 @@ export class Main { activeUserStateProvider, singleUserStateProvider, globalStateProvider, - new DefaultDerivedStateProvider(storageServiceProvider), + new DefaultDerivedStateProvider(), ); this.environmentService = new DefaultEnvironmentService(stateProvider, accountService); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index c7b27a25c2..b28e475cb2 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -1047,7 +1047,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: DerivedStateProvider, useClass: DefaultDerivedStateProvider, - deps: [StorageServiceProvider], + deps: [], }), safeProvider({ provide: StateProvider, diff --git a/libs/common/spec/fake-state-provider.ts b/libs/common/spec/fake-state-provider.ts index 306ae00c21..2078fe3abd 100644 --- a/libs/common/spec/fake-state-provider.ts +++ b/libs/common/spec/fake-state-provider.ts @@ -249,11 +249,11 @@ export class FakeDerivedStateProvider implements DerivedStateProvider { deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>, dependencies: TDeps, ): DerivedState<TTo> { - let result = this.states.get(deriveDefinition.buildCacheKey("memory")) as DerivedState<TTo>; + let result = this.states.get(deriveDefinition.buildCacheKey()) as DerivedState<TTo>; if (result == null) { result = new FakeDerivedState(parentState$, deriveDefinition, dependencies); - this.states.set(deriveDefinition.buildCacheKey("memory"), result); + this.states.set(deriveDefinition.buildCacheKey(), result); } return result; } diff --git a/libs/common/spec/index.ts b/libs/common/spec/index.ts index 6e9af8400e..90ee121896 100644 --- a/libs/common/spec/index.ts +++ b/libs/common/spec/index.ts @@ -5,3 +5,4 @@ export * from "./fake-state-provider"; export * from "./fake-state"; export * from "./fake-account-service"; export * from "./fake-storage.service"; +export * from "./observable-tracker"; diff --git a/libs/common/spec/observable-tracker.ts b/libs/common/spec/observable-tracker.ts index a6f3e6a879..588d3c3365 100644 --- a/libs/common/spec/observable-tracker.ts +++ b/libs/common/spec/observable-tracker.ts @@ -16,9 +16,11 @@ export class ObservableTracker<T> { /** * 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 + * @returns The next emission from the observable + * @throws If the timeout is exceeded */ - async expectEmission(msTimeout = 50) { - await firstValueFrom( + async expectEmission(msTimeout = 50): Promise<T> { + return await firstValueFrom( this.observable.pipe( timeout({ first: msTimeout, diff --git a/libs/common/src/platform/state/derive-definition.ts b/libs/common/src/platform/state/derive-definition.ts index 9cb5eff3e8..8f62d3a342 100644 --- a/libs/common/src/platform/state/derive-definition.ts +++ b/libs/common/src/platform/state/derive-definition.ts @@ -171,8 +171,8 @@ export class DeriveDefinition<TFrom, TTo, TDeps extends DerivedStateDependencies return this.options.clearOnCleanup ?? true; } - buildCacheKey(location: string): string { - return `derived_${location}_${this.stateDefinition.name}_${this.uniqueDerivationName}`; + buildCacheKey(): string { + return `derived_${this.stateDefinition.name}_${this.uniqueDerivationName}`; } /** diff --git a/libs/common/src/platform/state/implementations/default-derived-state.provider.ts b/libs/common/src/platform/state/implementations/default-derived-state.provider.ts index 02d35fdf0c..3c8c39e21e 100644 --- a/libs/common/src/platform/state/implementations/default-derived-state.provider.ts +++ b/libs/common/src/platform/state/implementations/default-derived-state.provider.ts @@ -1,11 +1,6 @@ import { Observable } from "rxjs"; import { DerivedStateDependencies } from "../../../types/state"; -import { - AbstractStorageService, - ObservableStorageService, -} from "../../abstractions/storage.service"; -import { StorageServiceProvider } from "../../services/storage-service.provider"; import { DeriveDefinition } from "../derive-definition"; import { DerivedState } from "../derived-state"; import { DerivedStateProvider } from "../derived-state.provider"; @@ -15,18 +10,14 @@ import { DefaultDerivedState } from "./default-derived-state"; export class DefaultDerivedStateProvider implements DerivedStateProvider { private cache: Record<string, DerivedState<unknown>> = {}; - constructor(protected storageServiceProvider: StorageServiceProvider) {} + constructor() {} get<TFrom, TTo, TDeps extends DerivedStateDependencies>( parentState$: Observable<TFrom>, deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>, dependencies: TDeps, ): DerivedState<TTo> { - // TODO: we probably want to support optional normal memory storage for browser - const [location, storageService] = this.storageServiceProvider.get("memory", { - browser: "memory-large-object", - }); - const cacheKey = deriveDefinition.buildCacheKey(location); + const cacheKey = deriveDefinition.buildCacheKey(); const existingDerivedState = this.cache[cacheKey]; if (existingDerivedState != null) { // I have to cast out of the unknown generic but this should be safe if rules @@ -34,10 +25,7 @@ export class DefaultDerivedStateProvider implements DerivedStateProvider { return existingDerivedState as DefaultDerivedState<TFrom, TTo, TDeps>; } - const newDerivedState = this.buildDerivedState(parentState$, deriveDefinition, dependencies, [ - location, - storageService, - ]); + const newDerivedState = this.buildDerivedState(parentState$, deriveDefinition, dependencies); this.cache[cacheKey] = newDerivedState; return newDerivedState; } @@ -46,13 +34,7 @@ export class DefaultDerivedStateProvider implements DerivedStateProvider { parentState$: Observable<TFrom>, deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>, dependencies: TDeps, - storageLocation: [string, AbstractStorageService & ObservableStorageService], ): DerivedState<TTo> { - return new DefaultDerivedState<TFrom, TTo, TDeps>( - parentState$, - deriveDefinition, - storageLocation[1], - dependencies, - ); + return new DefaultDerivedState<TFrom, TTo, TDeps>(parentState$, deriveDefinition, dependencies); } } diff --git a/libs/common/src/platform/state/implementations/default-derived-state.spec.ts b/libs/common/src/platform/state/implementations/default-derived-state.spec.ts index e3b1587e3a..7e8d76bd20 100644 --- a/libs/common/src/platform/state/implementations/default-derived-state.spec.ts +++ b/libs/common/src/platform/state/implementations/default-derived-state.spec.ts @@ -5,7 +5,6 @@ import { Subject, firstValueFrom } from "rxjs"; import { awaitAsync, trackEmissions } from "../../../../spec"; -import { FakeStorageService } from "../../../../spec/fake-storage.service"; import { DeriveDefinition } from "../derive-definition"; import { StateDefinition } from "../state-definition"; @@ -29,7 +28,6 @@ const deriveDefinition = new DeriveDefinition<string, Date, { date: Date }>( describe("DefaultDerivedState", () => { let parentState$: Subject<string>; - let memoryStorage: FakeStorageService; let sut: DefaultDerivedState<string, Date, { date: Date }>; const deps = { date: new Date(), @@ -38,8 +36,7 @@ describe("DefaultDerivedState", () => { beforeEach(() => { callCount = 0; parentState$ = new Subject(); - memoryStorage = new FakeStorageService(); - sut = new DefaultDerivedState(parentState$, deriveDefinition, memoryStorage, deps); + sut = new DefaultDerivedState(parentState$, deriveDefinition, deps); }); afterEach(() => { @@ -66,71 +63,33 @@ describe("DefaultDerivedState", () => { expect(callCount).toBe(1); }); - it("should store the derived state in memory", async () => { - const dateString = "2020-01-01"; - trackEmissions(sut.state$); - parentState$.next(dateString); - await awaitAsync(); - - expect(memoryStorage.internalStore[deriveDefinition.storageKey]).toEqual( - derivedValue(new Date(dateString)), - ); - const calls = memoryStorage.mock.save.mock.calls; - expect(calls.length).toBe(1); - expect(calls[0][0]).toBe(deriveDefinition.storageKey); - expect(calls[0][1]).toEqual(derivedValue(new Date(dateString))); - }); - describe("forceValue", () => { const initialParentValue = "2020-01-01"; const forced = new Date("2020-02-02"); let emissions: Date[]; - describe("without observers", () => { - beforeEach(async () => { - parentState$.next(initialParentValue); - await awaitAsync(); - }); - - it("should store the forced value", async () => { - await sut.forceValue(forced); - expect(memoryStorage.internalStore[deriveDefinition.storageKey]).toEqual( - derivedValue(forced), - ); - }); + beforeEach(async () => { + emissions = trackEmissions(sut.state$); + parentState$.next(initialParentValue); + await awaitAsync(); }); - describe("with observers", () => { - beforeEach(async () => { - emissions = trackEmissions(sut.state$); - parentState$.next(initialParentValue); - await awaitAsync(); - }); + it("should force the value", async () => { + await sut.forceValue(forced); + expect(emissions).toEqual([new Date(initialParentValue), forced]); + }); - it("should store the forced value", async () => { - await sut.forceValue(forced); - expect(memoryStorage.internalStore[deriveDefinition.storageKey]).toEqual( - derivedValue(forced), - ); - }); + it("should only force the value once", async () => { + await sut.forceValue(forced); - it("should force the value", async () => { - await sut.forceValue(forced); - expect(emissions).toEqual([new Date(initialParentValue), forced]); - }); + parentState$.next(initialParentValue); + await awaitAsync(); - it("should only force the value once", async () => { - await sut.forceValue(forced); - - parentState$.next(initialParentValue); - await awaitAsync(); - - expect(emissions).toEqual([ - new Date(initialParentValue), - forced, - new Date(initialParentValue), - ]); - }); + expect(emissions).toEqual([ + new Date(initialParentValue), + forced, + new Date(initialParentValue), + ]); }); }); @@ -148,42 +107,6 @@ describe("DefaultDerivedState", () => { expect(parentState$.observed).toBe(false); }); - it("should clear state after cleanup", async () => { - const subscription = sut.state$.subscribe(); - parentState$.next(newDate); - await awaitAsync(); - - expect(memoryStorage.internalStore[deriveDefinition.storageKey]).toEqual( - derivedValue(new Date(newDate)), - ); - - subscription.unsubscribe(); - // Wait for cleanup - await awaitAsync(cleanupDelayMs * 2); - - expect(memoryStorage.internalStore[deriveDefinition.storageKey]).toBeUndefined(); - }); - - it("should not clear state after cleanup if clearOnCleanup is false", async () => { - deriveDefinition.options.clearOnCleanup = false; - - const subscription = sut.state$.subscribe(); - parentState$.next(newDate); - await awaitAsync(); - - expect(memoryStorage.internalStore[deriveDefinition.storageKey]).toEqual( - derivedValue(new Date(newDate)), - ); - - subscription.unsubscribe(); - // Wait for cleanup - await awaitAsync(cleanupDelayMs * 2); - - expect(memoryStorage.internalStore[deriveDefinition.storageKey]).toEqual( - derivedValue(new Date(newDate)), - ); - }); - it("should not cleanup if there are still subscribers", async () => { const subscription1 = sut.state$.subscribe(); const sub2Emissions: Date[] = []; @@ -260,7 +183,3 @@ describe("DefaultDerivedState", () => { }); }); }); - -function derivedValue<T>(value: T) { - return { derived: true, value }; -} diff --git a/libs/common/src/platform/state/implementations/default-derived-state.ts b/libs/common/src/platform/state/implementations/default-derived-state.ts index 657df2bfdf..9abb299809 100644 --- a/libs/common/src/platform/state/implementations/default-derived-state.ts +++ b/libs/common/src/platform/state/implementations/default-derived-state.ts @@ -1,10 +1,6 @@ import { Observable, ReplaySubject, Subject, concatMap, merge, share, timer } from "rxjs"; import { DerivedStateDependencies } from "../../../types/state"; -import { - AbstractStorageService, - ObservableStorageService, -} from "../../abstractions/storage.service"; import { DeriveDefinition } from "../derive-definition"; import { DerivedState } from "../derived-state"; @@ -22,7 +18,6 @@ export class DefaultDerivedState<TFrom, TTo, TDeps extends DerivedStateDependenc constructor( private parentState$: Observable<TFrom>, protected deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>, - private memoryStorage: AbstractStorageService & ObservableStorageService, private dependencies: TDeps, ) { this.storageKey = deriveDefinition.storageKey; @@ -34,7 +29,6 @@ export class DefaultDerivedState<TFrom, TTo, TDeps extends DerivedStateDependenc derivedStateOrPromise = await derivedStateOrPromise; } const derivedState = derivedStateOrPromise; - await this.storeValue(derivedState); return derivedState; }), ); @@ -44,26 +38,13 @@ export class DefaultDerivedState<TFrom, TTo, TDeps extends DerivedStateDependenc connector: () => { return new ReplaySubject<TTo>(1); }, - resetOnRefCountZero: () => - timer(this.deriveDefinition.cleanupDelayMs).pipe( - concatMap(async () => { - if (this.deriveDefinition.clearOnCleanup) { - await this.memoryStorage.remove(this.storageKey); - } - return true; - }), - ), + resetOnRefCountZero: () => timer(this.deriveDefinition.cleanupDelayMs), }), ); } async forceValue(value: TTo) { - await this.storeValue(value); this.forcedValueSubject.next(value); return value; } - - private storeValue(value: TTo) { - return this.memoryStorage.save(this.storageKey, { derived: true, value }); - } } From a8e4366ec05528f57751c9368ebd80b99ecfd43d Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Fri, 26 Apr 2024 15:08:59 -0400 Subject: [PATCH 303/351] Check that `self` is undefined instead of `window` (#8940) --- libs/common/src/platform/misc/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/common/src/platform/misc/utils.ts b/libs/common/src/platform/misc/utils.ts index 83a2da5709..326ed5e8e8 100644 --- a/libs/common/src/platform/misc/utils.ts +++ b/libs/common/src/platform/misc/utils.ts @@ -10,7 +10,7 @@ import { CryptoService } from "../abstractions/crypto.service"; import { EncryptService } from "../abstractions/encrypt.service"; import { I18nService } from "../abstractions/i18n.service"; -const nodeURL = typeof window === "undefined" ? require("url") : null; +const nodeURL = typeof self === "undefined" ? require("url") : null; declare global { /* eslint-disable-next-line no-var */ From 5dc200577c070ec17c9b8f9ec2d2f80429f4c79a Mon Sep 17 00:00:00 2001 From: Cesar Gonzalez <cesar.a.gonzalezcs@gmail.com> Date: Fri, 26 Apr 2024 14:15:36 -0500 Subject: [PATCH 304/351] [PM-7663] Update Build Pipeline for Beta Labelling (#8903) * [PM-7663] Update build pipeline for beta labeling * [PM-7663] Update build pipeline for beta labelling * [PM-7663] Update build pipeline for beta labelling * [PM-7663] Update build pipeline for beta labelling * [PM-7663] Update build pipeline for beta labelling * [PM-7663] Incorporate build workflow for the Chrome manifest v3 beta * [PM-7663] Update build pipeline for beta labeling * [PM-7663] Update build pipeline for beta labeling * [PM-7663] Update build pipeline for beta labeling * [PM-7663] Ensure we can have a valid version number based on the github run id * [PM-7663] Ensure we can have a valid version number based on the github run id * [PM-7663] Reverting change made to the run id, as it will not function * [PM-7663] Reverting change made to the run id, as it will not function * [PM-7663] Reverting change made to the run id, as it will not function * [PM-7663] Reverting change made to the run id, as it will not function * [PM-7663] Reverting a typo * Fix Duplicate `process.env * Learn how to use --------- Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> --- .github/workflows/build-browser.yml | 11 ++++++++++ apps/browser/gulpfile.js | 31 +++++++++++++++++++++++++++++ apps/browser/package.json | 4 ++++ 3 files changed, 46 insertions(+) diff --git a/.github/workflows/build-browser.yml b/.github/workflows/build-browser.yml index 23f4bd35f1..f924c5c98e 100644 --- a/.github/workflows/build-browser.yml +++ b/.github/workflows/build-browser.yml @@ -164,6 +164,10 @@ jobs: run: npm run dist:mv3 working-directory: browser-source/apps/browser + - name: Build Chrome Manifest v3 Beta + run: npm run dist:chrome:beta + working-directory: browser-source/apps/browser + - name: Gulp run: gulp ci working-directory: browser-source/apps/browser @@ -196,6 +200,13 @@ jobs: path: browser-source/apps/browser/dist/dist-chrome-mv3.zip if-no-files-found: error + - name: Upload Chrome MV3 Beta artifact (DO NOT USE FOR PROD) + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + with: + name: DO-NOT-USE-FOR-PROD-dist-chrome-MV3-beta-${{ env._BUILD_NUMBER }}.zip + path: browser-source/apps/browser/dist/dist-chrome-mv3-beta.zip + if-no-files-found: error + - name: Upload Firefox artifact uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 with: diff --git a/apps/browser/gulpfile.js b/apps/browser/gulpfile.js index 6a0980fc27..d5b29ffc38 100644 --- a/apps/browser/gulpfile.js +++ b/apps/browser/gulpfile.js @@ -35,6 +35,9 @@ function buildString() { if (process.env.MANIFEST_VERSION) { build = `-mv${process.env.MANIFEST_VERSION}`; } + if (process.env.BETA_BUILD === "1") { + build += "-beta"; + } if (process.env.BUILD_NUMBER && process.env.BUILD_NUMBER !== "") { build = `-${process.env.BUILD_NUMBER}`; } @@ -65,6 +68,9 @@ function distFirefox() { manifest.optional_permissions = manifest.optional_permissions.filter( (permission) => permission !== "privacy", ); + if (process.env.BETA_BUILD === "1") { + manifest = applyBetaLabels(manifest); + } return manifest; }); } @@ -72,6 +78,9 @@ function distFirefox() { function distOpera() { return dist("opera", (manifest) => { delete manifest.applications; + if (process.env.BETA_BUILD === "1") { + manifest = applyBetaLabels(manifest); + } return manifest; }); } @@ -81,6 +90,9 @@ function distChrome() { delete manifest.applications; delete manifest.sidebar_action; delete manifest.commands._execute_sidebar_action; + if (process.env.BETA_BUILD === "1") { + manifest = applyBetaLabels(manifest); + } return manifest; }); } @@ -90,6 +102,9 @@ function distEdge() { delete manifest.applications; delete manifest.sidebar_action; delete manifest.commands._execute_sidebar_action; + if (process.env.BETA_BUILD === "1") { + manifest = applyBetaLabels(manifest); + } return manifest; }); } @@ -210,6 +225,9 @@ async function safariCopyBuild(source, dest) { delete manifest.commands._execute_sidebar_action; delete manifest.optional_permissions; manifest.permissions.push("nativeMessaging"); + if (process.env.BETA_BUILD === "1") { + manifest = applyBetaLabels(manifest); + } return manifest; }), ), @@ -235,6 +253,19 @@ async function ciCoverage(cb) { .pipe(gulp.dest(paths.coverage)); } +function applyBetaLabels(manifest) { + manifest.name = "Bitwarden Password Manager BETA"; + manifest.short_name = "Bitwarden BETA"; + manifest.description = "THIS EXTENSION IS FOR BETA TESTING BITWARDEN."; + if (process.env.GITHUB_RUN_ID) { + manifest.version_name = `${manifest.version} beta - ${process.env.GITHUB_SHA.slice(0, 8)}`; + manifest.version = `${manifest.version}.${parseInt(process.env.GITHUB_RUN_ID.slice(-4))}`; + } else { + manifest.version = `${manifest.version}.0`; + } + return manifest; +} + exports["dist:firefox"] = distFirefox; exports["dist:chrome"] = distChrome; exports["dist:opera"] = distOpera; diff --git a/apps/browser/package.json b/apps/browser/package.json index 506f19f279..580acfc3d0 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -7,10 +7,14 @@ "build:watch": "webpack --watch", "build:watch:mv3": "cross-env MANIFEST_VERSION=3 webpack --watch", "build:prod": "cross-env NODE_ENV=production webpack", + "build:prod:beta": "cross-env BETA_BUILD=1 NODE_ENV=production webpack", "build:prod:watch": "cross-env NODE_ENV=production webpack --watch", "dist": "npm run build:prod && gulp dist", + "dist:beta": "npm run build:prod:beta && cross-env BETA_BUILD=1 gulp dist", "dist:mv3": "cross-env MANIFEST_VERSION=3 npm run build:prod && cross-env MANIFEST_VERSION=3 gulp dist", + "dist:mv3:beta": "cross-env MANIFEST_VERSION=3 npm run build:prod:beta && cross-env MANIFEST_VERSION=3 BETA_BUILD=1 gulp dist", "dist:chrome": "npm run build:prod && gulp dist:chrome", + "dist:chrome:beta": "cross-env MANIFEST_VERSION=3 npm run build:prod:beta && cross-env MANIFEST_VERSION=3 BETA_BUILD=1 gulp dist:chrome", "dist:firefox": "npm run build:prod && gulp dist:firefox", "dist:opera": "npm run build:prod && gulp dist:opera", "dist:safari": "npm run build:prod && gulp dist:safari", From 6ae086f89ad5a846bf637b218c187ac67f744445 Mon Sep 17 00:00:00 2001 From: Jake Fink <jfink@bitwarden.com> Date: Fri, 26 Apr 2024 18:02:45 -0400 Subject: [PATCH 305/351] pass userId when logging out and add error handling if one isn't found in background (#8946) --- apps/browser/src/background/main.background.ts | 15 +++++++++++++-- .../src/popup/settings/settings.component.ts | 5 ++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 82fda90489..67b763e82d 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1,4 +1,4 @@ -import { Subject, firstValueFrom, merge } from "rxjs"; +import { Subject, firstValueFrom, merge, timeout } from "rxjs"; import { PinCryptoServiceAbstraction, @@ -1196,7 +1196,18 @@ export default class MainBackground { } async logout(expired: boolean, userId?: UserId) { - userId ??= (await firstValueFrom(this.accountService.activeAccount$))?.id; + userId ??= ( + await firstValueFrom( + this.accountService.activeAccount$.pipe( + timeout({ + first: 2000, + with: () => { + throw new Error("No active account found to logout"); + }, + }), + ), + ) + )?.id; await this.eventUploadService.uploadEvents(userId as UserId); diff --git a/apps/browser/src/popup/settings/settings.component.ts b/apps/browser/src/popup/settings/settings.component.ts index fa6c64fcc5..c7e5b7dc95 100644 --- a/apps/browser/src/popup/settings/settings.component.ts +++ b/apps/browser/src/popup/settings/settings.component.ts @@ -21,6 +21,7 @@ import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaul import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { DeviceType } from "@bitwarden/common/enums"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; @@ -86,6 +87,7 @@ export class SettingsComponent implements OnInit { private destroy$ = new Subject<void>(); constructor( + private accountService: AccountService, private policyService: PolicyService, private formBuilder: FormBuilder, private platformUtilsService: PlatformUtilsService, @@ -434,8 +436,9 @@ export class SettingsComponent implements OnInit { type: "info", }); + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; if (confirmed) { - this.messagingService.send("logout"); + this.messagingService.send("logout", { userId: userId }); } } From 3282b9b775826906ab859337bb25721210788f50 Mon Sep 17 00:00:00 2001 From: Jake Fink <jfink@bitwarden.com> Date: Sat, 27 Apr 2024 10:51:43 -0400 Subject: [PATCH 306/351] add error handling to runtime.background messaging (#8949) --- apps/browser/src/background/runtime.background.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts index 294346fe9f..14eb228fb0 100644 --- a/apps/browser/src/background/runtime.background.ts +++ b/apps/browser/src/background/runtime.background.ts @@ -85,7 +85,11 @@ export default class RuntimeBackground { this.messageListener.allMessages$ .pipe( mergeMap(async (message: any) => { - await this.processMessage(message); + try { + await this.processMessage(message); + } catch (err) { + this.logService.error(err); + } }), ) .subscribe(); From 72f411b6e3503d500b9c455b05a71b06826e4155 Mon Sep 17 00:00:00 2001 From: findseat <166101790+findseat@users.noreply.github.com> Date: Sat, 27 Apr 2024 23:15:27 +0800 Subject: [PATCH 307/351] Signed-off-by: findseat <penglili@outlook.com> (#8636) Signed-off-by: findseat <penglili@outlook.com> --- .../account-switching/services/account-switcher.service.ts | 2 +- apps/browser/src/platform/popup/browser-popup-utils.spec.ts | 2 +- apps/desktop/src/platform/services/desktop-settings.service.ts | 2 +- libs/common/spec/observable-tracker.ts | 2 +- libs/common/src/auth/services/token.service.ts | 2 +- libs/common/src/platform/abstractions/crypto.service.ts | 2 +- libs/common/src/state-migrations/migration-helper.ts | 2 +- libs/components/src/dialog/dialogs.mdx | 2 +- libs/components/src/form/forms.mdx | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) 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 a73ec3e1f6..e5a3b8f8f5 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 @@ -110,7 +110,7 @@ export class AccountSwitcherService { }), ); - // Create a reusable observable that listens to the the switchAccountFinish message and returns the userId from the message + // Create a reusable observable that listens to the switchAccountFinish message and returns the userId from the message this.switchAccountFinished$ = fromChromeEvent<[message: { command: string; userId: string }]>( chrome.runtime.onMessage, ).pipe( diff --git a/apps/browser/src/platform/popup/browser-popup-utils.spec.ts b/apps/browser/src/platform/popup/browser-popup-utils.spec.ts index e84cd19a45..c2d33369bd 100644 --- a/apps/browser/src/platform/popup/browser-popup-utils.spec.ts +++ b/apps/browser/src/platform/popup/browser-popup-utils.spec.ts @@ -203,7 +203,7 @@ describe("BrowserPopupUtils", () => { expect(BrowserPopupUtils["buildPopoutUrl"]).not.toHaveBeenCalled(); }); - it("replaces any existing `uilocation=` query params within the passed extension url path to state the the uilocaiton is a popup", async () => { + it("replaces any existing `uilocation=` query params within the passed extension url path to state the uilocation is a popup", async () => { const url = "popup/index.html?uilocation=sidebar#/tabs/vault"; jest.spyOn(BrowserPopupUtils as any, "isSingleActionPopoutOpen").mockResolvedValueOnce(false); diff --git a/apps/desktop/src/platform/services/desktop-settings.service.ts b/apps/desktop/src/platform/services/desktop-settings.service.ts index d967e5fb1d..09ddad07c1 100644 --- a/apps/desktop/src/platform/services/desktop-settings.service.ts +++ b/apps/desktop/src/platform/services/desktop-settings.service.ts @@ -164,7 +164,7 @@ export class DesktopSettingsService { /** * Sets the setting for whether or not the application should be shown in the dock. - * @param value `true` if the application should should in the dock, `false` if it should not. + * @param value `true` if the application should show in the dock, `false` if it should not. */ async setAlwaysShowDock(value: boolean) { await this.alwaysShowDockState.update(() => value); diff --git a/libs/common/spec/observable-tracker.ts b/libs/common/spec/observable-tracker.ts index 588d3c3365..16fad869c3 100644 --- a/libs/common/spec/observable-tracker.ts +++ b/libs/common/spec/observable-tracker.ts @@ -30,7 +30,7 @@ export class ObservableTracker<T> { ); } - /** Awaits until the the total number of emissions observed by this tracker equals or exceeds {@link count} + /** Awaits until 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<T[]> { diff --git a/libs/common/src/auth/services/token.service.ts b/libs/common/src/auth/services/token.service.ts index 40036a8453..56311671ad 100644 --- a/libs/common/src/auth/services/token.service.ts +++ b/libs/common/src/auth/services/token.service.ts @@ -252,7 +252,7 @@ export class TokenService implements TokenServiceAbstraction { 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. + // and we have to return null here to properly indicate the user isn't logged in. return null; } diff --git a/libs/common/src/platform/abstractions/crypto.service.ts b/libs/common/src/platform/abstractions/crypto.service.ts index 79a58f9d57..f56714bfda 100644 --- a/libs/common/src/platform/abstractions/crypto.service.ts +++ b/libs/common/src/platform/abstractions/crypto.service.ts @@ -223,7 +223,7 @@ export abstract class CryptoService { */ abstract makeOrgKey<T extends OrgKey | ProviderKey>(): Promise<[EncString, T]>; /** - * Sets the the user's encrypted private key in storage and + * Sets 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 diff --git a/libs/common/src/state-migrations/migration-helper.ts b/libs/common/src/state-migrations/migration-helper.ts index 5b8e4ff93e..aab0a7e9c8 100644 --- a/libs/common/src/state-migrations/migration-helper.ts +++ b/libs/common/src/state-migrations/migration-helper.ts @@ -154,7 +154,7 @@ export class MigrationHelper { * * This is useful from creating migrations off of this paradigm, but should not be used once a value is migrated to a state provider. * - * @returns a list of all accounts that have been authenticated with state service, cast the the expected type. + * @returns a list of all accounts that have been authenticated with state service, cast the expected type. */ async getAccounts<ExpectedAccountType>(): Promise< { userId: string; account: ExpectedAccountType }[] diff --git a/libs/components/src/dialog/dialogs.mdx b/libs/components/src/dialog/dialogs.mdx index 8ff46ad381..bd6a30d7f2 100644 --- a/libs/components/src/dialog/dialogs.mdx +++ b/libs/components/src/dialog/dialogs.mdx @@ -50,7 +50,7 @@ element after close since a user may not want to close the dialog immediately if additional interactive elements. See [WCAG Focus Order success criteria](https://www.w3.org/WAI/WCAG21/Understanding/focus-order.html) -Once closed, focus should remain on the the element which triggered the Dialog. +Once closed, focus should remain on the element which triggered the Dialog. **Note:** If a Simple Dialog is triggered from a main Dialog, be sure to make sure focus is moved to the Simple Dialog. diff --git a/libs/components/src/form/forms.mdx b/libs/components/src/form/forms.mdx index a42ddccbe6..02f53ef3b2 100644 --- a/libs/components/src/form/forms.mdx +++ b/libs/components/src/form/forms.mdx @@ -16,7 +16,7 @@ always use the native `form` element and bind a `formGroup`. Forms consists of 1 or more inputs, and ends with 1 or 2 buttons. -If there are many inputs in a form, they should should be organized into sections as content +If there are many inputs in a form, they should be organized into sections as content relates. **Example:** Item type form Each input within a section should follow the following spacing guidelines (see From 0ecde075256a259972ecc7ed5ec015413f19ee58 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Sat, 27 Apr 2024 12:37:19 -0400 Subject: [PATCH 308/351] Run `npm run prettier` (#8950) --- libs/components/src/form/forms.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/components/src/form/forms.mdx b/libs/components/src/form/forms.mdx index 02f53ef3b2..6c6fa33f68 100644 --- a/libs/components/src/form/forms.mdx +++ b/libs/components/src/form/forms.mdx @@ -16,8 +16,8 @@ always use the native `form` element and bind a `formGroup`. Forms consists of 1 or more inputs, and ends with 1 or 2 buttons. -If there are many inputs in a form, they should be organized into sections as content -relates. **Example:** Item type form +If there are many inputs in a form, they should be organized into sections as content relates. +**Example:** Item type form Each input within a section should follow the following spacing guidelines (see [Tailwind CSS spacing documentation](https://tailwindcss.com/docs/customizing-spacing)): From 88eeebb0844028206dfb57c67226374366085252 Mon Sep 17 00:00:00 2001 From: Matt Gibson <mgibson@bitwarden.com> Date: Sat, 27 Apr 2024 16:32:34 -0400 Subject: [PATCH 309/351] Use a passed in key in derivation so we can validate other keys (#8954) * Use a passed in key in derivation so we can validate other keys * Fix user key type tests --- .../src/platform/services/crypto.service.ts | 16 +++++++++++++--- .../services/key-state/user-key.state.spec.ts | 16 ++++++---------- .../services/key-state/user-key.state.ts | 8 ++++---- 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/libs/common/src/platform/services/crypto.service.ts b/libs/common/src/platform/services/crypto.service.ts index 798173f513..713fe7d230 100644 --- a/libs/common/src/platform/services/crypto.service.ts +++ b/libs/common/src/platform/services/crypto.service.ts @@ -100,7 +100,7 @@ export class CryptoService implements CryptoServiceAbstraction { USER_PRIVATE_KEY, { encryptService: this.encryptService, - cryptoService: this, + getUserKey: (userId) => this.getUserKey(userId), }, ); this.activeUserPrivateKey$ = this.activeUserPrivateKeyState.state$; // may be null @@ -738,13 +738,23 @@ export class CryptoService implements CryptoServiceAbstraction { // Can decrypt private key const privateKey = await USER_PRIVATE_KEY.derive([userId, encPrivateKey], { encryptService: this.encryptService, - cryptoService: this, + getUserKey: () => Promise.resolve(key), }); + if (privateKey == null) { + // failed to decrypt + return false; + } + // Can successfully derive public key - await USER_PUBLIC_KEY.derive(privateKey, { + const publicKey = await USER_PUBLIC_KEY.derive(privateKey, { cryptoFunctionService: this.cryptoFunctionService, }); + + if (publicKey == null) { + // failed to decrypt + return false; + } } catch (e) { return false; } diff --git a/libs/common/src/platform/services/key-state/user-key.state.spec.ts b/libs/common/src/platform/services/key-state/user-key.state.spec.ts index cb758943e5..5c5c5ac70c 100644 --- a/libs/common/src/platform/services/key-state/user-key.state.spec.ts +++ b/libs/common/src/platform/services/key-state/user-key.state.spec.ts @@ -8,7 +8,6 @@ import { EncryptService } from "../../abstractions/encrypt.service"; import { EncryptionType } from "../../enums"; import { Utils } from "../../misc/utils"; import { EncString } from "../../models/domain/enc-string"; -import { CryptoService } from "../crypto.service"; import { USER_ENCRYPTED_PRIVATE_KEY, @@ -89,40 +88,37 @@ describe("Derived decrypted private key", () => { }); it("should derive decrypted private key", async () => { - const cryptoService = mock<CryptoService>(); - cryptoService.getUserKey.mockResolvedValue(userKey); + const getUserKey = jest.fn(async () => userKey); const encryptService = mock<EncryptService>(); encryptService.decryptToBytes.mockResolvedValue(decryptedPrivateKey); const result = await sut.derive([userId, encryptedPrivateKey], { encryptService, - cryptoService, + getUserKey, }); expect(result).toEqual(decryptedPrivateKey); }); it("should handle null input values", async () => { - const cryptoService = mock<CryptoService>(); - cryptoService.getUserKey.mockResolvedValue(userKey); + const getUserKey = jest.fn(async () => userKey); const encryptService = mock<EncryptService>(); const result = await sut.derive([userId, null], { encryptService, - cryptoService, + getUserKey, }); expect(result).toEqual(null); }); it("should handle null user key", async () => { - const cryptoService = mock<CryptoService>(); - cryptoService.getUserKey.mockResolvedValue(null); + const getUserKey = jest.fn(async () => null); const encryptService = mock<EncryptService>(); const result = await sut.derive([userId, encryptedPrivateKey], { encryptService, - cryptoService, + getUserKey, }); expect(result).toEqual(null); 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 609525b0ac..3df3b2044b 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 @@ -1,10 +1,10 @@ +import { UserId } from "../../../types/guid"; import { UserPrivateKey, UserPublicKey, UserKey } from "../../../types/key"; import { CryptoFunctionService } from "../../abstractions/crypto-function.service"; import { EncryptService } from "../../abstractions/encrypt.service"; import { EncString, EncryptedString } from "../../models/domain/enc-string"; import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key"; import { CRYPTO_DISK, DeriveDefinition, CRYPTO_MEMORY, UserKeyDefinition } from "../../state"; -import { CryptoService } from "../crypto.service"; export const USER_EVER_HAD_USER_KEY = new UserKeyDefinition<boolean>( CRYPTO_DISK, @@ -28,15 +28,15 @@ export const USER_PRIVATE_KEY = DeriveDefinition.fromWithUserId< EncryptedString, UserPrivateKey, // TODO: update cryptoService to user key directly - { encryptService: EncryptService; cryptoService: CryptoService } + { encryptService: EncryptService; getUserKey: (userId: UserId) => Promise<UserKey> } >(USER_ENCRYPTED_PRIVATE_KEY, { deserializer: (obj) => new Uint8Array(Object.values(obj)) as UserPrivateKey, - derive: async ([userId, encPrivateKeyString], { encryptService, cryptoService }) => { + derive: async ([userId, encPrivateKeyString], { encryptService, getUserKey }) => { if (encPrivateKeyString == null) { return null; } - const userKey = await cryptoService.getUserKey(userId); + const userKey = await getUserKey(userId); if (userKey == null) { return null; } From 7f497553de4a63e0d3aabb95e7f89e45c60711cb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 28 Apr 2024 13:44:23 -0400 Subject: [PATCH 310/351] [deps] SM: Update jest-mock-extended to v3.0.6 (#8961) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 23 ++++++++++++++--------- package.json | 2 +- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index ba2f4d9f6f..9585f1ea55 100644 --- a/package-lock.json +++ b/package-lock.json @@ -152,7 +152,7 @@ "html-webpack-plugin": "5.6.0", "husky": "9.0.11", "jest-junit": "16.0.0", - "jest-mock-extended": "3.0.5", + "jest-mock-extended": "3.0.6", "jest-preset-angular": "14.0.3", "lint-staged": "15.2.2", "mini-css-extract-plugin": "2.8.1", @@ -24335,12 +24335,12 @@ } }, "node_modules/jest-mock-extended": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/jest-mock-extended/-/jest-mock-extended-3.0.5.tgz", - "integrity": "sha512-/eHdaNPUAXe7f65gHH5urc8SbRVWjYxBqmCgax2uqOBJy8UUcCBMN1upj1eZ8y/i+IqpyEm4Kq0VKss/GCCTdw==", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/jest-mock-extended/-/jest-mock-extended-3.0.6.tgz", + "integrity": "sha512-DJuEoFzio0loqdX8NIwkbE9dgIXNzaj//pefOQxGkoivohpxbSQeNHCAiXkDNA/fmM4EIJDoZnSibP4w3dUJ9g==", "dev": true, "dependencies": { - "ts-essentials": "^7.0.3" + "ts-essentials": "^9.4.2" }, "peerDependencies": { "jest": "^24.0.0 || ^25.0.0 || ^26.0.0 || ^27.0.0 || ^28.0.0 || ^29.0.0", @@ -36278,12 +36278,17 @@ } }, "node_modules/ts-essentials": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-7.0.3.tgz", - "integrity": "sha512-8+gr5+lqO3G84KdiTSMRLtuyJ+nTBVRKuCrK4lidMPdVeEp0uqC875uE5NMcaA7YYMN7XsNiFQuMvasF8HT/xQ==", + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-9.4.2.tgz", + "integrity": "sha512-mB/cDhOvD7pg3YCLk2rOtejHjjdSi9in/IBYE13S+8WA5FBSraYf4V/ws55uvs0IvQ/l0wBOlXy5yBNZ9Bl8ZQ==", "dev": true, "peerDependencies": { - "typescript": ">=3.7.0" + "typescript": ">=4.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/ts-interface-checker": { diff --git a/package.json b/package.json index 1a547e0a85..e1d9420d77 100644 --- a/package.json +++ b/package.json @@ -113,7 +113,7 @@ "html-webpack-plugin": "5.6.0", "husky": "9.0.11", "jest-junit": "16.0.0", - "jest-mock-extended": "3.0.5", + "jest-mock-extended": "3.0.6", "jest-preset-angular": "14.0.3", "lint-staged": "15.2.2", "mini-css-extract-plugin": "2.8.1", From 42f1f965afdc553c3fefd137b29ed75cfe11cc8e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 28 Apr 2024 14:21:49 -0400 Subject: [PATCH 311/351] [deps] SM: Update typescript-eslint monorepo to v7.7.1 (#8962) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Colton Hurst <colton@coltonhurst.com> --- package-lock.json | 184 +++++++++++++++++++++++++++++++--------------- package.json | 4 +- 2 files changed, 127 insertions(+), 61 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9585f1ea55..d623e2d5a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -117,8 +117,8 @@ "@types/react": "16.14.57", "@types/retry": "0.12.5", "@types/zxcvbn": "4.4.4", - "@typescript-eslint/eslint-plugin": "7.4.0", - "@typescript-eslint/parser": "7.4.0", + "@typescript-eslint/eslint-plugin": "7.7.1", + "@typescript-eslint/parser": "7.7.1", "@webcomponents/custom-elements": "1.6.0", "autoprefixer": "10.4.19", "base64-loader": "1.0.0", @@ -10812,22 +10812,22 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "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==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.7.1.tgz", + "integrity": "sha512-KwfdWXJBOviaBVhxO3p5TJiLpNuh2iyXyjmWN0f1nU87pwyvfS0EmjC6ukQVYVFJd/K1+0NWGPDXiyEyQorn0Q==", "dev": true, "dependencies": { - "@eslint-community/regexpp": "^4.5.1", - "@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", + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.7.1", + "@typescript-eslint/type-utils": "7.7.1", + "@typescript-eslint/utils": "7.7.1", + "@typescript-eslint/visitor-keys": "7.7.1", "debug": "^4.3.4", "graphemer": "^1.4.0", - "ignore": "^5.2.4", + "ignore": "^5.3.1", "natural-compare": "^1.4.0", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -10847,15 +10847,15 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.4.0.tgz", - "integrity": "sha512-247ETeHgr9WTRMqHbbQdzwzhuyaJ8dPTuyuUEMANqzMRB1rj/9qFIuIXK7l0FX9i9FXbHeBQl/4uz6mYuCE7Aw==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.7.1.tgz", + "integrity": "sha512-ZksJLW3WF7o75zaBPScdW1Gbkwhd/lyeXGf1kQCxJaOeITscoSl0MjynVvCzuV5boUz/3fOI06Lz8La55mu29Q==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "7.4.0", - "@typescript-eslint/utils": "7.4.0", + "@typescript-eslint/typescript-estree": "7.7.1", + "@typescript-eslint/utils": "7.7.1", "debug": "^4.3.4", - "ts-api-utils": "^1.0.1" + "ts-api-utils": "^1.3.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -10874,18 +10874,18 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.4.0.tgz", - "integrity": "sha512-NQt9QLM4Tt8qrlBVY9lkMYzfYtNz8/6qwZg8pI3cMGlPnj6mOpRxxAm7BMJN9K0AiY+1BwJ5lVC650YJqYOuNg==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.7.1.tgz", + "integrity": "sha512-QUvBxPEaBXf41ZBbaidKICgVL8Hin0p6prQDu6bbetWo39BKbWJxRsErOzMNT1rXvTll+J7ChrbmMCXM9rsvOQ==", "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": "7.4.0", - "@typescript-eslint/types": "7.4.0", - "@typescript-eslint/typescript-estree": "7.4.0", - "semver": "^7.5.4" + "@types/json-schema": "^7.0.15", + "@types/semver": "^7.5.8", + "@typescript-eslint/scope-manager": "7.7.1", + "@typescript-eslint/types": "7.7.1", + "@typescript-eslint/typescript-estree": "7.7.1", + "semver": "^7.6.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -10898,6 +10898,39 @@ "eslint": "^8.56.0" } }, + "node_modules/@typescript-eslint/eslint-plugin/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==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/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==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/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/@typescript-eslint/experimental-utils": { "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.62.0.tgz", @@ -10918,15 +10951,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.4.0.tgz", - "integrity": "sha512-ZvKHxHLusweEUVwrGRXXUVzFgnWhigo4JurEj0dGF1tbcGh6buL+ejDdjxOQxv6ytcY1uhun1p2sm8iWStlgLQ==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.7.1.tgz", + "integrity": "sha512-vmPzBOOtz48F6JAGVS/kZYk4EkXao6iGrD838sp1w3NQQC0W8ry/q641KU4PrG7AKNAf56NOcR8GOpH8l9FPCw==", "dev": true, "dependencies": { - "@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", + "@typescript-eslint/scope-manager": "7.7.1", + "@typescript-eslint/types": "7.7.1", + "@typescript-eslint/typescript-estree": "7.7.1", + "@typescript-eslint/visitor-keys": "7.7.1", "debug": "^4.3.4" }, "engines": { @@ -10946,13 +10979,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.4.0.tgz", - "integrity": "sha512-68VqENG5HK27ypafqLVs8qO+RkNc7TezCduYrx8YJpXq2QGZ30vmNZGJJJC48+MVn4G2dCV8m5ZTVnzRexTVtw==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.7.1.tgz", + "integrity": "sha512-PytBif2SF+9SpEUKynYn5g1RHFddJUcyynGpztX3l/ik7KmZEv19WCMhUBkHXPU9es/VWGD3/zg3wg90+Dh2rA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.4.0", - "@typescript-eslint/visitor-keys": "7.4.0" + "@typescript-eslint/types": "7.7.1", + "@typescript-eslint/visitor-keys": "7.7.1" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -11047,9 +11080,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.4.0.tgz", - "integrity": "sha512-mjQopsbffzJskos5B4HmbsadSJQWaRK0UxqQ7GuNA9Ga4bEKeiO6b2DnB6cM6bpc8lemaPseh0H9B/wyg+J7rw==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.7.1.tgz", + "integrity": "sha512-AmPmnGW1ZLTpWa+/2omPrPfR7BcbUU4oha5VIbSbS1a1Tv966bklvLNXxp3mrbc+P2j4MNOTfDffNsk4o0c6/w==", "dev": true, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -11060,19 +11093,19 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.4.0.tgz", - "integrity": "sha512-A99j5AYoME/UBQ1ucEbbMEmGkN7SE0BvZFreSnTd1luq7yulcHdyGamZKizU7canpGDWGJ+Q6ZA9SyQobipePg==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.7.1.tgz", + "integrity": "sha512-CXe0JHCXru8Fa36dteXqmH2YxngKJjkQLjxzoj6LYwzZ7qZvgsLSc+eqItCrqIop8Vl2UKoAi0StVWu97FQZIQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.4.0", - "@typescript-eslint/visitor-keys": "7.4.0", + "@typescript-eslint/types": "7.7.1", + "@typescript-eslint/visitor-keys": "7.7.1", "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" + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -11087,6 +11120,39 @@ } } }, + "node_modules/@typescript-eslint/typescript-estree/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==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/typescript-estree/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==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/typescript-estree/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/@typescript-eslint/utils": { "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", @@ -11210,13 +11276,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.4.0.tgz", - "integrity": "sha512-0zkC7YM0iX5Y41homUUeW1CHtZR01K3ybjM1l6QczoMuay0XKtrb93kv95AxUGwdjGr64nNqnOCwmEl616N8CA==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.7.1.tgz", + "integrity": "sha512-gBL3Eq25uADw1LQ9kVpf3hRM+DWzs0uZknHYK3hq4jcTPqVCClHGDnB6UUUV2SFeBeA4KWHWbbLqmbGcZ4FYbw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.4.0", - "eslint-visitor-keys": "^3.4.1" + "@typescript-eslint/types": "7.7.1", + "eslint-visitor-keys": "^3.4.3" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -27864,9 +27930,9 @@ "dev": true }, "node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" diff --git a/package.json b/package.json index e1d9420d77..7d08021a90 100644 --- a/package.json +++ b/package.json @@ -78,8 +78,8 @@ "@types/react": "16.14.57", "@types/retry": "0.12.5", "@types/zxcvbn": "4.4.4", - "@typescript-eslint/eslint-plugin": "7.4.0", - "@typescript-eslint/parser": "7.4.0", + "@typescript-eslint/eslint-plugin": "7.7.1", + "@typescript-eslint/parser": "7.7.1", "@webcomponents/custom-elements": "1.6.0", "autoprefixer": "10.4.19", "base64-loader": "1.0.0", From 3caa6cb635878df24c4c06577a9d406e4a699958 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Mon, 29 Apr 2024 07:28:58 -0400 Subject: [PATCH 312/351] [PM-7766] Add `clientType` to MigrationHelper (#8945) * Add `clientType` to MigrationHelper * PM-7766 - Fix migration builder tests to take new clientType into account. Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> * PM-7766 - Add client type to migration builder tests. * PM-7766 - Fix migration-helper.spec tests. * PM-7766 - Fix migrator.spec.ts --------- Co-authored-by: Jared Snider <jsnider@bitwarden.com> --- .../browser/src/background/main.background.ts | 2 + .../migration-runner.factory.ts | 2 + .../src/popup/services/services.module.ts | 6 + apps/cli/src/bw.ts | 1 + .../src/app/services/services.module.ts | 6 + apps/desktop/src/main.ts | 2 + apps/web/src/app/core/core.module.ts | 6 + .../src/app/platform/web-migration-runner.ts | 5 +- libs/angular/src/services/injection-tokens.ts | 2 + .../src/services/jslib-services.module.ts | 3 +- .../migration-builder.service.spec.ts | 52 +++-- .../services/migration-runner.spec.ts | 3 +- .../src/platform/services/migration-runner.ts | 3 + .../migration-builder.spec.ts | 119 +++++----- .../state-migrations/migration-helper.spec.ts | 208 ++++++++++-------- .../src/state-migrations/migration-helper.ts | 3 + .../src/state-migrations/migrator.spec.ts | 82 +++---- 17 files changed, 295 insertions(+), 210 deletions(-) diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 67b763e82d..5aec6e01a4 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -71,6 +71,7 @@ import { } from "@bitwarden/common/autofill/services/user-notification-settings.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { DefaultBillingAccountProfileStateService } from "@bitwarden/common/billing/services/account/billing-account-profile-state.service"; +import { ClientType } from "@bitwarden/common/enums"; import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service"; import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -520,6 +521,7 @@ export default class MainBackground { this.storageService, this.logService, new MigrationBuilderService(), + ClientType.Browser, ); this.stateService = new DefaultBrowserStateService( diff --git a/apps/browser/src/platform/background/service-factories/migration-runner.factory.ts b/apps/browser/src/platform/background/service-factories/migration-runner.factory.ts index a49699a615..090531f7cf 100644 --- a/apps/browser/src/platform/background/service-factories/migration-runner.factory.ts +++ b/apps/browser/src/platform/background/service-factories/migration-runner.factory.ts @@ -1,3 +1,4 @@ +import { ClientType } from "@bitwarden/common/enums"; import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; @@ -27,6 +28,7 @@ export async function migrationRunnerFactory( await diskStorageServiceFactory(cache, opts), await logServiceFactory(cache, opts), new MigrationBuilderService(), + ClientType.Browser, ), ); } diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 5944783232..bec278aeeb 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -13,6 +13,7 @@ import { SYSTEM_THEME_OBSERVABLE, SafeInjectionToken, INTRAPROCESS_MESSAGING_SUBJECT, + CLIENT_TYPE, } from "@bitwarden/angular/services/injection-tokens"; import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module"; import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common"; @@ -45,6 +46,7 @@ import { UserNotificationSettingsServiceAbstraction, } from "@bitwarden/common/autofill/services/user-notification-settings.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; +import { ClientType } from "@bitwarden/common/enums"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; @@ -558,6 +560,10 @@ const safeProviders: SafeProvider[] = [ OBSERVABLE_LARGE_OBJECT_MEMORY_STORAGE, ], }), + safeProvider({ + provide: CLIENT_TYPE, + useValue: ClientType.Browser, + }), ]; @NgModule({ diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index 85fba27089..b3fb68fe63 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -344,6 +344,7 @@ export class Main { this.storageService, this.logService, new MigrationBuilderService(), + ClientType.Cli, ); this.stateService = new StateService( diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index b888df8013..1e3a7fdfa5 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -15,6 +15,7 @@ import { SafeInjectionToken, STATE_FACTORY, INTRAPROCESS_MESSAGING_SUBJECT, + CLIENT_TYPE, } 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"; @@ -25,6 +26,7 @@ import { KdfConfigService as KdfConfigServiceAbstraction } from "@bitwarden/comm import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; +import { ClientType } from "@bitwarden/common/enums"; 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"; @@ -275,6 +277,10 @@ const safeProviders: SafeProvider[] = [ useClass: NativeMessagingManifestService, deps: [], }), + safeProvider({ + provide: CLIENT_TYPE, + useValue: ClientType.Desktop, + }), ]; @NgModule({ diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 0766af90b6..d11fceeacc 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -6,6 +6,7 @@ import { Subject, 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 { ClientType } from "@bitwarden/common/enums"; 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"; @@ -190,6 +191,7 @@ export class Main { this.storageService, this.logService, new MigrationBuilderService(), + ClientType.Desktop, ); // TODO: this state service will have access to on disk storage, but not in memory storage. diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index 7a95650039..c60280014c 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -13,10 +13,12 @@ import { OBSERVABLE_DISK_LOCAL_STORAGE, WINDOW, SafeInjectionToken, + CLIENT_TYPE, } 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 { ClientType } from "@bitwarden/common/enums"; 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"; @@ -157,6 +159,10 @@ const safeProviders: SafeProvider[] = [ new DefaultThemeStateService(globalStateProvider, ThemeType.Light), deps: [GlobalStateProvider], }), + safeProvider({ + provide: CLIENT_TYPE, + useValue: ClientType.Web, + }), ]; @NgModule({ diff --git a/apps/web/src/app/platform/web-migration-runner.ts b/apps/web/src/app/platform/web-migration-runner.ts index 4bd1d2d4b5..392eeeae04 100644 --- a/apps/web/src/app/platform/web-migration-runner.ts +++ b/apps/web/src/app/platform/web-migration-runner.ts @@ -1,3 +1,4 @@ +import { ClientType } from "@bitwarden/common/enums"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -14,7 +15,7 @@ export class WebMigrationRunner extends MigrationRunner { migrationBuilderService: MigrationBuilderService, private diskLocalStorage: WindowStorageService, ) { - super(diskStorage, logService, migrationBuilderService); + super(diskStorage, logService, migrationBuilderService, ClientType.Web); } override async run(): Promise<void> { @@ -46,7 +47,7 @@ class WebMigrationHelper extends MigrationHelper { storageService: WindowStorageService, logService: LogService, ) { - super(currentVersion, storageService, logService, "web-disk-local"); + super(currentVersion, storageService, logService, "web-disk-local", ClientType.Web); this.diskLocalStorageService = storageService; } diff --git a/libs/angular/src/services/injection-tokens.ts b/libs/angular/src/services/injection-tokens.ts index 413fc5b530..b7989e7f32 100644 --- a/libs/angular/src/services/injection-tokens.ts +++ b/libs/angular/src/services/injection-tokens.ts @@ -1,6 +1,7 @@ import { InjectionToken } from "@angular/core"; import { Observable, Subject } from "rxjs"; +import { ClientType } from "@bitwarden/common/enums"; import { AbstractMemoryStorageService, AbstractStorageService, @@ -52,3 +53,4 @@ export const SYSTEM_THEME_OBSERVABLE = new SafeInjectionToken<Observable<ThemeTy export const INTRAPROCESS_MESSAGING_SUBJECT = new SafeInjectionToken<Subject<Message<object>>>( "INTRAPROCESS_MESSAGING_SUBJECT", ); +export const CLIENT_TYPE = new SafeInjectionToken<ClientType>("CLIENT_TYPE"); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index b28e475cb2..2f6167d676 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -276,6 +276,7 @@ import { SYSTEM_THEME_OBSERVABLE, WINDOW, INTRAPROCESS_MESSAGING_SUBJECT, + CLIENT_TYPE, } from "./injection-tokens"; import { ModalService } from "./modal.service"; @@ -1099,7 +1100,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: MigrationRunner, useClass: MigrationRunner, - deps: [AbstractStorageService, LogService, MigrationBuilderService], + deps: [AbstractStorageService, LogService, MigrationBuilderService, CLIENT_TYPE], }), safeProvider({ provide: MigrationBuilderService, 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 1330ea07a4..ee9508e8b1 100644 --- a/libs/common/src/platform/services/migration-builder.service.spec.ts +++ b/libs/common/src/platform/services/migration-builder.service.spec.ts @@ -1,6 +1,7 @@ import { mock } from "jest-mock-extended"; import { FakeStorageService } from "../../../spec/fake-storage.service"; +import { ClientType } from "../../enums"; import { MigrationHelper } from "../../state-migrations/migration-helper"; import { MigrationBuilderService } from "./migration-builder.service"; @@ -66,25 +67,38 @@ describe("MigrationBuilderService", () => { global: {}, }; - it.each([ - noAccounts, - nullAndUndefinedAccounts, - emptyAccountObject, - nullCommonAccountProperties, - emptyCommonAccountProperties, - nullGlobal, - undefinedGlobal, - emptyGlobalObject, - ])("should not produce migrations that throw when given data: %s", async (startingState) => { - const sut = new MigrationBuilderService(); + const startingStates = [ + { data: noAccounts, description: "No Accounts" }, + { data: nullAndUndefinedAccounts, description: "Null and Undefined Accounts" }, + { data: emptyAccountObject, description: "Empty Account Object" }, + { data: nullCommonAccountProperties, description: "Null Common Account Properties" }, + { data: emptyCommonAccountProperties, description: "Empty Common Account Properties" }, + { data: nullGlobal, description: "Null Global" }, + { data: undefinedGlobal, description: "Undefined Global" }, + { data: emptyGlobalObject, description: "Empty Global Object" }, + ]; - const helper = new MigrationHelper( - startingStateVersion, - new FakeStorageService(startingState), - mock(), - "general", - ); + const clientTypes = Object.values(ClientType); - await sut.build().migrate(helper); - }); + // Generate all possible test cases + const testCases = startingStates.flatMap((startingState) => + clientTypes.map((clientType) => ({ startingState, clientType })), + ); + + it.each(testCases)( + "should not produce migrations that throw when given $startingState.description for client $clientType", + async ({ startingState, clientType }) => { + const sut = new MigrationBuilderService(); + + const helper = new MigrationHelper( + startingStateVersion, + new FakeStorageService(startingState), + mock(), + "general", + clientType, + ); + + await sut.build().migrate(helper); + }, + ); }); diff --git a/libs/common/src/platform/services/migration-runner.spec.ts b/libs/common/src/platform/services/migration-runner.spec.ts index 3934137f66..fc0d98bc56 100644 --- a/libs/common/src/platform/services/migration-runner.spec.ts +++ b/libs/common/src/platform/services/migration-runner.spec.ts @@ -1,6 +1,7 @@ import { mock } from "jest-mock-extended"; import { awaitAsync } from "../../../spec"; +import { ClientType } from "../../enums"; import { CURRENT_VERSION } from "../../state-migrations"; import { MigrationBuilder } from "../../state-migrations/migration-builder"; import { LogService } from "../abstractions/log.service"; @@ -17,7 +18,7 @@ describe("MigrationRunner", () => { migrationBuilderService.build.mockReturnValue(mockMigrationBuilder); - const sut = new MigrationRunner(storage, logService, migrationBuilderService); + const sut = new MigrationRunner(storage, logService, migrationBuilderService, ClientType.Web); describe("migrate", () => { it("should not run migrations if state is empty", async () => { diff --git a/libs/common/src/platform/services/migration-runner.ts b/libs/common/src/platform/services/migration-runner.ts index 006031f7e5..9e3a6118af 100644 --- a/libs/common/src/platform/services/migration-runner.ts +++ b/libs/common/src/platform/services/migration-runner.ts @@ -1,3 +1,4 @@ +import { ClientType } from "../../enums"; import { waitForMigrations } from "../../state-migrations"; import { CURRENT_VERSION, currentVersion } from "../../state-migrations/migrate"; import { MigrationHelper } from "../../state-migrations/migration-helper"; @@ -11,6 +12,7 @@ export class MigrationRunner { protected diskStorage: AbstractStorageService, protected logService: LogService, protected migrationBuilderService: MigrationBuilderService, + private clientType: ClientType, ) {} async run(): Promise<void> { @@ -19,6 +21,7 @@ export class MigrationRunner { this.diskStorage, this.logService, "general", + this.clientType, ); if (migrationHelper.currentVersion < 0) { diff --git a/libs/common/src/state-migrations/migration-builder.spec.ts b/libs/common/src/state-migrations/migration-builder.spec.ts index 6a4ff8e6d4..59d85609e0 100644 --- a/libs/common/src/state-migrations/migration-builder.spec.ts +++ b/libs/common/src/state-migrations/migration-builder.spec.ts @@ -1,5 +1,8 @@ import { mock } from "jest-mock-extended"; +// eslint-disable-next-line import/no-restricted-paths +import { ClientType } from "../enums"; + import { MigrationBuilder } from "./migration-builder"; import { MigrationHelper } from "./migration-helper"; import { Migrator } from "./migrator"; @@ -72,65 +75,69 @@ describe("MigrationBuilder", () => { expect(migrations[1]).toMatchObject({ migrator: expect.any(TestMigrator), direction: "down" }); }); - describe("migrate", () => { - let migrator: TestMigrator; - let rollback_migrator: TestMigrator; + const clientTypes = Object.values(ClientType); - beforeEach(() => { - sut = sut.with(TestMigrator, 0, 1).rollback(TestMigrator, 1, 0); - migrator = (sut as any).migrations[0].migrator; - rollback_migrator = (sut as any).migrations[1].migrator; + describe.each(clientTypes)("for client %s", (clientType) => { + describe("migrate", () => { + let migrator: TestMigrator; + let rollback_migrator: TestMigrator; + + beforeEach(() => { + sut = sut.with(TestMigrator, 0, 1).rollback(TestMigrator, 1, 0); + migrator = (sut as any).migrations[0].migrator; + rollback_migrator = (sut as any).migrations[1].migrator; + }); + + it("should migrate", async () => { + const helper = new MigrationHelper(0, mock(), mock(), "general", clientType); + 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(), "general", clientType); + 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(), "general", clientType); + 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(), "general", clientType); + 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(), "general", clientType); + const migrate = jest.spyOn(migrator, "migrate"); + const rollback = jest.spyOn(rollback_migrator, "rollback"); + await sut.migrate(helper); + expect(migrate).not.toBeCalled(); + expect(rollback).not.toBeCalled(); + }); + + it("should not update version if the current version does not match the from version", async () => { + const helper = new MigrationHelper(3, mock(), mock(), "general", clientType); + const migrate = jest.spyOn(migrator, "updateVersion"); + const rollback = jest.spyOn(rollback_migrator, "updateVersion"); + await sut.migrate(helper); + expect(migrate).not.toBeCalled(); + expect(rollback).not.toBeCalled(); + }); }); - it("should migrate", async () => { - const helper = new MigrationHelper(0, mock(), mock(), "general"); - const spy = jest.spyOn(migrator, "migrate"); - await sut.migrate(helper); - expect(spy).toBeCalledWith(helper); + it("should be able to call instance methods", async () => { + const helper = new MigrationHelper(0, mock(), mock(), "general", clientType); + await sut.with(TestMigratorWithInstanceMethod, 0, 1).migrate(helper); }); - - it("should rollback", async () => { - 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(), "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(), "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(), "general"); - const migrate = jest.spyOn(migrator, "migrate"); - const rollback = jest.spyOn(rollback_migrator, "rollback"); - await sut.migrate(helper); - expect(migrate).not.toBeCalled(); - expect(rollback).not.toBeCalled(); - }); - - it("should not update version if the current version does not match the from version", async () => { - 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); - expect(migrate).not.toBeCalled(); - expect(rollback).not.toBeCalled(); - }); - }); - - it("should be able to call instance methods", async () => { - 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 f86cac8768..5f366f2597 100644 --- a/libs/common/src/state-migrations/migration-helper.spec.ts +++ b/libs/common/src/state-migrations/migration-helper.spec.ts @@ -2,6 +2,8 @@ import { MockProxy, mock } from "jest-mock-extended"; // eslint-disable-next-line import/no-restricted-paths -- Needed to print log messages import { FakeStorageService } from "../../spec/fake-storage.service"; +// eslint-disable-next-line import/no-restricted-paths -- Needed client type enum +import { ClientType } from "../enums"; // eslint-disable-next-line import/no-restricted-paths -- Needed to print log messages import { LogService } from "../platform/abstractions/log.service"; // eslint-disable-next-line import/no-restricted-paths -- Needed to interface with storage locations @@ -32,116 +34,129 @@ describe("RemoveLegacyEtmKeyMigrator", () => { let logService: MockProxy<LogService>; let sut: MigrationHelper; - beforeEach(() => { - logService = mock(); - storage = mock(); - storage.get.mockImplementation((key) => (exampleJSON as any)[key]); + const clientTypes = Object.values(ClientType); - sut = new MigrationHelper(0, storage, logService, "general"); - }); + describe.each(clientTypes)("for client %s", (clientType) => { + beforeEach(() => { + logService = mock(); + storage = mock(); + storage.get.mockImplementation((key) => (exampleJSON as any)[key]); - describe("get", () => { - it("should delegate to storage.get", async () => { - await sut.get("key"); - expect(storage.get).toHaveBeenCalledWith("key"); - }); - }); - - describe("set", () => { - it("should delegate to storage.save", async () => { - await sut.set("key", "value"); - expect(storage.save).toHaveBeenCalledWith("key", "value"); - }); - }); - - describe("getAccounts", () => { - it("should return all accounts", async () => { - const accounts = await sut.getAccounts(); - expect(accounts).toEqual([ - { userId: "c493ed01-4e08-4e88-abc7-332f380ca760", account: { otherStuff: "otherStuff1" } }, - { userId: "23e61a5f-2ece-4f5e-b499-f0bc489482a9", account: { otherStuff: "otherStuff2" } }, - ]); + sut = new MigrationHelper(0, storage, logService, "general", clientType); }); - it("should handle missing authenticatedAccounts", async () => { - storage.get.mockImplementation((key) => - key === "authenticatedAccounts" ? undefined : (exampleJSON as any)[key], - ); - const accounts = await sut.getAccounts(); - expect(accounts).toEqual([]); - }); - }); - - describe("getFromGlobal", () => { - it("should return the correct value", async () => { - sut.currentVersion = 9; - const value = await sut.getFromGlobal({ - stateDefinition: { name: "serviceName" }, - key: "key", + describe("get", () => { + it("should delegate to storage.get", async () => { + await sut.get("key"); + expect(storage.get).toHaveBeenCalledWith("key"); }); - expect(value).toEqual("global_serviceName_key"); }); - it("should throw if the current version is less than 9", () => { - expect(() => - sut.getFromGlobal({ stateDefinition: { name: "serviceName" }, key: "key" }), - ).toThrowError("No key builder should be used for versions prior to 9."); - }); - }); - - describe("setToGlobal", () => { - it("should set the correct value", async () => { - sut.currentVersion = 9; - await sut.setToGlobal({ stateDefinition: { name: "serviceName" }, key: "key" }, "new_value"); - expect(storage.save).toHaveBeenCalledWith("global_serviceName_key", "new_value"); + describe("set", () => { + it("should delegate to storage.save", async () => { + await sut.set("key", "value"); + expect(storage.save).toHaveBeenCalledWith("key", "value"); + }); }); - it("should throw if the current version is less than 9", () => { - expect(() => - sut.setToGlobal( + describe("getAccounts", () => { + it("should return all accounts", async () => { + const accounts = await sut.getAccounts(); + expect(accounts).toEqual([ + { + userId: "c493ed01-4e08-4e88-abc7-332f380ca760", + account: { otherStuff: "otherStuff1" }, + }, + { + userId: "23e61a5f-2ece-4f5e-b499-f0bc489482a9", + account: { otherStuff: "otherStuff2" }, + }, + ]); + }); + + it("should handle missing authenticatedAccounts", async () => { + storage.get.mockImplementation((key) => + key === "authenticatedAccounts" ? undefined : (exampleJSON as any)[key], + ); + const accounts = await sut.getAccounts(); + expect(accounts).toEqual([]); + }); + }); + + describe("getFromGlobal", () => { + it("should return the correct value", async () => { + sut.currentVersion = 9; + const value = await sut.getFromGlobal({ + stateDefinition: { name: "serviceName" }, + key: "key", + }); + expect(value).toEqual("global_serviceName_key"); + }); + + it("should throw if the current version is less than 9", () => { + expect(() => + sut.getFromGlobal({ stateDefinition: { name: "serviceName" }, key: "key" }), + ).toThrowError("No key builder should be used for versions prior to 9."); + }); + }); + + describe("setToGlobal", () => { + it("should set the correct value", async () => { + sut.currentVersion = 9; + await sut.setToGlobal( { stateDefinition: { name: "serviceName" }, key: "key" }, - "global_serviceName_key", - ), - ).toThrowError("No key builder should be used for versions prior to 9."); - }); - }); - - describe("getFromUser", () => { - it("should return the correct value", async () => { - sut.currentVersion = 9; - const value = await sut.getFromUser("userId", { - stateDefinition: { name: "serviceName" }, - key: "key", + "new_value", + ); + expect(storage.save).toHaveBeenCalledWith("global_serviceName_key", "new_value"); + }); + + it("should throw if the current version is less than 9", () => { + expect(() => + sut.setToGlobal( + { stateDefinition: { name: "serviceName" }, key: "key" }, + "global_serviceName_key", + ), + ).toThrowError("No key builder should be used for versions prior to 9."); }); - expect(value).toEqual("user_userId_serviceName_key"); }); - it("should throw if the current version is less than 9", () => { - expect(() => - sut.getFromUser("userId", { stateDefinition: { name: "serviceName" }, key: "key" }), - ).toThrowError("No key builder should be used for versions prior to 9."); - }); - }); + describe("getFromUser", () => { + it("should return the correct value", async () => { + sut.currentVersion = 9; + const value = await sut.getFromUser("userId", { + stateDefinition: { name: "serviceName" }, + key: "key", + }); + expect(value).toEqual("user_userId_serviceName_key"); + }); - describe("setToUser", () => { - it("should set the correct value", async () => { - sut.currentVersion = 9; - await sut.setToUser( - "userId", - { stateDefinition: { name: "serviceName" }, key: "key" }, - "new_value", - ); - expect(storage.save).toHaveBeenCalledWith("user_userId_serviceName_key", "new_value"); + it("should throw if the current version is less than 9", () => { + expect(() => + sut.getFromUser("userId", { stateDefinition: { name: "serviceName" }, key: "key" }), + ).toThrowError("No key builder should be used for versions prior to 9."); + }); }); - it("should throw if the current version is less than 9", () => { - expect(() => - sut.setToUser( + describe("setToUser", () => { + it("should set the correct value", async () => { + sut.currentVersion = 9; + await sut.setToUser( "userId", { stateDefinition: { name: "serviceName" }, key: "key" }, "new_value", - ), - ).toThrowError("No key builder should be used for versions prior to 9."); + ); + expect(storage.save).toHaveBeenCalledWith("user_userId_serviceName_key", "new_value"); + }); + + it("should throw if the current version is less than 9", () => { + expect(() => + sut.setToUser( + "userId", + { stateDefinition: { name: "serviceName" }, key: "key" }, + "new_value", + ), + ).toThrowError("No key builder should be used for versions prior to 9."); + }); }); }); }); @@ -151,6 +166,7 @@ export function mockMigrationHelper( storageJson: any, stateVersion = 0, type: MigrationHelperType = "general", + clientType: ClientType = ClientType.Web, ): MockProxy<MigrationHelper> { const logService: MockProxy<LogService> = mock(); const storage: MockProxy<AbstractStorageService> = mock(); @@ -158,7 +174,7 @@ export function mockMigrationHelper( storage.save.mockImplementation(async (key, value) => { (storageJson as any)[key] = value; }); - const helper = new MigrationHelper(stateVersion, storage, logService, type); + const helper = new MigrationHelper(stateVersion, storage, logService, type, clientType); const mockHelper = mock<MigrationHelper>(); mockHelper.get.mockImplementation((key) => helper.get(key)); @@ -295,7 +311,13 @@ export async function runMigrator< const allInjectedData = injectData(initalData, []); const fakeStorageService = new FakeStorageService(initalData); - const helper = new MigrationHelper(migrator.fromVersion, fakeStorageService, mock(), "general"); + const helper = new MigrationHelper( + migrator.fromVersion, + fakeStorageService, + mock(), + "general", + ClientType.Web, + ); // 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 aab0a7e9c8..2505e2b264 100644 --- a/libs/common/src/state-migrations/migration-helper.ts +++ b/libs/common/src/state-migrations/migration-helper.ts @@ -1,3 +1,5 @@ +// eslint-disable-next-line import/no-restricted-paths -- Needed to provide client type to migrations +import { ClientType } from "../enums"; // eslint-disable-next-line import/no-restricted-paths -- Needed to print log messages import { LogService } from "../platform/abstractions/log.service"; // eslint-disable-next-line import/no-restricted-paths -- Needed to interface with storage locations @@ -17,6 +19,7 @@ export class MigrationHelper { private storageService: AbstractStorageService, public logService: LogService, type: MigrationHelperType, + public clientType: ClientType, ) { this.type = type; } diff --git a/libs/common/src/state-migrations/migrator.spec.ts b/libs/common/src/state-migrations/migrator.spec.ts index d1189c25ea..4079dc3fda 100644 --- a/libs/common/src/state-migrations/migrator.spec.ts +++ b/libs/common/src/state-migrations/migrator.spec.ts @@ -1,5 +1,7 @@ import { mock, MockProxy } from "jest-mock-extended"; +// eslint-disable-next-line import/no-restricted-paths -- Needed client type enum +import { ClientType } from "../enums"; // eslint-disable-next-line import/no-restricted-paths -- Needed to print log messages import { LogService } from "../platform/abstractions/log.service"; // eslint-disable-next-line import/no-restricted-paths -- Needed to interface with storage locations @@ -23,52 +25,56 @@ describe("migrator default methods", () => { let helper: MigrationHelper; let sut: TestMigrator; - beforeEach(() => { - storage = mock(); - logService = mock(); - helper = new MigrationHelper(0, storage, logService, "general"); - sut = new TestMigrator(0, 1); - }); + const clientTypes = Object.values(ClientType); - describe("shouldMigrate", () => { - describe("up", () => { - it("should return true if the current version equals the from version", async () => { - expect(await sut.shouldMigrate(helper, "up")).toBe(true); + describe.each(clientTypes)("for client %s", (clientType) => { + beforeEach(() => { + storage = mock(); + logService = mock(); + helper = new MigrationHelper(0, storage, logService, "general", clientType); + sut = new TestMigrator(0, 1); + }); + + describe("shouldMigrate", () => { + describe("up", () => { + it("should return true if the current version equals the from version", async () => { + expect(await sut.shouldMigrate(helper, "up")).toBe(true); + }); + + it("should return false if the current version does not equal the from version", async () => { + helper.currentVersion = 1; + expect(await sut.shouldMigrate(helper, "up")).toBe(false); + }); }); - it("should return false if the current version does not equal the from version", async () => { - helper.currentVersion = 1; - expect(await sut.shouldMigrate(helper, "up")).toBe(false); + describe("down", () => { + it("should return true if the current version equals the to version", async () => { + helper.currentVersion = 1; + expect(await sut.shouldMigrate(helper, "down")).toBe(true); + }); + + it("should return false if the current version does not equal the to version", async () => { + expect(await sut.shouldMigrate(helper, "down")).toBe(false); + }); }); }); - describe("down", () => { - it("should return true if the current version equals the to version", async () => { - helper.currentVersion = 1; - expect(await sut.shouldMigrate(helper, "down")).toBe(true); + describe("updateVersion", () => { + describe("up", () => { + it("should update the version", async () => { + await sut.updateVersion(helper, "up"); + expect(storage.save).toBeCalledWith("stateVersion", 1); + expect(helper.currentVersion).toBe(1); + }); }); - it("should return false if the current version does not equal the to version", async () => { - expect(await sut.shouldMigrate(helper, "down")).toBe(false); - }); - }); - }); - - describe("updateVersion", () => { - describe("up", () => { - it("should update the version", async () => { - await sut.updateVersion(helper, "up"); - expect(storage.save).toBeCalledWith("stateVersion", 1); - expect(helper.currentVersion).toBe(1); - }); - }); - - describe("down", () => { - it("should update the version", async () => { - helper.currentVersion = 1; - await sut.updateVersion(helper, "down"); - expect(storage.save).toBeCalledWith("stateVersion", 0); - expect(helper.currentVersion).toBe(0); + describe("down", () => { + it("should update the version", async () => { + helper.currentVersion = 1; + await sut.updateVersion(helper, "down"); + expect(storage.save).toBeCalledWith("stateVersion", 0); + expect(helper.currentVersion).toBe(0); + }); }); }); }); From f07d1039c12678d74297045b439b13ac64473609 Mon Sep 17 00:00:00 2001 From: MtnBurrit0 <77340197+mimartin12@users.noreply.github.com> Date: Mon, 29 Apr 2024 08:25:59 -0600 Subject: [PATCH 313/351] Display commit status in GH summary (#8918) --- .github/workflows/deploy-web.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/deploy-web.yml b/.github/workflows/deploy-web.yml index 6a5d9f1405..b034136f58 100644 --- a/.github/workflows/deploy-web.yml +++ b/.github/workflows/deploy-web.yml @@ -230,6 +230,17 @@ jobs: url: https://github.com/bitwarden/clients/actions/runs/${{ github.run_id }} AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} + update-summary: + name: Display commit + needs: artifact-check + runs-on: ubuntu-22.04 + steps: + - name: Display commit SHA + run: | + REPO_URL="https://github.com/bitwarden/clients/commit" + COMMIT_SHA="${{ needs.artifact-check.outputs.artifact-build-commit }}" + echo ":steam_locomotive: View [commit]($REPO_URL/$COMMIT_SHA)" >> $GITHUB_STEP_SUMMARY + azure-deploy: name: Deploy Web Vault to ${{ inputs.environment }} Storage Account needs: From e8b2fab90858c488da3a0430c16ba26309591ed6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 29 Apr 2024 10:35:11 -0400 Subject: [PATCH 314/351] [deps] Autofill: Update tldts to v6.1.18 (#8965) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- apps/cli/package.json | 2 +- package-lock.json | 18 +++++++++--------- package.json | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/cli/package.json b/apps/cli/package.json index b06caacfd4..d6c449de48 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -71,7 +71,7 @@ "papaparse": "5.4.1", "proper-lockfile": "4.1.2", "rxjs": "7.8.1", - "tldts": "6.1.16", + "tldts": "6.1.18", "zxcvbn": "4.4.2" } } diff --git a/package-lock.json b/package-lock.json index d623e2d5a9..e7ff0692bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -67,7 +67,7 @@ "qrious": "4.0.2", "rxjs": "7.8.1", "tabbable": "6.2.0", - "tldts": "6.1.16", + "tldts": "6.1.18", "utf-8-validate": "6.0.3", "zone.js": "0.13.3", "zxcvbn": "4.4.2" @@ -224,7 +224,7 @@ "papaparse": "5.4.1", "proper-lockfile": "4.1.2", "rxjs": "7.8.1", - "tldts": "6.1.16", + "tldts": "6.1.18", "zxcvbn": "4.4.2" }, "bin": { @@ -36056,20 +36056,20 @@ "dev": true }, "node_modules/tldts": { - "version": "6.1.16", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.16.tgz", - "integrity": "sha512-X6VrQzW4RymhI1kBRvrWzYlRLXTftZpi7/s/9ZlDILA04yM2lNX7mBvkzDib9L4uSymHt8mBbeaielZMdsAkfQ==", + "version": "6.1.18", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.18.tgz", + "integrity": "sha512-F+6zjPFnFxZ0h6uGb8neQWwHQm8u3orZVFribsGq4eBgEVrzSkHxzWS2l6aKr19T1vXiOMFjqfff4fQt+WgJFg==", "dependencies": { - "tldts-core": "^6.1.16" + "tldts-core": "^6.1.18" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "6.1.16", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.16.tgz", - "integrity": "sha512-rxnuCux+zn3hMF57nBzr1m1qGZH7Od2ErbDZjVm04fk76cEynTg3zqvHjx5BsBl8lvRTjpzIhsEGMHDH/Hr2Vw==" + "version": "6.1.18", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.18.tgz", + "integrity": "sha512-e4wx32F/7dMBSZyKAx825Yte3U0PQtZZ0bkWxYQiwLteRVnQ5zM40fEbi0IyNtwQssgJAk3GCr7Q+w39hX0VKA==" }, "node_modules/tmp": { "version": "0.0.33", diff --git a/package.json b/package.json index 7d08021a90..8c0c79cce9 100644 --- a/package.json +++ b/package.json @@ -200,7 +200,7 @@ "qrious": "4.0.2", "rxjs": "7.8.1", "tabbable": "6.2.0", - "tldts": "6.1.16", + "tldts": "6.1.18", "utf-8-validate": "6.0.3", "zone.js": "0.13.3", "zxcvbn": "4.4.2" From 6365dcdc43b6b0df3c76c580ea0b61974dd97622 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 29 Apr 2024 10:57:14 -0400 Subject: [PATCH 315/351] [deps] Autofill: Update prettier-plugin-tailwindcss to v0.5.14 (#8964) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index e7ff0692bf..4c652b7419 100644 --- a/package-lock.json +++ b/package-lock.json @@ -161,7 +161,7 @@ "postcss": "8.4.38", "postcss-loader": "8.1.1", "prettier": "3.2.2", - "prettier-plugin-tailwindcss": "0.5.13", + "prettier-plugin-tailwindcss": "0.5.14", "process": "0.11.10", "react": "18.2.0", "react-dom": "18.2.0", @@ -31446,9 +31446,9 @@ } }, "node_modules/prettier-plugin-tailwindcss": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.5.13.tgz", - "integrity": "sha512-2tPWHCFNC+WRjAC4SIWQNSOdcL1NNkydXim8w7TDqlZi+/ulZYz2OouAI6qMtkggnPt7lGamboj6LcTMwcCvoQ==", + "version": "0.5.14", + "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.5.14.tgz", + "integrity": "sha512-Puaz+wPUAhFp8Lo9HuciYKM2Y2XExESjeT+9NQoVFXZsPPnc9VYss2SpxdQ6vbatmt8/4+SN0oe0I1cPDABg9Q==", "dev": true, "engines": { "node": ">=14.21.3" diff --git a/package.json b/package.json index 8c0c79cce9..6c1fac2e54 100644 --- a/package.json +++ b/package.json @@ -122,7 +122,7 @@ "postcss": "8.4.38", "postcss-loader": "8.1.1", "prettier": "3.2.2", - "prettier-plugin-tailwindcss": "0.5.13", + "prettier-plugin-tailwindcss": "0.5.14", "process": "0.11.10", "react": "18.2.0", "react-dom": "18.2.0", From bb0a65f6d61f79fa1cf0629f784c27602badea6c Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Tue, 30 Apr 2024 02:49:10 +1000 Subject: [PATCH 316/351] [AC-2523] Fix broken members dialog for Manage Users custom permission (#8968) * Let Manage Users permission edit collection access * Remove unused comment --- .../member-dialog/member-dialog.component.ts | 5 +++-- .../src/app/vault/core/views/collection-admin.view.ts | 10 ++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) 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 3cccd6e28f..a67bea39c0 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 @@ -619,7 +619,7 @@ export class MemberDialogComponent implements OnDestroy { } function mapCollectionToAccessItemView( - collection: CollectionView, + collection: CollectionAdminView, organization: Organization, flexibleCollectionsV1Enabled: boolean, accessSelection?: CollectionAccessSelectionView, @@ -631,7 +631,8 @@ function mapCollectionToAccessItemView( labelName: collection.name, listName: collection.name, readonly: - group !== undefined || !collection.canEdit(organization, flexibleCollectionsV1Enabled), + group !== undefined || + !collection.canEditUserAccess(organization, flexibleCollectionsV1Enabled), readonlyPermission: accessSelection ? convertToPermission(accessSelection) : undefined, viaGroupName: group?.name, }; diff --git a/apps/web/src/app/vault/core/views/collection-admin.view.ts b/apps/web/src/app/vault/core/views/collection-admin.view.ts index d942d42fb8..369c78636c 100644 --- a/apps/web/src/app/vault/core/views/collection-admin.view.ts +++ b/apps/web/src/app/vault/core/views/collection-admin.view.ts @@ -31,6 +31,9 @@ export class CollectionAdminView extends CollectionView { this.assigned = response.assigned; } + /** + * Whether the current user can edit the collection, including user and group access + */ override canEdit(org: Organization, flexibleCollectionsV1Enabled: boolean): boolean { return org?.flexibleCollections ? org?.canEditAnyCollection(flexibleCollectionsV1Enabled) || this.manage @@ -43,4 +46,11 @@ export class CollectionAdminView extends CollectionView { ? org?.canDeleteAnyCollection || (!org?.limitCollectionCreationDeletion && this.manage) : org?.canDeleteAnyCollection || (org?.canDeleteAssignedCollections && this.assigned); } + + /** + * Whether the user can modify user access to this collection + */ + canEditUserAccess(org: Organization, flexibleCollectionsV1Enabled: boolean): boolean { + return this.canEdit(org, flexibleCollectionsV1Enabled) || org.canManageUsers; + } } From 443da7f62db23ab1ff762e0d8ac68946451db95e Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Mon, 29 Apr 2024 13:40:44 -0400 Subject: [PATCH 317/351] Fix Beta Version (#8944) * Fix Beta Version * Add Comments * Fix Function Reference --- apps/browser/gulpfile.js | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/apps/browser/gulpfile.js b/apps/browser/gulpfile.js index d5b29ffc38..3fe2c44dd1 100644 --- a/apps/browser/gulpfile.js +++ b/apps/browser/gulpfile.js @@ -30,6 +30,19 @@ const filters = { safari: ["!build/safari/**/*"], }; +/** + * Converts a number to a tuple containing two Uint16's + * @param num {number} This number is expected to be a integer style number with no decimals + * + * @returns {number[]} A tuple containing two elements that are both numbers. + */ +function numToUint16s(num) { + var arr = new ArrayBuffer(4); + var view = new DataView(arr); + view.setUint32(0, num, false); + return [view.getUint16(0), view.getUint16(2)]; +} + function buildString() { var build = ""; if (process.env.MANIFEST_VERSION) { @@ -258,8 +271,19 @@ function applyBetaLabels(manifest) { manifest.short_name = "Bitwarden BETA"; manifest.description = "THIS EXTENSION IS FOR BETA TESTING BITWARDEN."; if (process.env.GITHUB_RUN_ID) { - manifest.version_name = `${manifest.version} beta - ${process.env.GITHUB_SHA.slice(0, 8)}`; - manifest.version = `${manifest.version}.${parseInt(process.env.GITHUB_RUN_ID.slice(-4))}`; + const existingVersionParts = manifest.version.split("."); // 3 parts expected 2024.4.0 + + // GITHUB_RUN_ID is a number like: 8853654662 + // which will convert to [ 4024, 3206 ] + // and a single incremented id of 8853654663 will become [ 4024, 3207 ] + const runIdParts = numToUint16s(parseInt(process.env.GITHUB_RUN_ID)); + + // Only use the first 2 parts from the given version number and base the other 2 numbers from the GITHUB_RUN_ID + // Example: 2024.4.4024.3206 + const betaVersion = `${existingVersionParts[0]}.${existingVersionParts[1]}.${runIdParts[0]}.${runIdParts[1]}`; + + manifest.version_name = `${betaVersion} beta - ${process.env.GITHUB_SHA.slice(0, 8)}`; + manifest.version = betaVersion; } else { manifest.version = `${manifest.version}.0`; } From 82ae2fe62cc4a82e59dc594d810be51ab78fa19d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 29 Apr 2024 15:57:37 -0400 Subject: [PATCH 318/351] [deps] Platform (CL): Update react monorepo (#8734) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 36 ++++++++++++++++++------------------ package.json | 8 ++++---- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4c652b7419..ba0119ca63 100644 --- a/package-lock.json +++ b/package-lock.json @@ -114,7 +114,7 @@ "@types/node-ipc": "9.2.3", "@types/papaparse": "5.3.14", "@types/proper-lockfile": "4.1.4", - "@types/react": "16.14.57", + "@types/react": "16.14.60", "@types/retry": "0.12.5", "@types/zxcvbn": "4.4.4", "@typescript-eslint/eslint-plugin": "7.7.1", @@ -163,8 +163,8 @@ "prettier": "3.2.2", "prettier-plugin-tailwindcss": "0.5.14", "process": "0.11.10", - "react": "18.2.0", - "react-dom": "18.2.0", + "react": "18.3.1", + "react-dom": "18.3.1", "regedit": "^3.0.3", "remark-gfm": "3.0.1", "rimraf": "5.0.5", @@ -10618,13 +10618,13 @@ "dev": true }, "node_modules/@types/react": { - "version": "16.14.57", - "resolved": "https://registry.npmjs.org/@types/react/-/react-16.14.57.tgz", - "integrity": "sha512-fuNq/GV1a6GgqSuVuC457vYeTbm4E1CUBQVZwSPxqYnRhIzSXCJ1gGqyv+PKhqLyfbKCga9dXHJDzv+4XE41fw==", + "version": "16.14.60", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.14.60.tgz", + "integrity": "sha512-wIFmnczGsTcgwCBeIYOuy2mdXEiKZ5znU/jNOnMZPQyCcIxauMGWlX0TNG4lZ7NxRKj7YUIZRneJQSSdB2jKgg==", "dev": true, "dependencies": { "@types/prop-types": "*", - "@types/scheduler": "*", + "@types/scheduler": "^0.16", "csstype": "^3.0.2" } }, @@ -32085,9 +32085,9 @@ } }, "node_modules/react": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", - "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "dev": true, "dependencies": { "loose-envify": "^1.1.0" @@ -32107,16 +32107,16 @@ } }, "node_modules/react-dom": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", - "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "dev": true, "dependencies": { "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" + "scheduler": "^0.23.2" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^18.3.1" } }, "node_modules/react-is": { @@ -33604,9 +33604,9 @@ } }, "node_modules/scheduler": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", - "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", "dev": true, "dependencies": { "loose-envify": "^1.1.0" diff --git a/package.json b/package.json index 6c1fac2e54..c73ae492f5 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "@types/node-ipc": "9.2.3", "@types/papaparse": "5.3.14", "@types/proper-lockfile": "4.1.4", - "@types/react": "16.14.57", + "@types/react": "16.14.60", "@types/retry": "0.12.5", "@types/zxcvbn": "4.4.4", "@typescript-eslint/eslint-plugin": "7.7.1", @@ -124,8 +124,8 @@ "prettier": "3.2.2", "prettier-plugin-tailwindcss": "0.5.14", "process": "0.11.10", - "react": "18.2.0", - "react-dom": "18.2.0", + "react": "18.3.1", + "react-dom": "18.3.1", "regedit": "^3.0.3", "remark-gfm": "3.0.1", "rimraf": "5.0.5", @@ -213,7 +213,7 @@ "replacestream": "4.0.3" }, "resolutions": { - "@types/react": "16.14.57" + "@types/react": "16.14.60" }, "lint-staged": { "*": "prettier --cache --ignore-unknown --write", From 20de053770a9411c46e1a675d0ed53b551297ac0 Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Mon, 29 Apr 2024 17:43:14 -0400 Subject: [PATCH 319/351] Auth/PM-7811 - Refactor User Auto Unlock Key Hydration Process To Remove Race Conditions (#8979) * PM-7811 - Refactor UserKeyInitService to UserAutoUnlockKeyService - remove active account listening logic as it introduced race conditions with user key memory retrieval happening before the user auto unlock key was set into memory. * PM-7811 - CLI - (1) Fix deps (2) On CLI init (pre command execution), if there is an active account, then set the user key in memory from the user auto unlock key. * PM-7811 - Browser Extension / desktop - (1) Update deps (2) Sets user key in memory if the auto unlock key is set on account switch and background init (must act on all accounts so that account switcher displays unlock status properly). * PM-7811 - Web - (1) Update deps (2) Sets user key in memory if the auto unlock key is set on init * PM-7811 - Fix account switcher service changes not being necessary. --- .../browser/src/background/main.background.ts | 23 ++- apps/cli/src/bw.ts | 17 +- apps/desktop/src/app/services/init.service.ts | 21 ++- apps/web/src/app/core/init.service.ts | 15 +- .../src/services/jslib-services.module.ts | 9 +- .../user-auto-unlock-key.service.spec.ts | 71 ++++++++ .../services/user-auto-unlock-key.service.ts | 36 ++++ .../services/user-key-init.service.spec.ts | 162 ------------------ .../services/user-key-init.service.ts | 57 ------ 9 files changed, 165 insertions(+), 246 deletions(-) create mode 100644 libs/common/src/platform/services/user-auto-unlock-key.service.spec.ts create mode 100644 libs/common/src/platform/services/user-auto-unlock-key.service.ts delete mode 100644 libs/common/src/platform/services/user-key-init.service.spec.ts delete mode 100644 libs/common/src/platform/services/user-key-init.service.ts diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 5aec6e01a4..2dc229e402 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -113,7 +113,7 @@ import { MemoryStorageService } from "@bitwarden/common/platform/services/memory import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; import { SystemService } from "@bitwarden/common/platform/services/system.service"; -import { UserKeyInitService } from "@bitwarden/common/platform/services/user-key-init.service"; +import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service"; import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service"; import { ActiveUserStateProvider, @@ -334,7 +334,7 @@ export default class MainBackground { billingAccountProfileStateService: BillingAccountProfileStateService; // eslint-disable-next-line rxjs/no-exposed-subjects -- Needed to give access to services module intraprocessMessagingSubject: Subject<Message<object>>; - userKeyInitService: UserKeyInitService; + userAutoUnlockKeyService: UserAutoUnlockKeyService; scriptInjectorService: BrowserScriptInjectorService; kdfConfigService: kdfConfigServiceAbstraction; @@ -1064,11 +1064,7 @@ export default class MainBackground { } } - this.userKeyInitService = new UserKeyInitService( - this.accountService, - this.cryptoService, - this.logService, - ); + this.userAutoUnlockKeyService = new UserAutoUnlockKeyService(this.cryptoService); } async bootstrap() { @@ -1079,7 +1075,18 @@ export default class MainBackground { // This is here instead of in in the InitService b/c we don't plan for // side effects to run in the Browser InitService. - this.userKeyInitService.listenForActiveUserChangesToSetUserKey(); + const accounts = await firstValueFrom(this.accountService.accounts$); + + const setUserKeyInMemoryPromises = []; + for (const userId of Object.keys(accounts) as UserId[]) { + // For each acct, we must await the process of setting the user key in memory + // if the auto user key is set to avoid race conditions of any code trying to access + // the user key from mem. + setUserKeyInMemoryPromises.push( + this.userAutoUnlockKeyService.setUserKeyInMemoryIfAutoUserKeySet(userId), + ); + } + await Promise.all(setUserKeyInMemoryPromises); await (this.i18nService as I18nService).init(); (this.eventUploadService as EventUploadService).init(true); diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index b3fb68fe63..4c2066dbf1 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -3,6 +3,7 @@ import * as path from "path"; import { program } from "commander"; import * as jsdom from "jsdom"; +import { firstValueFrom } from "rxjs"; import { InternalUserDecryptionOptionsServiceAbstraction, @@ -79,7 +80,7 @@ import { MigrationBuilderService } from "@bitwarden/common/platform/services/mig import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; import { StateService } from "@bitwarden/common/platform/services/state.service"; import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider"; -import { UserKeyInitService } from "@bitwarden/common/platform/services/user-key-init.service"; +import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service"; import { ActiveUserStateProvider, DerivedStateProvider, @@ -236,7 +237,7 @@ export class Main { biometricStateService: BiometricStateService; billingAccountProfileStateService: BillingAccountProfileStateService; providerApiService: ProviderApiServiceAbstraction; - userKeyInitService: UserKeyInitService; + userAutoUnlockKeyService: UserAutoUnlockKeyService; kdfConfigService: KdfConfigServiceAbstraction; constructor() { @@ -709,11 +710,7 @@ export class Main { this.providerApiService = new ProviderApiService(this.apiService); - this.userKeyInitService = new UserKeyInitService( - this.accountService, - this.cryptoService, - this.logService, - ); + this.userAutoUnlockKeyService = new UserAutoUnlockKeyService(this.cryptoService); } async run() { @@ -757,7 +754,11 @@ export class Main { this.containerService.attachToGlobal(global); await this.i18nService.init(); this.twoFactorService.init(); - this.userKeyInitService.listenForActiveUserChangesToSetUserKey(); + + const activeAccount = await firstValueFrom(this.accountService.activeAccount$); + if (activeAccount) { + await this.userAutoUnlockKeyService.setUserKeyInMemoryIfAutoUserKeySet(activeAccount.id); + } } } diff --git a/apps/desktop/src/app/services/init.service.ts b/apps/desktop/src/app/services/init.service.ts index ae2e1ba97c..0452e9be83 100644 --- a/apps/desktop/src/app/services/init.service.ts +++ b/apps/desktop/src/app/services/init.service.ts @@ -1,10 +1,12 @@ import { DOCUMENT } from "@angular/common"; import { Inject, Injectable } from "@angular/core"; +import { firstValueFrom } from "rxjs"; import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction"; import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service"; import { NotificationsService as NotificationsServiceAbstraction } from "@bitwarden/common/abstractions/notifications.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; @@ -12,9 +14,10 @@ import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platfor import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; -import { UserKeyInitService } from "@bitwarden/common/platform/services/user-key-init.service"; +import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service"; import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service"; import { VaultTimeoutService } from "@bitwarden/common/services/vault-timeout/vault-timeout.service"; +import { UserId } from "@bitwarden/common/types/guid"; import { SyncService as SyncServiceAbstraction } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { I18nRendererService } from "../../platform/services/i18n.renderer.service"; @@ -36,7 +39,8 @@ export class InitService { private nativeMessagingService: NativeMessagingService, private themingService: AbstractThemingService, private encryptService: EncryptService, - private userKeyInitService: UserKeyInitService, + private userAutoUnlockKeyService: UserAutoUnlockKeyService, + private accountService: AccountService, @Inject(DOCUMENT) private document: Document, ) {} @@ -44,7 +48,18 @@ export class InitService { return async () => { this.nativeMessagingService.init(); await this.stateService.init({ runMigrations: false }); // Desktop will run them in main process - this.userKeyInitService.listenForActiveUserChangesToSetUserKey(); + + const accounts = await firstValueFrom(this.accountService.accounts$); + const setUserKeyInMemoryPromises = []; + for (const userId of Object.keys(accounts) as UserId[]) { + // For each acct, we must await the process of setting the user key in memory + // if the auto user key is set to avoid race conditions of any code trying to access + // the user key from mem. + setUserKeyInMemoryPromises.push( + this.userAutoUnlockKeyService.setUserKeyInMemoryIfAutoUserKeySet(userId), + ); + } + await Promise.all(setUserKeyInMemoryPromises); // 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/core/init.service.ts b/apps/web/src/app/core/init.service.ts index dab6ed5e3d..55dc1544ff 100644 --- a/apps/web/src/app/core/init.service.ts +++ b/apps/web/src/app/core/init.service.ts @@ -1,17 +1,19 @@ import { DOCUMENT } from "@angular/common"; import { Inject, Injectable } from "@angular/core"; +import { firstValueFrom } from "rxjs"; import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction"; import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service"; import { NotificationsService as NotificationsServiceAbstraction } from "@bitwarden/common/abstractions/notifications.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service"; 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 { ContainerService } from "@bitwarden/common/platform/services/container.service"; -import { UserKeyInitService } from "@bitwarden/common/platform/services/user-key-init.service"; +import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service"; import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service"; import { VaultTimeoutService } from "@bitwarden/common/services/vault-timeout/vault-timeout.service"; @@ -28,14 +30,21 @@ export class InitService { private cryptoService: CryptoServiceAbstraction, private themingService: AbstractThemingService, private encryptService: EncryptService, - private userKeyInitService: UserKeyInitService, + private userAutoUnlockKeyService: UserAutoUnlockKeyService, + private accountService: AccountService, @Inject(DOCUMENT) private document: Document, ) {} init() { return async () => { await this.stateService.init(); - this.userKeyInitService.listenForActiveUserChangesToSetUserKey(); + + const activeAccount = await firstValueFrom(this.accountService.activeAccount$); + if (activeAccount) { + // If there is an active account, we must await the process of setting the user key in memory + // if the auto user key is set to avoid race conditions of any code trying to access the user key from mem. + await this.userAutoUnlockKeyService.setUserKeyInMemoryIfAutoUserKeySet(activeAccount.id); + } setTimeout(() => this.notificationsService.init(), 3000); await this.vaultTimeoutService.init(true); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 2f6167d676..46d739d0f5 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -53,7 +53,6 @@ import { ProviderApiService } from "@bitwarden/common/admin-console/services/pro import { ProviderService } from "@bitwarden/common/admin-console/services/provider.service"; import { AccountApiService as AccountApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/account-api.service"; import { - AccountService, AccountService as AccountServiceAbstraction, InternalAccountService, } from "@bitwarden/common/auth/abstractions/account.service"; @@ -162,7 +161,7 @@ import { MigrationRunner } from "@bitwarden/common/platform/services/migration-r import { NoopNotificationsService } from "@bitwarden/common/platform/services/noop-notifications.service"; import { StateService } from "@bitwarden/common/platform/services/state.service"; import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider"; -import { UserKeyInitService } from "@bitwarden/common/platform/services/user-key-init.service"; +import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service"; import { ValidationService } from "@bitwarden/common/platform/services/validation.service"; import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service"; import { @@ -1128,9 +1127,9 @@ const safeProviders: SafeProvider[] = [ deps: [StateProvider], }), safeProvider({ - provide: UserKeyInitService, - useClass: UserKeyInitService, - deps: [AccountService, CryptoServiceAbstraction, LogService], + provide: UserAutoUnlockKeyService, + useClass: UserAutoUnlockKeyService, + deps: [CryptoServiceAbstraction], }), safeProvider({ provide: ErrorHandler, diff --git a/libs/common/src/platform/services/user-auto-unlock-key.service.spec.ts b/libs/common/src/platform/services/user-auto-unlock-key.service.spec.ts new file mode 100644 index 0000000000..f0d60158c1 --- /dev/null +++ b/libs/common/src/platform/services/user-auto-unlock-key.service.spec.ts @@ -0,0 +1,71 @@ +import { mock } from "jest-mock-extended"; + +import { CsprngArray } from "../../types/csprng"; +import { UserId } from "../../types/guid"; +import { UserKey } from "../../types/key"; +import { KeySuffixOptions } from "../enums"; +import { Utils } from "../misc/utils"; +import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; + +import { CryptoService } from "./crypto.service"; +import { UserAutoUnlockKeyService } from "./user-auto-unlock-key.service"; + +describe("UserAutoUnlockKeyService", () => { + let userAutoUnlockKeyService: UserAutoUnlockKeyService; + + const mockUserId = Utils.newGuid() as UserId; + + const cryptoService = mock<CryptoService>(); + + beforeEach(() => { + userAutoUnlockKeyService = new UserAutoUnlockKeyService(cryptoService); + }); + + describe("setUserKeyInMemoryIfAutoUserKeySet", () => { + it("does nothing if the userId is null", async () => { + // Act + await (userAutoUnlockKeyService as any).setUserKeyInMemoryIfAutoUserKeySet(null); + + // Assert + expect(cryptoService.getUserKeyFromStorage).not.toHaveBeenCalled(); + expect(cryptoService.setUserKey).not.toHaveBeenCalled(); + }); + + it("does nothing if the autoUserKey is null", async () => { + // Arrange + const userId = mockUserId; + + cryptoService.getUserKeyFromStorage.mockResolvedValue(null); + + // Act + await (userAutoUnlockKeyService as any).setUserKeyInMemoryIfAutoUserKeySet(userId); + + // Assert + expect(cryptoService.getUserKeyFromStorage).toHaveBeenCalledWith( + KeySuffixOptions.Auto, + userId, + ); + expect(cryptoService.setUserKey).not.toHaveBeenCalled(); + }); + + it("sets the user key in memory if the autoUserKey is not null", async () => { + // Arrange + const userId = mockUserId; + + const mockRandomBytes = new Uint8Array(64) as CsprngArray; + const mockAutoUserKey: UserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey; + + cryptoService.getUserKeyFromStorage.mockResolvedValue(mockAutoUserKey); + + // Act + await (userAutoUnlockKeyService as any).setUserKeyInMemoryIfAutoUserKeySet(userId); + + // Assert + expect(cryptoService.getUserKeyFromStorage).toHaveBeenCalledWith( + KeySuffixOptions.Auto, + userId, + ); + expect(cryptoService.setUserKey).toHaveBeenCalledWith(mockAutoUserKey, userId); + }); + }); +}); diff --git a/libs/common/src/platform/services/user-auto-unlock-key.service.ts b/libs/common/src/platform/services/user-auto-unlock-key.service.ts new file mode 100644 index 0000000000..6c8ce3f048 --- /dev/null +++ b/libs/common/src/platform/services/user-auto-unlock-key.service.ts @@ -0,0 +1,36 @@ +import { UserId } from "../../types/guid"; +import { CryptoService } from "../abstractions/crypto.service"; +import { KeySuffixOptions } from "../enums"; + +// TODO: this is a half measure improvement which allows us to reduce some side effects today (cryptoService.getUserKey setting user key in memory if auto key exists) +// but ideally, in the future, we would be able to put this logic into the cryptoService +// after the vault timeout settings service is transitioned to state provider so that +// the getUserKey logic can simply go to the correct location based on the vault timeout settings +// similar to the TokenService (it would either go to secure storage for the auto user key or memory for the user key) + +export class UserAutoUnlockKeyService { + constructor(private cryptoService: CryptoService) {} + + /** + * The presence of the user key in memory dictates whether the user's vault is locked or unlocked. + * However, for users that have the auto unlock user key set, we need to set the user key in memory + * on application bootstrap and on active account changes so that the user's vault loads unlocked. + * @param userId - The user id to check for an auto user key. + */ + async setUserKeyInMemoryIfAutoUserKeySet(userId: UserId): Promise<void> { + if (userId == null) { + return; + } + + const autoUserKey = await this.cryptoService.getUserKeyFromStorage( + KeySuffixOptions.Auto, + userId, + ); + + if (autoUserKey == null) { + return; + } + + await this.cryptoService.setUserKey(autoUserKey, userId); + } +} diff --git a/libs/common/src/platform/services/user-key-init.service.spec.ts b/libs/common/src/platform/services/user-key-init.service.spec.ts deleted file mode 100644 index 567320ded6..0000000000 --- a/libs/common/src/platform/services/user-key-init.service.spec.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { mock } from "jest-mock-extended"; - -import { FakeAccountService, mockAccountServiceWith } from "../../../spec"; -import { CsprngArray } from "../../types/csprng"; -import { UserId } from "../../types/guid"; -import { UserKey } from "../../types/key"; -import { LogService } from "../abstractions/log.service"; -import { KeySuffixOptions } from "../enums"; -import { Utils } from "../misc/utils"; -import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; - -import { CryptoService } from "./crypto.service"; -import { UserKeyInitService } from "./user-key-init.service"; - -describe("UserKeyInitService", () => { - let userKeyInitService: UserKeyInitService; - - const mockUserId = Utils.newGuid() as UserId; - - const accountService: FakeAccountService = mockAccountServiceWith(mockUserId); - - const cryptoService = mock<CryptoService>(); - const logService = mock<LogService>(); - - beforeEach(() => { - userKeyInitService = new UserKeyInitService(accountService, cryptoService, logService); - }); - - describe("listenForActiveUserChangesToSetUserKey()", () => { - it("calls setUserKeyInMemoryIfAutoUserKeySet if there is an active user", () => { - // Arrange - accountService.activeAccountSubject.next({ - id: mockUserId, - name: "name", - email: "email", - }); - - (userKeyInitService as any).setUserKeyInMemoryIfAutoUserKeySet = jest.fn(); - - // Act - - const subscription = userKeyInitService.listenForActiveUserChangesToSetUserKey(); - - // Assert - - expect(subscription).not.toBeFalsy(); - - expect((userKeyInitService as any).setUserKeyInMemoryIfAutoUserKeySet).toHaveBeenCalledWith( - mockUserId, - ); - }); - - it("calls setUserKeyInMemoryIfAutoUserKeySet if there is an active user and tracks subsequent emissions", () => { - // Arrange - accountService.activeAccountSubject.next({ - id: mockUserId, - name: "name", - email: "email", - }); - - const mockUser2Id = Utils.newGuid() as UserId; - - jest - .spyOn(userKeyInitService as any, "setUserKeyInMemoryIfAutoUserKeySet") - .mockImplementation(() => Promise.resolve()); - - // Act - - const subscription = userKeyInitService.listenForActiveUserChangesToSetUserKey(); - - accountService.activeAccountSubject.next({ - id: mockUser2Id, - name: "name", - email: "email", - }); - - // Assert - - expect(subscription).not.toBeFalsy(); - - expect((userKeyInitService as any).setUserKeyInMemoryIfAutoUserKeySet).toHaveBeenCalledTimes( - 2, - ); - - expect( - (userKeyInitService as any).setUserKeyInMemoryIfAutoUserKeySet, - ).toHaveBeenNthCalledWith(1, mockUserId); - expect( - (userKeyInitService as any).setUserKeyInMemoryIfAutoUserKeySet, - ).toHaveBeenNthCalledWith(2, mockUser2Id); - - subscription.unsubscribe(); - }); - - it("does not call setUserKeyInMemoryIfAutoUserKeySet if there is not an active user", () => { - // Arrange - accountService.activeAccountSubject.next(null); - - (userKeyInitService as any).setUserKeyInMemoryIfAutoUserKeySet = jest.fn(); - - // Act - - const subscription = userKeyInitService.listenForActiveUserChangesToSetUserKey(); - - // Assert - - expect(subscription).not.toBeFalsy(); - - expect( - (userKeyInitService as any).setUserKeyInMemoryIfAutoUserKeySet, - ).not.toHaveBeenCalledWith(mockUserId); - }); - }); - - describe("setUserKeyInMemoryIfAutoUserKeySet", () => { - it("does nothing if the userId is null", async () => { - // Act - await (userKeyInitService as any).setUserKeyInMemoryIfAutoUserKeySet(null); - - // Assert - expect(cryptoService.getUserKeyFromStorage).not.toHaveBeenCalled(); - expect(cryptoService.setUserKey).not.toHaveBeenCalled(); - }); - - it("does nothing if the autoUserKey is null", async () => { - // Arrange - const userId = mockUserId; - - cryptoService.getUserKeyFromStorage.mockResolvedValue(null); - - // Act - await (userKeyInitService as any).setUserKeyInMemoryIfAutoUserKeySet(userId); - - // Assert - expect(cryptoService.getUserKeyFromStorage).toHaveBeenCalledWith( - KeySuffixOptions.Auto, - userId, - ); - expect(cryptoService.setUserKey).not.toHaveBeenCalled(); - }); - - it("sets the user key in memory if the autoUserKey is not null", async () => { - // Arrange - const userId = mockUserId; - - const mockRandomBytes = new Uint8Array(64) as CsprngArray; - const mockAutoUserKey: UserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey; - - cryptoService.getUserKeyFromStorage.mockResolvedValue(mockAutoUserKey); - - // Act - await (userKeyInitService as any).setUserKeyInMemoryIfAutoUserKeySet(userId); - - // Assert - expect(cryptoService.getUserKeyFromStorage).toHaveBeenCalledWith( - KeySuffixOptions.Auto, - userId, - ); - expect(cryptoService.setUserKey).toHaveBeenCalledWith(mockAutoUserKey, userId); - }); - }); -}); diff --git a/libs/common/src/platform/services/user-key-init.service.ts b/libs/common/src/platform/services/user-key-init.service.ts deleted file mode 100644 index 1f6aacce8f..0000000000 --- a/libs/common/src/platform/services/user-key-init.service.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { EMPTY, Subscription, catchError, filter, from, switchMap } from "rxjs"; - -import { AccountService } from "../../auth/abstractions/account.service"; -import { UserId } from "../../types/guid"; -import { CryptoService } from "../abstractions/crypto.service"; -import { LogService } from "../abstractions/log.service"; -import { KeySuffixOptions } from "../enums"; - -// TODO: this is a half measure improvement which allows us to reduce some side effects today (cryptoService.getUserKey setting user key in memory if auto key exists) -// but ideally, in the future, we would be able to put this logic into the cryptoService -// after the vault timeout settings service is transitioned to state provider so that -// the getUserKey logic can simply go to the correct location based on the vault timeout settings -// similar to the TokenService (it would either go to secure storage for the auto user key or memory for the user key) - -export class UserKeyInitService { - constructor( - private accountService: AccountService, - private cryptoService: CryptoService, - private logService: LogService, - ) {} - - // Note: must listen for changes to support account switching - listenForActiveUserChangesToSetUserKey(): Subscription { - return this.accountService.activeAccount$ - .pipe( - filter((activeAccount) => activeAccount != null), - switchMap((activeAccount) => - from(this.setUserKeyInMemoryIfAutoUserKeySet(activeAccount?.id)).pipe( - catchError((err: unknown) => { - this.logService.warning( - `setUserKeyInMemoryIfAutoUserKeySet failed with error: ${err}`, - ); - // Returning EMPTY to protect observable chain from cancellation in case of error - return EMPTY; - }), - ), - ), - ) - .subscribe(); - } - - private async setUserKeyInMemoryIfAutoUserKeySet(userId: UserId) { - if (userId == null) { - return; - } - - const autoUserKey = await this.cryptoService.getUserKeyFromStorage( - KeySuffixOptions.Auto, - userId, - ); - if (autoUserKey == null) { - return; - } - - await this.cryptoService.setUserKey(autoUserKey, userId); - } -} From 61d079cc3485799e1396eef75b94b90a27967114 Mon Sep 17 00:00:00 2001 From: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> Date: Mon, 29 Apr 2024 17:30:18 -0500 Subject: [PATCH 320/351] [SM-1168] Update access policy selector to disable on submit (#8519) * Add loading and disabled on all inputs * Add proper spinner and form disable on submit --- .../project/project-people.component.html | 8 ++++++- .../project/project-people.component.ts | 22 +++++++++---------- .../service-account-people.component.html | 8 ++++++- .../service-account-people.component.ts | 22 ++++++++----------- .../access-policy-selector.component.html | 10 ++++++++- 5 files changed, 43 insertions(+), 27 deletions(-) diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-people.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-people.component.html index 9a47f42686..3f107486e2 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-people.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-people.component.html @@ -1,4 +1,4 @@ -<form [formGroup]="formGroup" [bitSubmit]="submit"> +<form [formGroup]="formGroup" [bitSubmit]="submit" *ngIf="!loading; else spinner"> <div class="tw-w-2/5"> <p class="tw-mt-8" *ngIf="!loading"> {{ "projectPeopleDescription" | i18n }} @@ -19,3 +19,9 @@ </button> </div> </form> + +<ng-template #spinner> + <div class="tw-items-center tw-justify-center tw-pt-64 tw-text-center"> + <i class="bwi bwi-spinner bwi-spin bwi-3x"></i> + </div> +</ng-template> diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-people.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-people.component.ts index b52a8938b4..835d3825a0 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-people.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-people.component.ts @@ -1,7 +1,7 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from "@angular/core"; import { FormControl, FormGroup } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; -import { combineLatest, Subject, switchMap, takeUntil, catchError, EMPTY } from "rxjs"; +import { combineLatest, Subject, switchMap, takeUntil, catchError } from "rxjs"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -37,11 +37,9 @@ export class ProjectPeopleComponent implements OnInit, OnDestroy { return convertToAccessPolicyItemViews(policies); }), ), - catchError(() => { - // 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(["/sm", this.organizationId, "projects"]); - return EMPTY; + catchError(async () => { + await this.router.navigate(["/sm", this.organizationId, "projects"]); + return []; }), ); @@ -99,17 +97,20 @@ export class ProjectPeopleComponent implements OnInit, OnDestroy { if (this.formGroup.invalid) { return; } + const formValues = this.formGroup.value.accessPolicies; + this.formGroup.disable(); const showAccessRemovalWarning = await this.accessPolicySelectorService.showAccessRemovalWarning( this.organizationId, - this.formGroup.value.accessPolicies, + formValues, ); if (showAccessRemovalWarning) { const confirmed = await this.showWarning(); if (!confirmed) { this.setSelected(this.currentAccessPolicies); + this.formGroup.enable(); return; } } @@ -117,7 +118,7 @@ export class ProjectPeopleComponent implements OnInit, OnDestroy { try { const projectPeopleView = convertToProjectPeopleAccessPoliciesView( this.projectId, - this.formGroup.value.accessPolicies, + formValues, ); const peoplePoliciesViews = await this.accessPolicyService.putProjectPeopleAccessPolicies( this.projectId, @@ -126,9 +127,7 @@ export class ProjectPeopleComponent implements OnInit, OnDestroy { this.currentAccessPolicies = convertToAccessPolicyItemViews(peoplePoliciesViews); if (showAccessRemovalWarning) { - // 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(["sm", this.organizationId, "projects"]); + await this.router.navigate(["sm", this.organizationId, "projects"]); } this.platformUtilsService.showToast( "success", @@ -139,6 +138,7 @@ export class ProjectPeopleComponent implements OnInit, OnDestroy { this.validationService.showError(e); this.setSelected(this.currentAccessPolicies); } + this.formGroup.enable(); }; private setSelected(policiesToSelect: ApItemViewType[]) { 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 074fa8ca00..96f7ae4d2b 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,4 +1,4 @@ -<form [formGroup]="formGroup" [bitSubmit]="submit"> +<form [formGroup]="formGroup" [bitSubmit]="submit" *ngIf="!loading; else spinner"> <div class="tw-w-2/5"> <p class="tw-mt-8" *ngIf="!loading"> {{ "machineAccountPeopleDescription" | i18n }} @@ -20,3 +20,9 @@ </button> </div> </form> + +<ng-template #spinner> + <div class="tw-items-center tw-justify-center tw-pt-64 tw-text-center"> + <i class="bwi bwi-spinner bwi-spin bwi-3x"></i> + </div> +</ng-template> diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/people/service-account-people.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/people/service-account-people.component.ts index aeb124aa6a..a3d3984ea8 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/people/service-account-people.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/people/service-account-people.component.ts @@ -1,7 +1,7 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from "@angular/core"; import { FormControl, FormGroup } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; -import { catchError, combineLatest, EMPTY, Subject, switchMap, takeUntil } from "rxjs"; +import { combineLatest, Subject, switchMap, takeUntil } from "rxjs"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -40,12 +40,6 @@ export class ServiceAccountPeopleComponent implements OnInit, OnDestroy { return convertToAccessPolicyItemViews(policies); }), ), - catchError(() => { - // 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(["/sm", this.organizationId, "machine-accounts"]); - return EMPTY; - }), ); private potentialGrantees$ = combineLatest([this.route.params]).pipe( @@ -101,29 +95,32 @@ export class ServiceAccountPeopleComponent implements OnInit, OnDestroy { if (this.isFormInvalid()) { return; } + const formValues = this.formGroup.value.accessPolicies; + this.formGroup.disable(); const showAccessRemovalWarning = await this.accessPolicySelectorService.showAccessRemovalWarning( this.organizationId, - this.formGroup.value.accessPolicies, + formValues, ); if ( await this.handleAccessRemovalWarning(showAccessRemovalWarning, this.currentAccessPolicies) ) { + this.formGroup.enable(); return; } try { const peoplePoliciesViews = await this.updateServiceAccountPeopleAccessPolicies( this.serviceAccountId, - this.formGroup.value.accessPolicies, + formValues, ); await this.handleAccessTokenAvailableWarning( showAccessRemovalWarning, this.currentAccessPolicies, - this.formGroup.value.accessPolicies, + formValues, ); this.currentAccessPolicies = convertToAccessPolicyItemViews(peoplePoliciesViews); @@ -137,6 +134,7 @@ export class ServiceAccountPeopleComponent implements OnInit, OnDestroy { this.validationService.showError(e); this.setSelected(this.currentAccessPolicies); } + this.formGroup.enable(); }; private setSelected(policiesToSelect: ApItemViewType[]) { @@ -198,9 +196,7 @@ export class ServiceAccountPeopleComponent implements OnInit, OnDestroy { selectedPolicies: ApItemValueType[], ): Promise<void> { if (showAccessRemovalWarning) { - // 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(["sm", this.organizationId, "machine-accounts"]); + await this.router.navigate(["sm", this.organizationId, "machine-accounts"]); } else if ( this.accessPolicySelectorService.isAccessRemoval(currentAccessPolicies, selectedPolicies) ) { diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.component.html index 4b3c839264..e1faf2a185 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.component.html @@ -55,6 +55,7 @@ bitIconButton="bwi-close" buttonType="main" size="default" + [disabled]="disabled" [attr.title]="'remove' | i18n" [attr.aria-label]="'remove' | i18n" (click)="selectionList.deselectItem(item.id); handleBlur()" @@ -84,7 +85,14 @@ </bit-form-field> <div class="tw-ml-3 tw-mt-7 tw-shrink-0"> - <button type="button" bitButton buttonType="secondary" (click)="addButton()"> + <button + type="button" + bitButton + buttonType="secondary" + [loading]="loading" + [disabled]="disabled" + (click)="addButton()" + > {{ "add" | i18n }} </button> </div> From c70a5aa02457e515c6a94854da71e1200ea1b6e2 Mon Sep 17 00:00:00 2001 From: Matt Gibson <mgibson@bitwarden.com> Date: Tue, 30 Apr 2024 09:13:02 -0400 Subject: [PATCH 321/351] [PM-6688] Use AccountService as account source (#8893) * Use account service to track accounts and active account * Remove state service active account Observables. * Add email verified to account service * Do not store account info on logged out accounts * Add account activity tracking to account service * Use last account activity from account service * migrate or replicate account service data * Add `AccountActivityService` that handles storing account last active data * Move active and next active user to account service * Remove authenticated accounts from state object * Fold account activity into account service * Fix builds * Fix desktop app switch * Fix logging out non active user * Expand helper to handle new authenticated accounts location * Prefer view observable to tons of async pipes * Fix `npm run test:types` * Correct user activity sorting test * Be more precise about log out messaging * Fix dev compare errors All stored values are serializable, the next step wasn't necessary and was erroring on some types that lack `toString`. * If the account in unlocked on load of lock component, navigate away from lock screen * Handle no users case for auth service statuses * Specify account to switch to * Filter active account out of inactive accounts * Prefer constructor init * Improve comparator * Use helper methods internally * Fixup component tests * Clarify name * Ensure accounts object has only valid userIds * Capitalize const values * Prefer descriptive, single-responsibility guards * Update libs/common/src/state-migrations/migrate.ts Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> * Fix merge * Add user Id validation activity for undefined was being set, which was resulting in requests for the auth status of `"undefined"` (string) userId, due to key enumeration. These changes stop that at both locations, as well as account add for good measure. --------- Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> --- .../account-switcher.component.html | 4 +- .../account-switcher.component.ts | 9 +- .../services/account-switcher.service.spec.ts | 3 + apps/browser/src/auth/popup/lock.component.ts | 3 +- .../context-menu-clicked-handler.spec.ts | 9 +- .../browser/context-menu-clicked-handler.ts | 17 +- .../browser/src/background/main.background.ts | 20 +- .../src/background/runtime.background.ts | 13 +- .../src/platform/popup/header.component.ts | 17 +- .../services/browser-state.service.spec.ts | 16 +- .../services/default-browser-state.service.ts | 2 - .../local-backed-session-storage.service.ts | 41 +-- apps/browser/src/popup/app.component.ts | 23 +- .../popup/send/send-add-edit.component.ts | 3 + apps/cli/src/bw.ts | 5 +- apps/desktop/src/app/app-routing.module.ts | 4 +- apps/desktop/src/app/app.component.ts | 65 ++--- .../layout/account-switcher.component.html | 214 ++++++++-------- .../app/layout/account-switcher.component.ts | 130 ++++++---- .../src/app/layout/search/search.component.ts | 6 +- .../src/app/services/services.module.ts | 3 +- .../src/app/tools/generator.component.spec.ts | 5 + .../src/app/tools/send/add-edit.component.ts | 3 + apps/desktop/src/auth/guards/login.guard.ts | 29 --- .../src/auth/guards/max-accounts.guard.ts | 38 +++ apps/desktop/src/auth/lock.component.spec.ts | 9 +- apps/desktop/src/auth/lock.component.ts | 3 + apps/desktop/src/main/menu/menubar.ts | 5 +- apps/web/src/app/app.component.ts | 17 +- .../layouts/header/web-header.component.html | 4 +- .../layouts/header/web-header.component.ts | 20 +- .../src/app/tools/send/add-edit.component.ts | 3 + .../vault-items/vault-items.stories.ts | 1 - .../src/auth/components/lock.component.ts | 38 ++- libs/angular/src/pipes/user-name.pipe.ts | 2 +- .../src/tools/send/add-edit.component.ts | 6 +- .../auth-request-login.strategy.spec.ts | 1 + .../login-strategies/login.strategy.spec.ts | 5 +- .../common/login-strategies/login.strategy.ts | 8 + .../password-login.strategy.spec.ts | 3 + .../user-decryption-options.service.spec.ts | 1 + libs/common/spec/fake-account-service.ts | 57 ++++- .../src/auth/abstractions/account.service.ts | 47 +++- .../src/auth/services/account.service.spec.ts | 203 ++++++++++++++- .../src/auth/services/account.service.ts | 90 ++++++- .../src/auth/services/auth.service.spec.ts | 21 +- libs/common/src/auth/services/auth.service.ts | 28 +-- ...-enrollment.service.implementation.spec.ts | 1 + .../platform/abstractions/state.service.ts | 10 +- libs/common/src/platform/misc/utils.spec.ts | 27 ++ .../src/platform/models/domain/state.ts | 3 - .../default-environment.service.spec.ts | 3 + .../src/platform/services/state.service.ts | 234 +++++------------- .../src/platform/services/system.service.ts | 26 +- ...default-active-user-state.provider.spec.ts | 3 +- .../default-active-user-state.spec.ts | 1 + .../default-state.provider.spec.ts | 14 +- .../src/platform/state/state-definitions.ts | 1 + .../vault-timeout.service.spec.ts | 49 ++-- .../vault-timeout/vault-timeout.service.ts | 33 ++- libs/common/src/state-migrations/migrate.ts | 6 +- .../state-migrations/migration-helper.spec.ts | 43 ++++ .../src/state-migrations/migration-helper.ts | 28 ++- .../migrations/60-known-accounts.spec.ts | 145 +++++++++++ .../migrations/60-known-accounts.ts | 111 +++++++++ .../tools/send/services/send.service.spec.ts | 1 + .../src/vault/services/sync/sync.service.ts | 5 +- 67 files changed, 1380 insertions(+), 618 deletions(-) delete mode 100644 apps/desktop/src/auth/guards/login.guard.ts create mode 100644 apps/desktop/src/auth/guards/max-accounts.guard.ts create mode 100644 libs/common/src/state-migrations/migrations/60-known-accounts.spec.ts create mode 100644 libs/common/src/state-migrations/migrations/60-known-accounts.ts diff --git a/apps/browser/src/auth/popup/account-switching/account-switcher.component.html b/apps/browser/src/auth/popup/account-switching/account-switcher.component.html index aebf2219ff..806dae084d 100644 --- a/apps/browser/src/auth/popup/account-switching/account-switcher.component.html +++ b/apps/browser/src/auth/popup/account-switching/account-switcher.component.html @@ -49,7 +49,7 @@ <button type="button" class="account-switcher-row tw-flex tw-w-full tw-items-center tw-gap-3 tw-rounded-md tw-p-3 disabled:tw-cursor-not-allowed disabled:tw-border-text-muted/60 disabled:!tw-text-muted/60" - (click)="lock()" + (click)="lock(currentAccount.id)" [disabled]="currentAccount.status === lockedStatus || !activeUserCanLock" [title]="!activeUserCanLock ? ('unlockMethodNeeded' | i18n) : ''" > @@ -59,7 +59,7 @@ <button type="button" class="account-switcher-row tw-flex tw-w-full tw-items-center tw-gap-3 tw-rounded-md tw-p-3" - (click)="logOut()" + (click)="logOut(currentAccount.id)" > <i class="bwi bwi-sign-out tw-text-2xl" aria-hidden="true"></i> {{ "logOut" | i18n }} 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 9a0423fca3..e56a2d5c38 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 @@ -10,6 +10,7 @@ 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"; +import { UserId } from "@bitwarden/common/types/guid"; import { DialogService } from "@bitwarden/components"; import { AccountSwitcherService } from "./services/account-switcher.service"; @@ -64,9 +65,9 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy { this.location.back(); } - async lock(userId?: string) { + async lock(userId: string) { this.loading = true; - await this.vaultTimeoutService.lock(userId ? userId : null); + await this.vaultTimeoutService.lock(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 this.router.navigate(["lock"]); @@ -96,7 +97,7 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy { .subscribe(() => this.router.navigate(["lock"])); } - async logOut() { + async logOut(userId: UserId) { this.loading = true; const confirmed = await this.dialogService.openSimpleDialog({ title: { key: "logOut" }, @@ -105,7 +106,7 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy { }); if (confirmed) { - this.messagingService.send("logout"); + this.messagingService.send("logout", { userId }); } // 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/account-switching/services/account-switcher.service.spec.ts b/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.spec.ts index fe04bee20e..d27410a5d0 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 @@ -58,6 +58,7 @@ describe("AccountSwitcherService", () => { const accountInfo: AccountInfo = { name: "Test User 1", email: "test1@email.com", + emailVerified: true, }; avatarService.getUserAvatarColor$.mockReturnValue(of("#cccccc")); @@ -89,6 +90,7 @@ describe("AccountSwitcherService", () => { for (let i = 0; i < numberOfAccounts; i++) { seedAccounts[`${i}` as UserId] = { email: `test${i}@email.com`, + emailVerified: true, name: "Test User ${i}", }; seedStatuses[`${i}` as UserId] = AuthenticationStatus.Unlocked; @@ -113,6 +115,7 @@ describe("AccountSwitcherService", () => { const user1AccountInfo: AccountInfo = { name: "Test User 1", email: "", + emailVerified: true, }; accountsSubject.next({ ["1" as UserId]: user1AccountInfo }); authStatusSubject.next({ ["1" as UserId]: AuthenticationStatus.LoggedOut }); diff --git a/apps/browser/src/auth/popup/lock.component.ts b/apps/browser/src/auth/popup/lock.component.ts index 4d47417df6..86352e2c82 100644 --- a/apps/browser/src/auth/popup/lock.component.ts +++ b/apps/browser/src/auth/popup/lock.component.ts @@ -59,7 +59,7 @@ export class LockComponent extends BaseLockComponent { policyApiService: PolicyApiServiceAbstraction, policyService: InternalPolicyService, passwordStrengthService: PasswordStrengthServiceAbstraction, - private authService: AuthService, + authService: AuthService, dialogService: DialogService, deviceTrustService: DeviceTrustServiceAbstraction, userVerificationService: UserVerificationService, @@ -92,6 +92,7 @@ export class LockComponent extends BaseLockComponent { pinCryptoService, biometricStateService, accountService, + authService, kdfConfigService, ); this.successRoute = "/tabs/current"; diff --git a/apps/browser/src/autofill/browser/context-menu-clicked-handler.spec.ts b/apps/browser/src/autofill/browser/context-menu-clicked-handler.spec.ts index e54f37489b..6ef004f797 100644 --- a/apps/browser/src/autofill/browser/context-menu-clicked-handler.spec.ts +++ b/apps/browser/src/autofill/browser/context-menu-clicked-handler.spec.ts @@ -11,7 +11,8 @@ import { GENERATE_PASSWORD_ID, NOOP_COMMAND_SUFFIX, } from "@bitwarden/common/autofill/constants"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; import { CipherType } from "@bitwarden/common/vault/enums"; @@ -65,7 +66,7 @@ describe("ContextMenuClickedHandler", () => { let autofill: AutofillAction; let authService: MockProxy<AuthService>; let cipherService: MockProxy<CipherService>; - let stateService: MockProxy<StateService>; + let accountService: FakeAccountService; let totpService: MockProxy<TotpService>; let eventCollectionService: MockProxy<EventCollectionService>; let userVerificationService: MockProxy<UserVerificationService>; @@ -78,7 +79,7 @@ describe("ContextMenuClickedHandler", () => { autofill = jest.fn<Promise<void>, [tab: chrome.tabs.Tab, cipher: CipherView]>(); authService = mock(); cipherService = mock(); - stateService = mock(); + accountService = mockAccountServiceWith("userId" as UserId); totpService = mock(); eventCollectionService = mock(); @@ -88,10 +89,10 @@ describe("ContextMenuClickedHandler", () => { autofill, authService, cipherService, - stateService, totpService, eventCollectionService, userVerificationService, + accountService, ); }); 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 596d6b7235..5ba48a9f27 100644 --- a/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts +++ b/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts @@ -1,4 +1,7 @@ +import { firstValueFrom, map } from "rxjs"; + import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; @@ -17,7 +20,6 @@ import { NOOP_COMMAND_SUFFIX, } from "@bitwarden/common/autofill/constants"; import { EventType } from "@bitwarden/common/enums"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -26,6 +28,7 @@ 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"; +import { accountServiceFactory } from "../../auth/background/service-factories/account-service.factory"; import { authServiceFactory, AuthServiceInitOptions, @@ -37,7 +40,6 @@ import { autofillSettingsServiceFactory } from "../../autofill/background/servic import { eventCollectionServiceFactory } from "../../background/service-factories/event-collection-service.factory"; import { Account } from "../../models/account"; import { CachedServices } from "../../platform/background/service-factories/factory-options"; -import { stateServiceFactory } from "../../platform/background/service-factories/state-service.factory"; import { BrowserApi } from "../../platform/browser/browser-api"; import { passwordGenerationServiceFactory } from "../../tools/background/service_factories/password-generation-service.factory"; import { @@ -71,10 +73,10 @@ export class ContextMenuClickedHandler { private autofillAction: AutofillAction, private authService: AuthService, private cipherService: CipherService, - private stateService: StateService, private totpService: TotpService, private eventCollectionService: EventCollectionService, private userVerificationService: UserVerificationService, + private accountService: AccountService, ) {} static async mv3Create(cachedServices: CachedServices) { @@ -128,10 +130,10 @@ export class ContextMenuClickedHandler { (tab, cipher) => autofillCommand.doAutofillTabWithCipherCommand(tab, cipher), await authServiceFactory(cachedServices, serviceOptions), await cipherServiceFactory(cachedServices, serviceOptions), - await stateServiceFactory(cachedServices, serviceOptions), await totpServiceFactory(cachedServices, serviceOptions), await eventCollectionServiceFactory(cachedServices, serviceOptions), await userVerificationServiceFactory(cachedServices, serviceOptions), + await accountServiceFactory(cachedServices, serviceOptions), ); } @@ -239,9 +241,10 @@ export class ContextMenuClickedHandler { 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.stateService.setLastActive(new Date().getTime()); + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + await this.accountService.setAccountActivity(activeUserId, new Date()); switch (info.parentMenuItemId) { case AUTOFILL_ID: case AUTOFILL_IDENTITY_ID: diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 2dc229e402..01e325ad51 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1,4 +1,4 @@ -import { Subject, firstValueFrom, merge, timeout } from "rxjs"; +import { Subject, firstValueFrom, map, merge, timeout } from "rxjs"; import { PinCryptoServiceAbstraction, @@ -902,6 +902,7 @@ export default class MainBackground { this.autofillSettingsService, this.vaultTimeoutSettingsService, this.biometricStateService, + this.accountService, ); // Other fields @@ -920,7 +921,6 @@ export default class MainBackground { this.autofillService, this.platformUtilsService as BrowserPlatformUtilsService, this.notificationsService, - this.stateService, this.autofillSettingsService, this.systemService, this.environmentService, @@ -929,6 +929,7 @@ export default class MainBackground { this.configService, this.fido2Background, messageListener, + this.accountService, ); this.nativeMessagingBackground = new NativeMessagingBackground( this.accountService, @@ -1018,10 +1019,10 @@ export default class MainBackground { }, this.authService, this.cipherService, - this.stateService, this.totpService, this.eventCollectionService, this.userVerificationService, + this.accountService, ); this.contextMenusBackground = new ContextMenusBackground(contextMenuClickedHandler); @@ -1168,7 +1169,12 @@ export default class MainBackground { */ async switchAccount(userId: UserId) { try { - await this.stateService.setActiveUser(userId); + const currentlyActiveAccount = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((account) => account?.id)), + ); + // can be removed once password generation history is migrated to state providers + await this.stateService.clearDecryptedData(currentlyActiveAccount); + await this.accountService.switchAccount(userId); if (userId == null) { this.loginEmailService.setRememberEmail(false); @@ -1240,7 +1246,11 @@ export default class MainBackground { //Needs to be checked before state is cleaned const needStorageReseed = await this.needsStorageReseed(); - const newActiveUser = await this.stateService.clean({ userId: userId }); + const newActiveUser = await firstValueFrom( + this.accountService.nextUpAccount$.pipe(map((a) => a?.id)), + ); + await this.stateService.clean({ userId: userId }); + await this.accountService.clean(userId); await this.stateEventRunnerService.handleEvent("logout", userId); diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts index 14eb228fb0..98b1df9c80 100644 --- a/apps/browser/src/background/runtime.background.ts +++ b/apps/browser/src/background/runtime.background.ts @@ -1,6 +1,7 @@ -import { firstValueFrom, mergeMap } from "rxjs"; +import { firstValueFrom, map, mergeMap } from "rxjs"; import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -19,7 +20,6 @@ import { import { LockedVaultPendingNotificationsData } from "../autofill/background/abstractions/notification.background"; import { AutofillService } from "../autofill/services/abstractions/autofill.service"; import { BrowserApi } from "../platform/browser/browser-api"; -import { BrowserStateService } from "../platform/services/abstractions/browser-state.service"; import { BrowserEnvironmentService } from "../platform/services/browser-environment.service"; import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service"; import { Fido2Background } from "../vault/fido2/background/abstractions/fido2.background"; @@ -37,7 +37,6 @@ export default class RuntimeBackground { private autofillService: AutofillService, private platformUtilsService: BrowserPlatformUtilsService, private notificationsService: NotificationsService, - private stateService: BrowserStateService, private autofillSettingsService: AutofillSettingsServiceAbstraction, private systemService: SystemService, private environmentService: BrowserEnvironmentService, @@ -46,6 +45,7 @@ export default class RuntimeBackground { private configService: ConfigService, private fido2Background: Fido2Background, private messageListener: MessageListener, + private accountService: AccountService, ) { // onInstalled listener must be wired up before anything else, so we do it in the ctor chrome.runtime.onInstalled.addListener((details: any) => { @@ -111,9 +111,10 @@ export default class RuntimeBackground { switch (msg.sender) { case "autofiller": case "autofill_cmd": { - // 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.stateService.setLastActive(new Date().getTime()); + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + await this.accountService.setAccountActivity(activeUserId, new Date()); const totpCode = await this.autofillService.doAutoFillActiveTab( [ { diff --git a/apps/browser/src/platform/popup/header.component.ts b/apps/browser/src/platform/popup/header.component.ts index ebda12c2a4..1373837866 100644 --- a/apps/browser/src/platform/popup/header.component.ts +++ b/apps/browser/src/platform/popup/header.component.ts @@ -1,10 +1,8 @@ import { Component, Input } from "@angular/core"; -import { Observable, combineLatest, map, of, switchMap } from "rxjs"; +import { Observable, map, of, switchMap } 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"; -import { UserId } from "@bitwarden/common/types/guid"; import { enableAccountSwitching } from "../flags"; @@ -16,18 +14,15 @@ export class HeaderComponent { @Input() noTheme = false; @Input() hideAccountSwitcher = false; authedAccounts$: Observable<boolean>; - constructor(accountService: AccountService, authService: AuthService) { - this.authedAccounts$ = accountService.accounts$.pipe( - switchMap((accounts) => { + constructor(authService: AuthService) { + this.authedAccounts$ = authService.authStatuses$.pipe( + map((record) => Object.values(record)), + switchMap((statuses) => { if (!enableAccountSwitching()) { return of(false); } - return combineLatest( - Object.keys(accounts).map((id) => authService.authStatusFor$(id as UserId)), - ).pipe( - map((statuses) => statuses.some((status) => status !== AuthenticationStatus.LoggedOut)), - ); + return of(statuses.some((status) => status !== AuthenticationStatus.LoggedOut)); }), ); } diff --git a/apps/browser/src/platform/services/browser-state.service.spec.ts b/apps/browser/src/platform/services/browser-state.service.spec.ts index f06126dcf5..a0a52ff622 100644 --- a/apps/browser/src/platform/services/browser-state.service.spec.ts +++ b/apps/browser/src/platform/services/browser-state.service.spec.ts @@ -1,5 +1,4 @@ import { mock, MockProxy } from "jest-mock-extended"; -import { firstValueFrom } from "rxjs"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; @@ -50,7 +49,6 @@ describe("Browser State Service", () => { state.accounts[userId] = new Account({ profile: { userId: userId }, }); - state.activeUserId = userId; }); afterEach(() => { @@ -78,18 +76,8 @@ describe("Browser State Service", () => { ); }); - describe("add Account", () => { - it("should add account", async () => { - const newUserId = "newUserId" as UserId; - const newAcct = new Account({ - profile: { userId: newUserId }, - }); - - await sut.addAccount(newAcct); - - const accts = await firstValueFrom(sut.accounts$); - expect(accts[newUserId]).toBeDefined(); - }); + it("exists", () => { + expect(sut).toBeDefined(); }); }); }); diff --git a/apps/browser/src/platform/services/default-browser-state.service.ts b/apps/browser/src/platform/services/default-browser-state.service.ts index b9cd219076..f717ab96d8 100644 --- a/apps/browser/src/platform/services/default-browser-state.service.ts +++ b/apps/browser/src/platform/services/default-browser-state.service.ts @@ -29,8 +29,6 @@ export class DefaultBrowserStateService initializeAs: "record", }) protected accountsSubject: BehaviorSubject<{ [userId: string]: Account }>; - @sessionSync({ initializer: (s: string) => s }) - protected activeAccountSubject: BehaviorSubject<string>; protected accountDeserializer = Account.fromJSON; diff --git a/apps/browser/src/platform/services/local-backed-session-storage.service.ts b/apps/browser/src/platform/services/local-backed-session-storage.service.ts index 0fa359181d..c29b9c69dc 100644 --- a/apps/browser/src/platform/services/local-backed-session-storage.service.ts +++ b/apps/browser/src/platform/services/local-backed-session-storage.service.ts @@ -200,26 +200,29 @@ export class LocalBackedSessionStorageService } private compareValues<T>(value1: T, value2: T): boolean { - if (value1 == null && value2 == null) { + try { + if (value1 == null && value2 == null) { + return true; + } + + if (value1 && value2 == null) { + return false; + } + + if (value1 == null && value2) { + return false; + } + + if (typeof value1 !== "object" || typeof value2 !== "object") { + return value1 === value2; + } + + return JSON.stringify(value1) === JSON.stringify(value2); + } catch (e) { + this.logService.error( + `error comparing values\n${JSON.stringify(value1)}\n${JSON.stringify(value2)}`, + ); return true; } - - if (value1 && value2 == null) { - return false; - } - - if (value1 == null && value2) { - return false; - } - - if (typeof value1 !== "object" || typeof value2 !== "object") { - return value1 === value2; - } - - if (JSON.stringify(value1) === JSON.stringify(value2)) { - return true; - } - - return Object.entries(value1).sort().toString() === Object.entries(value2).sort().toString(); } } diff --git a/apps/browser/src/popup/app.component.ts b/apps/browser/src/popup/app.component.ts index 7acaf1ba93..25fac44450 100644 --- a/apps/browser/src/popup/app.component.ts +++ b/apps/browser/src/popup/app.component.ts @@ -1,12 +1,14 @@ import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core"; import { NavigationEnd, Router, RouterOutlet } from "@angular/router"; -import { filter, concatMap, Subject, takeUntil, firstValueFrom, tap, map } from "rxjs"; +import { Subject, takeUntil, firstValueFrom, concatMap, filter, tap } 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"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { MessageListener } from "@bitwarden/common/platform/messaging"; +import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { DialogService, SimpleDialogOptions, ToastService } from "@bitwarden/components"; @@ -27,8 +29,9 @@ import { DesktopSyncVerificationDialogComponent } from "./components/desktop-syn </div>`, }) export class AppComponent implements OnInit, OnDestroy { - private lastActivity: number = null; - private activeUserId: string; + private lastActivity: Date; + private activeUserId: UserId; + private recordActivitySubject = new Subject<void>(); private destroy$ = new Subject<void>(); @@ -46,6 +49,7 @@ export class AppComponent implements OnInit, OnDestroy { private dialogService: DialogService, private messageListener: MessageListener, private toastService: ToastService, + private accountService: AccountService, ) {} async ngOnInit() { @@ -53,14 +57,13 @@ export class AppComponent implements OnInit, OnDestroy { // Clear them aggressively to make sure this doesn't occur await this.clearComponentStates(); - this.stateService.activeAccount$.pipe(takeUntil(this.destroy$)).subscribe((userId) => { - this.activeUserId = userId; + this.accountService.activeAccount$.pipe(takeUntil(this.destroy$)).subscribe((account) => { + this.activeUserId = account?.id; }); this.authService.activeAccountStatus$ .pipe( - map((status) => status === AuthenticationStatus.Unlocked), - filter((unlocked) => unlocked), + filter((status) => status === AuthenticationStatus.Unlocked), concatMap(async () => { await this.recordActivity(); }), @@ -200,13 +203,13 @@ export class AppComponent implements OnInit, OnDestroy { return; } - const now = new Date().getTime(); - if (this.lastActivity != null && now - this.lastActivity < 250) { + const now = new Date(); + if (this.lastActivity != null && now.getTime() - this.lastActivity.getTime() < 250) { return; } this.lastActivity = now; - await this.stateService.setLastActive(now, { userId: this.activeUserId }); + await this.accountService.setAccountActivity(this.activeUserId, now); } private showToast(msg: any) { diff --git a/apps/browser/src/tools/popup/send/send-add-edit.component.ts b/apps/browser/src/tools/popup/send/send-add-edit.component.ts index baf985b6e9..c20bf7cb8d 100644 --- a/apps/browser/src/tools/popup/send/send-add-edit.component.ts +++ b/apps/browser/src/tools/popup/send/send-add-edit.component.ts @@ -6,6 +6,7 @@ import { first } from "rxjs/operators"; import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/tools/send/add-edit.component"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -51,6 +52,7 @@ export class SendAddEditComponent extends BaseAddEditComponent { formBuilder: FormBuilder, private filePopoutUtilsService: FilePopoutUtilsService, billingAccountProfileStateService: BillingAccountProfileStateService, + accountService: AccountService, ) { super( i18nService, @@ -66,6 +68,7 @@ export class SendAddEditComponent extends BaseAddEditComponent { dialogService, formBuilder, billingAccountProfileStateService, + accountService, ); } diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index 4c2066dbf1..665701639e 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -731,7 +731,7 @@ export class Main { this.authService.logOut(() => { /* Do nothing */ }); - const userId = await this.stateService.getUserId(); + const userId = (await this.stateService.getUserId()) as UserId; await Promise.all([ this.eventUploadService.uploadEvents(userId as UserId), this.syncService.setLastSync(new Date(0)), @@ -742,9 +742,10 @@ export class Main { this.passwordGenerationService.clear(), ]); - await this.stateEventRunnerService.handleEvent("logout", userId as UserId); + await this.stateEventRunnerService.handleEvent("logout", userId); await this.stateService.clean(); + await this.accountService.clean(userId); process.env.BW_SESSION = null; } diff --git a/apps/desktop/src/app/app-routing.module.ts b/apps/desktop/src/app/app-routing.module.ts index 4fc19c8433..bb8deb2339 100644 --- a/apps/desktop/src/app/app-routing.module.ts +++ b/apps/desktop/src/app/app-routing.module.ts @@ -9,7 +9,7 @@ import { } from "@bitwarden/angular/auth/guards"; import { AccessibilityCookieComponent } from "../auth/accessibility-cookie.component"; -import { LoginGuard } from "../auth/guards/login.guard"; +import { maxAccountsGuardFn } from "../auth/guards/max-accounts.guard"; import { HintComponent } from "../auth/hint.component"; import { LockComponent } from "../auth/lock.component"; import { LoginDecryptionOptionsComponent } from "../auth/login/login-decryption-options/login-decryption-options.component"; @@ -40,7 +40,7 @@ const routes: Routes = [ { path: "login", component: LoginComponent, - canActivate: [LoginGuard], + canActivate: [maxAccountsGuardFn()], }, { path: "login-with-device", diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index ad99a3a447..4e540efdc6 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -8,7 +8,7 @@ import { ViewContainerRef, } from "@angular/core"; import { Router } from "@angular/router"; -import { firstValueFrom, Subject, takeUntil } from "rxjs"; +import { firstValueFrom, map, Subject, takeUntil } from "rxjs"; import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref"; import { ModalService } from "@bitwarden/angular/services/modal.service"; @@ -18,9 +18,9 @@ import { NotificationsService } from "@bitwarden/common/abstractions/notificatio import { SearchService } from "@bitwarden/common/abstractions/search.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 { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; +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"; @@ -107,11 +107,11 @@ export class AppComponent implements OnInit, OnDestroy { loading = false; - private lastActivity: number = null; + private lastActivity: Date = null; private modal: ModalRef = null; private idleTimer: number = null; private isIdle = false; - private activeUserId: string = null; + private activeUserId: UserId = null; private destroy$ = new Subject<void>(); @@ -150,12 +150,12 @@ export class AppComponent implements OnInit, OnDestroy { private biometricStateService: BiometricStateService, private stateEventRunnerService: StateEventRunnerService, private providerService: ProviderService, - private organizationService: InternalOrganizationServiceAbstraction, + private accountService: AccountService, ) {} ngOnInit() { - this.stateService.activeAccount$.pipe(takeUntil(this.destroy$)).subscribe((userId) => { - this.activeUserId = userId; + this.accountService.activeAccount$.pipe(takeUntil(this.destroy$)).subscribe((account) => { + this.activeUserId = account?.id; }); this.ngZone.runOutsideAngular(() => { @@ -400,7 +400,8 @@ export class AppComponent implements OnInit, OnDestroy { break; case "switchAccount": { if (message.userId != null) { - await this.stateService.setActiveUser(message.userId); + await this.stateService.clearDecryptedData(message.userId); + await this.accountService.switchAccount(message.userId); } const locked = (await this.authService.getAuthStatus(message.userId)) === @@ -522,7 +523,7 @@ export class AppComponent implements OnInit, OnDestroy { private async updateAppMenu() { let updateRequest: MenuUpdateRequest; - const stateAccounts = await firstValueFrom(this.stateService.accounts$); + const stateAccounts = await firstValueFrom(this.accountService.accounts$); if (stateAccounts == null || Object.keys(stateAccounts).length < 1) { updateRequest = { accounts: null, @@ -531,32 +532,32 @@ export class AppComponent implements OnInit, OnDestroy { } else { const accounts: { [userId: string]: MenuAccount } = {}; for (const i in stateAccounts) { + const userId = i as UserId; if ( i != null && - stateAccounts[i]?.profile?.userId != null && - !this.isAccountCleanUpInProgress(stateAccounts[i].profile.userId) // skip accounts that are being cleaned up + userId != null && + !this.isAccountCleanUpInProgress(userId) // skip accounts that are being cleaned up ) { - const userId = stateAccounts[i].profile.userId; const availableTimeoutActions = await firstValueFrom( this.vaultTimeoutSettingsService.availableVaultTimeoutActions$(userId), ); + const authStatus = await firstValueFrom(this.authService.authStatusFor$(userId)); accounts[userId] = { - isAuthenticated: await this.stateService.getIsAuthenticated({ - userId: userId, - }), - isLocked: - (await this.authService.getAuthStatus(userId)) === AuthenticationStatus.Locked, + isAuthenticated: authStatus >= AuthenticationStatus.Locked, + isLocked: authStatus === AuthenticationStatus.Locked, isLockable: availableTimeoutActions.includes(VaultTimeoutAction.Lock), - email: stateAccounts[i].profile.email, - userId: stateAccounts[i].profile.userId, + email: stateAccounts[userId].email, + userId: userId, hasMasterPassword: await this.userVerificationService.hasMasterPassword(userId), }; } } updateRequest = { accounts: accounts, - activeUserId: await this.stateService.getUserId(), + activeUserId: await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ), }; } @@ -564,7 +565,9 @@ export class AppComponent implements OnInit, OnDestroy { } private async logOut(expired: boolean, userId?: string) { - const userBeingLoggedOut = await this.stateService.getUserId({ userId: userId }); + const userBeingLoggedOut = + (userId as UserId) ?? + (await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id)))); // Mark account as being cleaned up so that the updateAppMenu logic (executed on syncCompleted) // doesn't attempt to update a user that is being logged out as we will manually @@ -572,9 +575,10 @@ export class AppComponent implements OnInit, OnDestroy { this.startAccountCleanUp(userBeingLoggedOut); let preLogoutActiveUserId; + const nextUpAccount = await firstValueFrom(this.accountService.nextUpAccount$); try { // Provide the userId of the user to upload events for - await this.eventUploadService.uploadEvents(userBeingLoggedOut as UserId); + await this.eventUploadService.uploadEvents(userBeingLoggedOut); await this.syncService.setLastSync(new Date(0), userBeingLoggedOut); await this.cryptoService.clearKeys(userBeingLoggedOut); await this.cipherService.clear(userBeingLoggedOut); @@ -582,22 +586,23 @@ export class AppComponent implements OnInit, OnDestroy { await this.collectionService.clear(userBeingLoggedOut); await this.passwordGenerationService.clear(userBeingLoggedOut); await this.vaultTimeoutSettingsService.clear(userBeingLoggedOut); - await this.biometricStateService.logout(userBeingLoggedOut as UserId); + await this.biometricStateService.logout(userBeingLoggedOut); - await this.stateEventRunnerService.handleEvent("logout", userBeingLoggedOut as UserId); + await this.stateEventRunnerService.handleEvent("logout", userBeingLoggedOut); preLogoutActiveUserId = this.activeUserId; await this.stateService.clean({ userId: userBeingLoggedOut }); + await this.accountService.clean(userBeingLoggedOut); } finally { this.finishAccountCleanUp(userBeingLoggedOut); } - if (this.activeUserId == null) { + if (nextUpAccount == 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 this.router.navigate(["login"]); - } else if (preLogoutActiveUserId !== this.activeUserId) { - this.messagingService.send("switchAccount"); + } else if (preLogoutActiveUserId !== nextUpAccount.id) { + this.messagingService.send("switchAccount", { userId: nextUpAccount.id }); } await this.updateAppMenu(); @@ -622,13 +627,13 @@ export class AppComponent implements OnInit, OnDestroy { return; } - const now = new Date().getTime(); - if (this.lastActivity != null && now - this.lastActivity < 250) { + const now = new Date(); + if (this.lastActivity != null && now.getTime() - this.lastActivity.getTime() < 250) { return; } this.lastActivity = now; - await this.stateService.setLastActive(now, { userId: this.activeUserId }); + await this.accountService.setAccountActivity(this.activeUserId, now); // Idle states if (this.isIdle) { diff --git a/apps/desktop/src/app/layout/account-switcher.component.html b/apps/desktop/src/app/layout/account-switcher.component.html index eedafbcfe0..b5741a1a1b 100644 --- a/apps/desktop/src/app/layout/account-switcher.component.html +++ b/apps/desktop/src/app/layout/account-switcher.component.html @@ -1,110 +1,112 @@ <!-- Please remove this disable statement when editing this file! --> <!-- eslint-disable @angular-eslint/template/button-has-type --> -<button - class="account-switcher" - (click)="toggle()" - cdkOverlayOrigin - #trigger="cdkOverlayOrigin" - [hidden]="!showSwitcher" - aria-haspopup="dialog" -> - <ng-container *ngIf="activeAccount?.email != null; else noActiveAccount"> - <app-avatar - [text]="activeAccount.name" - [id]="activeAccount.id" - [color]="activeAccount.avatarColor" - [size]="25" - [circle]="true" - [fontSize]="14" - [dynamic]="true" - *ngIf="activeAccount.email != null" - aria-hidden="true" - ></app-avatar> - <div class="active-account"> - <div>{{ activeAccount.email }}</div> - <span>{{ activeAccount.server }}</span> - <span class="sr-only">&nbsp;({{ "switchAccount" | i18n }})</span> - </div> - </ng-container> - <ng-template #noActiveAccount> - <span>{{ "switchAccount" | i18n }}</span> - </ng-template> - <i - class="bwi" - aria-hidden="true" - [ngClass]="{ 'bwi-angle-down': !isOpen, 'bwi-angle-up': isOpen }" - ></i> -</button> - -<ng-template - cdkConnectedOverlay - [cdkConnectedOverlayOrigin]="trigger" - [cdkConnectedOverlayHasBackdrop]="true" - [cdkConnectedOverlayBackdropClass]="'cdk-overlay-transparent-backdrop'" - (backdropClick)="close()" - (detach)="close()" - [cdkConnectedOverlayOpen]="showSwitcher && isOpen" - [cdkConnectedOverlayPositions]="overlayPosition" - cdkConnectedOverlayMinWidth="250px" -> - <div - class="account-switcher-dropdown" - [@transformPanel]="'open'" - cdkTrapFocus - cdkTrapFocusAutoCapture - role="dialog" - aria-modal="true" +<ng-container *ngIf="view$ | async as view"> + <button + class="account-switcher" + (click)="toggle()" + cdkOverlayOrigin + #trigger="cdkOverlayOrigin" + [hidden]="!view.showSwitcher" + aria-haspopup="dialog" > - <div class="accounts" *ngIf="numberOfAccounts > 0"> - <button - *ngFor="let account of inactiveAccounts | keyvalue" - class="account" - (click)="switch(account.key)" - > - <app-avatar - [text]="account.value.name ?? account.value.email" - [id]="account.value.id" - [size]="25" - [circle]="true" - [fontSize]="14" - [dynamic]="true" - [color]="account.value.avatarColor" - *ngIf="account.value.email != null" - aria-hidden="true" - ></app-avatar> - <div class="accountInfo"> - <span class="sr-only">{{ "switchAccount" | i18n }}:&nbsp;</span> - <span class="email" aria-hidden="true">{{ account.value.email }}</span> - <span class="server" aria-hidden="true"> - <span class="sr-only"> / </span>{{ account.value.server }} - </span> - <span class="status" aria-hidden="true" - ><span class="sr-only">&nbsp;(</span - >{{ - (account.value.authenticationStatus === authStatus.Unlocked ? "unlocked" : "locked") - | i18n - }}<span class="sr-only">)</span></span - > - </div> - <i - class="bwi bwi-2x text-muted" - [ngClass]=" - account.value.authenticationStatus === authStatus.Unlocked ? 'bwi-unlock' : 'bwi-lock' - " - aria-hidden="true" - ></i> - </button> - </div> - <ng-container *ngIf="activeAccount?.email != null"> - <div class="border" *ngIf="numberOfAccounts > 0"></div> - <ng-container *ngIf="numberOfAccounts < 4"> - <button type="button" class="add" (click)="addAccount()"> - <i class="bwi bwi-plus" aria-hidden="true"></i> {{ "addAccount" | i18n }} - </button> - </ng-container> - <ng-container *ngIf="numberOfAccounts === 4"> - <span class="accountLimitReached">{{ "accountSwitcherLimitReached" | i18n }} </span> - </ng-container> + <ng-container *ngIf="view.activeAccount; else noActiveAccount"> + <app-avatar + [text]="view.activeAccount.name ?? view.activeAccount.email" + [id]="view.activeAccount.id" + [color]="view.activeAccount.avatarColor" + [size]="25" + [circle]="true" + [fontSize]="14" + [dynamic]="true" + *ngIf="view.activeAccount.email != null" + aria-hidden="true" + ></app-avatar> + <div class="active-account"> + <div>{{ view.activeAccount.email }}</div> + <span>{{ view.activeAccount.server }}</span> + <span class="sr-only">&nbsp;({{ "switchAccount" | i18n }})</span> + </div> </ng-container> - </div> -</ng-template> + <ng-template #noActiveAccount> + <span>{{ "switchAccount" | i18n }}</span> + </ng-template> + <i + class="bwi" + aria-hidden="true" + [ngClass]="{ 'bwi-angle-down': !isOpen, 'bwi-angle-up': isOpen }" + ></i> + </button> + + <ng-template + cdkConnectedOverlay + [cdkConnectedOverlayOrigin]="trigger" + [cdkConnectedOverlayHasBackdrop]="true" + [cdkConnectedOverlayBackdropClass]="'cdk-overlay-transparent-backdrop'" + (backdropClick)="close()" + (detach)="close()" + [cdkConnectedOverlayOpen]="view.showSwitcher && isOpen" + [cdkConnectedOverlayPositions]="overlayPosition" + cdkConnectedOverlayMinWidth="250px" + > + <div + class="account-switcher-dropdown" + [@transformPanel]="'open'" + cdkTrapFocus + cdkTrapFocusAutoCapture + role="dialog" + aria-modal="true" + > + <div class="accounts" *ngIf="view.numberOfAccounts > 0"> + <button + *ngFor="let account of view.inactiveAccounts | keyvalue" + class="account" + (click)="switch(account.key)" + > + <app-avatar + [text]="account.value.name ?? account.value.email" + [id]="account.value.id" + [size]="25" + [circle]="true" + [fontSize]="14" + [dynamic]="true" + [color]="account.value.avatarColor" + *ngIf="account.value.email != null" + aria-hidden="true" + ></app-avatar> + <div class="accountInfo"> + <span class="sr-only">{{ "switchAccount" | i18n }}:&nbsp;</span> + <span class="email" aria-hidden="true">{{ account.value.email }}</span> + <span class="server" aria-hidden="true"> + <span class="sr-only"> / </span>{{ account.value.server }} + </span> + <span class="status" aria-hidden="true" + ><span class="sr-only">&nbsp;(</span + >{{ + (account.value.authenticationStatus === authStatus.Unlocked ? "unlocked" : "locked") + | i18n + }}<span class="sr-only">)</span></span + > + </div> + <i + class="bwi bwi-2x text-muted" + [ngClass]=" + account.value.authenticationStatus === authStatus.Unlocked ? 'bwi-unlock' : 'bwi-lock' + " + aria-hidden="true" + ></i> + </button> + </div> + <ng-container *ngIf="view.activeAccount"> + <div class="border" *ngIf="view.numberOfAccounts > 0"></div> + <ng-container *ngIf="view.numberOfAccounts < 4"> + <button type="button" class="add" (click)="addAccount()"> + <i class="bwi bwi-plus" aria-hidden="true"></i> {{ "addAccount" | i18n }} + </button> + </ng-container> + <ng-container *ngIf="view.numberOfAccounts === 4"> + <span class="accountLimitReached">{{ "accountSwitcherLimitReached" | i18n }} </span> + </ng-container> + </ng-container> + </div> + </ng-template> +</ng-container> diff --git a/apps/desktop/src/app/layout/account-switcher.component.ts b/apps/desktop/src/app/layout/account-switcher.component.ts index 4e39ab0029..c8a26065c1 100644 --- a/apps/desktop/src/app/layout/account-switcher.component.ts +++ b/apps/desktop/src/app/layout/account-switcher.component.ts @@ -1,19 +1,17 @@ import { animate, state, style, transition, trigger } from "@angular/animations"; import { ConnectedPosition } from "@angular/cdk/overlay"; -import { Component, OnDestroy, OnInit } from "@angular/core"; +import { Component } from "@angular/core"; import { Router } from "@angular/router"; -import { concatMap, firstValueFrom, Subject, takeUntil } from "rxjs"; +import { combineLatest, firstValueFrom, map, Observable, switchMap } from "rxjs"; import { LoginEmailServiceAbstraction } from "@bitwarden/auth/common"; +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 { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.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 { Account } from "@bitwarden/common/platform/models/domain/account"; import { UserId } from "@bitwarden/common/types/guid"; type ActiveAccount = { @@ -52,12 +50,18 @@ type InactiveAccount = ActiveAccount & { ]), ], }) -export class AccountSwitcherComponent implements OnInit, OnDestroy { - activeAccount?: ActiveAccount; - inactiveAccounts: { [userId: string]: InactiveAccount } = {}; - +export class AccountSwitcherComponent { + activeAccount$: Observable<ActiveAccount | null>; + inactiveAccounts$: Observable<{ [userId: string]: InactiveAccount }>; authStatus = AuthenticationStatus; + view$: Observable<{ + activeAccount: ActiveAccount | null; + inactiveAccounts: { [userId: string]: InactiveAccount }; + numberOfAccounts: number; + showSwitcher: boolean; + }>; + isOpen = false; overlayPosition: ConnectedPosition[] = [ { @@ -68,21 +72,9 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy { }, ]; - private destroy$ = new Subject<void>(); + showSwitcher$: Observable<boolean>; - get showSwitcher() { - const userIsInAVault = !Utils.isNullOrWhitespace(this.activeAccount?.email); - const userIsAddingAnAdditionalAccount = Object.keys(this.inactiveAccounts).length > 0; - return userIsInAVault || userIsAddingAnAdditionalAccount; - } - - get numberOfAccounts() { - if (this.inactiveAccounts == null) { - this.isOpen = false; - return 0; - } - return Object.keys(this.inactiveAccounts).length; - } + numberOfAccounts$: Observable<number>; constructor( private stateService: StateService, @@ -90,37 +82,65 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy { private avatarService: AvatarService, private messagingService: MessagingService, private router: Router, - private tokenService: TokenService, private environmentService: EnvironmentService, private loginEmailService: LoginEmailServiceAbstraction, - ) {} + private accountService: AccountService, + ) { + this.activeAccount$ = this.accountService.activeAccount$.pipe( + switchMap(async (active) => { + if (active == null) { + return null; + } - async ngOnInit(): Promise<void> { - this.stateService.accounts$ - .pipe( - concatMap(async (accounts: { [userId: string]: Account }) => { - this.inactiveAccounts = await this.createInactiveAccounts(accounts); + return { + id: active.id, + name: active.name, + email: active.email, + avatarColor: await firstValueFrom(this.avatarService.avatarColor$), + server: (await this.environmentService.getEnvironment())?.getHostname(), + }; + }), + ); + this.inactiveAccounts$ = combineLatest([ + this.activeAccount$, + this.accountService.accounts$, + this.authService.authStatuses$, + ]).pipe( + switchMap(async ([activeAccount, accounts, accountStatuses]) => { + // Filter out logged out accounts and active account + accounts = Object.fromEntries( + Object.entries(accounts).filter( + ([id]: [UserId, AccountInfo]) => + accountStatuses[id] !== AuthenticationStatus.LoggedOut || id === activeAccount?.id, + ), + ); + return this.createInactiveAccounts(accounts); + }), + ); + this.showSwitcher$ = combineLatest([this.activeAccount$, this.inactiveAccounts$]).pipe( + map(([activeAccount, inactiveAccounts]) => { + const hasActiveUser = activeAccount != null; + const userIsAddingAnAdditionalAccount = Object.keys(inactiveAccounts).length > 0; + return hasActiveUser || userIsAddingAnAdditionalAccount; + }), + ); + this.numberOfAccounts$ = this.inactiveAccounts$.pipe( + map((accounts) => Object.keys(accounts).length), + ); - try { - this.activeAccount = { - id: await this.tokenService.getUserId(), - name: (await this.tokenService.getName()) ?? (await this.tokenService.getEmail()), - email: await this.tokenService.getEmail(), - avatarColor: await firstValueFrom(this.avatarService.avatarColor$), - server: (await this.environmentService.getEnvironment())?.getHostname(), - }; - } catch { - this.activeAccount = undefined; - } - }), - takeUntil(this.destroy$), - ) - .subscribe(); - } - - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); + this.view$ = combineLatest([ + this.activeAccount$, + this.inactiveAccounts$, + this.numberOfAccounts$, + this.showSwitcher$, + ]).pipe( + map(([activeAccount, inactiveAccounts, numberOfAccounts, showSwitcher]) => ({ + activeAccount, + inactiveAccounts, + numberOfAccounts, + showSwitcher, + })), + ); } toggle() { @@ -144,11 +164,13 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy { await this.loginEmailService.saveEmailSettings(); await this.router.navigate(["/login"]); - await this.stateService.setActiveUser(null); + const activeAccount = await firstValueFrom(this.accountService.activeAccount$); + await this.stateService.clearDecryptedData(activeAccount?.id as UserId); + await this.accountService.switchAccount(null); } private async createInactiveAccounts(baseAccounts: { - [userId: string]: Account; + [userId: string]: AccountInfo; }): Promise<{ [userId: string]: InactiveAccount }> { const inactiveAccounts: { [userId: string]: InactiveAccount } = {}; @@ -159,8 +181,8 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy { inactiveAccounts[userId] = { id: userId, - name: baseAccounts[userId].profile.name, - email: baseAccounts[userId].profile.email, + name: baseAccounts[userId].name, + email: baseAccounts[userId].email, authenticationStatus: await this.authService.getAuthStatus(userId), avatarColor: await firstValueFrom(this.avatarService.getUserAvatarColor$(userId as UserId)), server: (await this.environmentService.getEnvironment(userId))?.getHostname(), diff --git a/apps/desktop/src/app/layout/search/search.component.ts b/apps/desktop/src/app/layout/search/search.component.ts index 9a7226218a..06c67d8af2 100644 --- a/apps/desktop/src/app/layout/search/search.component.ts +++ b/apps/desktop/src/app/layout/search/search.component.ts @@ -2,7 +2,7 @@ import { Component, OnDestroy, OnInit } from "@angular/core"; import { UntypedFormControl } from "@angular/forms"; import { Subscription } from "rxjs"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { SearchBarService, SearchBarState } from "./search-bar.service"; @@ -18,7 +18,7 @@ export class SearchComponent implements OnInit, OnDestroy { constructor( private searchBarService: SearchBarService, - private stateService: StateService, + private accountService: AccountService, ) { // eslint-disable-next-line rxjs-angular/prefer-takeuntil this.searchBarService.state$.subscribe((state) => { @@ -33,7 +33,7 @@ export class SearchComponent implements OnInit, OnDestroy { ngOnInit() { // eslint-disable-next-line rxjs-angular/prefer-takeuntil - this.activeAccountSubscription = this.stateService.activeAccount$.subscribe((value) => { + this.activeAccountSubscription = this.accountService.activeAccount$.subscribe((_) => { this.searchBarService.setSearchText(""); this.searchText.patchValue(""); }); diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index 1e3a7fdfa5..a485b925ba 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -59,7 +59,6 @@ import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/ge import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service"; 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"; @@ -102,7 +101,6 @@ const safeProviders: SafeProvider[] = [ safeProvider(InitService), safeProvider(NativeMessagingService), safeProvider(SearchBarService), - safeProvider(LoginGuard), safeProvider(DialogService), safeProvider({ provide: APP_INITIALIZER as SafeInjectionToken<() => void>, @@ -192,6 +190,7 @@ const safeProviders: SafeProvider[] = [ AutofillSettingsServiceAbstraction, VaultTimeoutSettingsService, BiometricStateService, + AccountServiceAbstraction, ], }), safeProvider({ diff --git a/apps/desktop/src/app/tools/generator.component.spec.ts b/apps/desktop/src/app/tools/generator.component.spec.ts index 51b5bf93a2..d908de8ef7 100644 --- a/apps/desktop/src/app/tools/generator.component.spec.ts +++ b/apps/desktop/src/app/tools/generator.component.spec.ts @@ -4,6 +4,7 @@ import { ActivatedRoute } from "@angular/router"; import { mock, MockProxy } from "jest-mock-extended"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.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"; @@ -59,6 +60,10 @@ describe("GeneratorComponent", () => { provide: CipherService, useValue: mock<CipherService>(), }, + { + provide: AccountService, + useValue: mock<AccountService>(), + }, ], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); diff --git a/apps/desktop/src/app/tools/send/add-edit.component.ts b/apps/desktop/src/app/tools/send/add-edit.component.ts index 7bdd5efbba..804a390438 100644 --- a/apps/desktop/src/app/tools/send/add-edit.component.ts +++ b/apps/desktop/src/app/tools/send/add-edit.component.ts @@ -4,6 +4,7 @@ import { FormBuilder } from "@angular/forms"; import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/tools/send/add-edit.component"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -34,6 +35,7 @@ export class AddEditComponent extends BaseAddEditComponent { dialogService: DialogService, formBuilder: FormBuilder, billingAccountProfileStateService: BillingAccountProfileStateService, + accountService: AccountService, ) { super( i18nService, @@ -49,6 +51,7 @@ export class AddEditComponent extends BaseAddEditComponent { dialogService, formBuilder, billingAccountProfileStateService, + accountService, ); } diff --git a/apps/desktop/src/auth/guards/login.guard.ts b/apps/desktop/src/auth/guards/login.guard.ts deleted file mode 100644 index f6c67d5af9..0000000000 --- a/apps/desktop/src/auth/guards/login.guard.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Injectable } from "@angular/core"; -import { CanActivate } from "@angular/router"; -import { firstValueFrom } from "rxjs"; - -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"; - -const maxAllowedAccounts = 5; - -@Injectable() -export class LoginGuard implements CanActivate { - protected homepage = "vault"; - constructor( - private stateService: StateService, - private platformUtilsService: PlatformUtilsService, - private i18nService: I18nService, - ) {} - - async canActivate() { - const accounts = await firstValueFrom(this.stateService.accounts$); - if (accounts != null && Object.keys(accounts).length >= maxAllowedAccounts) { - this.platformUtilsService.showToast("error", null, this.i18nService.t("accountLimitReached")); - return false; - } - - return true; - } -} diff --git a/apps/desktop/src/auth/guards/max-accounts.guard.ts b/apps/desktop/src/auth/guards/max-accounts.guard.ts new file mode 100644 index 0000000000..65c4ac99d0 --- /dev/null +++ b/apps/desktop/src/auth/guards/max-accounts.guard.ts @@ -0,0 +1,38 @@ +import { inject } from "@angular/core"; +import { CanActivateFn } from "@angular/router"; +import { Observable, map } from "rxjs"; + +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { ToastService } from "@bitwarden/components"; + +const maxAllowedAccounts = 5; + +function maxAccountsGuard(): Observable<boolean> { + const authService = inject(AuthService); + const toastService = inject(ToastService); + const i18nService = inject(I18nService); + + return authService.authStatuses$.pipe( + map((statuses) => + Object.values(statuses).filter((status) => status != AuthenticationStatus.LoggedOut), + ), + map((accounts) => { + if (accounts != null && Object.keys(accounts).length >= maxAllowedAccounts) { + toastService.showToast({ + variant: "error", + title: null, + message: i18nService.t("accountLimitReached"), + }); + return false; + } + + return true; + }), + ); +} + +export function maxAccountsGuardFn(): CanActivateFn { + return () => maxAccountsGuard(); +} diff --git a/apps/desktop/src/auth/lock.component.spec.ts b/apps/desktop/src/auth/lock.component.spec.ts index f998e75d7a..2137b707f6 100644 --- a/apps/desktop/src/auth/lock.component.spec.ts +++ b/apps/desktop/src/auth/lock.component.spec.ts @@ -13,6 +13,7 @@ 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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; @@ -50,7 +51,7 @@ describe("LockComponent", () => { let component: LockComponent; let fixture: ComponentFixture<LockComponent>; let stateServiceMock: MockProxy<StateService>; - const biometricStateService = mock<BiometricStateService>(); + let biometricStateService: MockProxy<BiometricStateService>; let messagingServiceMock: MockProxy<MessagingService>; let broadcasterServiceMock: MockProxy<BroadcasterService>; let platformUtilsServiceMock: MockProxy<PlatformUtilsService>; @@ -62,7 +63,6 @@ describe("LockComponent", () => { beforeEach(async () => { stateServiceMock = mock<StateService>(); - stateServiceMock.activeAccount$ = of(null); messagingServiceMock = mock<MessagingService>(); broadcasterServiceMock = mock<BroadcasterService>(); @@ -73,6 +73,7 @@ describe("LockComponent", () => { mockMasterPasswordService = new FakeMasterPasswordService(); + biometricStateService = mock(); biometricStateService.dismissedRequirePasswordOnStartCallout$ = of(false); biometricStateService.promptAutomatically$ = of(false); biometricStateService.promptCancelled$ = of(false); @@ -165,6 +166,10 @@ describe("LockComponent", () => { provide: AccountService, useValue: accountService, }, + { + provide: AuthService, + useValue: mock(), + }, { provide: KdfConfigService, useValue: mock<KdfConfigService>(), diff --git a/apps/desktop/src/auth/lock.component.ts b/apps/desktop/src/auth/lock.component.ts index 8e87b6663f..d95df419e1 100644 --- a/apps/desktop/src/auth/lock.component.ts +++ b/apps/desktop/src/auth/lock.component.ts @@ -10,6 +10,7 @@ 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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; @@ -64,6 +65,7 @@ export class LockComponent extends BaseLockComponent { pinCryptoService: PinCryptoServiceAbstraction, biometricStateService: BiometricStateService, accountService: AccountService, + authService: AuthService, kdfConfigService: KdfConfigService, ) { super( @@ -89,6 +91,7 @@ export class LockComponent extends BaseLockComponent { pinCryptoService, biometricStateService, accountService, + authService, kdfConfigService, ); } diff --git a/apps/desktop/src/main/menu/menubar.ts b/apps/desktop/src/main/menu/menubar.ts index eb1dacf825..b71774c5af 100644 --- a/apps/desktop/src/main/menu/menubar.ts +++ b/apps/desktop/src/main/menu/menubar.ts @@ -65,9 +65,10 @@ export class Menubar { isLocked = updateRequest.accounts[updateRequest.activeUserId]?.isLocked ?? true; } - const isLockable = !isLocked && updateRequest?.accounts[updateRequest.activeUserId]?.isLockable; + const isLockable = + !isLocked && updateRequest?.accounts?.[updateRequest.activeUserId]?.isLockable; const hasMasterPassword = - updateRequest?.accounts[updateRequest.activeUserId]?.hasMasterPassword ?? false; + updateRequest?.accounts?.[updateRequest.activeUserId]?.hasMasterPassword ?? false; this.items = [ new FileMenu( diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts index 1da2d94c15..1939bb11f5 100644 --- a/apps/web/src/app/app.component.ts +++ b/apps/web/src/app/app.component.ts @@ -2,7 +2,7 @@ import { DOCUMENT } from "@angular/common"; import { Component, Inject, NgZone, OnDestroy, OnInit } from "@angular/core"; import { NavigationEnd, Router } from "@angular/router"; import * as jq from "jquery"; -import { Subject, switchMap, takeUntil, timer } from "rxjs"; +import { Subject, firstValueFrom, map, switchMap, takeUntil, timer } from "rxjs"; import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service"; import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service"; @@ -10,6 +10,7 @@ import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.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 { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { PaymentMethodWarningsServiceAbstraction as PaymentMethodWarningService } from "@bitwarden/common/billing/abstractions/payment-method-warnings-service.abstraction"; @@ -51,7 +52,7 @@ const PaymentMethodWarningsRefresh = 60000; // 1 Minute templateUrl: "app.component.html", }) export class AppComponent implements OnDestroy, OnInit { - private lastActivity: number = null; + private lastActivity: Date = null; private idleTimer: number = null; private isIdle = false; private destroy$ = new Subject<void>(); @@ -86,6 +87,7 @@ export class AppComponent implements OnDestroy, OnInit { private stateEventRunnerService: StateEventRunnerService, private paymentMethodWarningService: PaymentMethodWarningService, private organizationService: InternalOrganizationServiceAbstraction, + private accountService: AccountService, ) {} ngOnInit() { @@ -298,15 +300,16 @@ export class AppComponent implements OnDestroy, OnInit { } private async recordActivity() { - const now = new Date().getTime(); - if (this.lastActivity != null && now - this.lastActivity < 250) { + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + const now = new Date(); + if (this.lastActivity != null && now.getTime() - this.lastActivity.getTime() < 250) { return; } this.lastActivity = now; - // 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.stateService.setLastActive(now); + await this.accountService.setAccountActivity(activeUserId, now); // Idle states if (this.isIdle) { this.isIdle = false; 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 e24013de6f..e2b3e7910a 100644 --- a/apps/web/src/app/layouts/header/web-header.component.html +++ b/apps/web/src/app/layouts/header/web-header.component.html @@ -58,7 +58,7 @@ [bitMenuTriggerFor]="accountMenu" class="tw-border-0 tw-bg-transparent tw-p-0" > - <dynamic-avatar [id]="account.userId" [text]="account | userName"></dynamic-avatar> + <dynamic-avatar [id]="account.id" [text]="account | userName"></dynamic-avatar> </button> <bit-menu #accountMenu> @@ -67,7 +67,7 @@ class="tw-flex tw-items-center tw-px-4 tw-py-1 tw-leading-tight tw-text-info" appStopProp > - <dynamic-avatar [id]="account.userId" [text]="account | userName"></dynamic-avatar> + <dynamic-avatar [id]="account.id" [text]="account | userName"></dynamic-avatar> <div class="tw-ml-2 tw-block tw-overflow-hidden tw-whitespace-nowrap"> <span>{{ "loggedInAs" | i18n }}</span> <small class="tw-block tw-overflow-hidden tw-whitespace-nowrap tw-text-muted"> diff --git a/apps/web/src/app/layouts/header/web-header.component.ts b/apps/web/src/app/layouts/header/web-header.component.ts index 1f012e52dd..9906bd53ba 100644 --- a/apps/web/src/app/layouts/header/web-header.component.ts +++ b/apps/web/src/app/layouts/header/web-header.component.ts @@ -1,16 +1,17 @@ import { Component, Input } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { combineLatest, map, Observable } from "rxjs"; +import { map, Observable } from "rxjs"; +import { User } from "@bitwarden/angular/pipes/user-name.pipe"; import { UnassignedItemsBannerService } from "@bitwarden/angular/services/unassigned-items-banner.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.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 { AccountProfile } from "@bitwarden/common/platform/models/domain/account"; +import { UserId } from "@bitwarden/common/types/guid"; @Component({ selector: "app-header", @@ -28,7 +29,7 @@ export class WebHeaderComponent { @Input() icon: string; protected routeData$: Observable<{ titleId: string }>; - protected account$: Observable<AccountProfile>; + protected account$: Observable<User & { id: UserId }>; protected canLock$: Observable<boolean>; protected selfHosted: boolean; protected hostname = location.hostname; @@ -38,12 +39,12 @@ export class WebHeaderComponent { constructor( private route: ActivatedRoute, - private stateService: StateService, private platformUtilsService: PlatformUtilsService, private vaultTimeoutSettingsService: VaultTimeoutSettingsService, private messagingService: MessagingService, protected unassignedItemsBannerService: UnassignedItemsBannerService, private configService: ConfigService, + private accountService: AccountService, ) { this.routeData$ = this.route.data.pipe( map((params) => { @@ -55,14 +56,7 @@ export class WebHeaderComponent { this.selfHosted = this.platformUtilsService.isSelfHost(); - this.account$ = combineLatest([ - this.stateService.activeAccount$, - this.stateService.accounts$, - ]).pipe( - map(([activeAccount, accounts]) => { - return accounts[activeAccount]?.profile; - }), - ); + this.account$ = this.accountService.activeAccount$; this.canLock$ = this.vaultTimeoutSettingsService .availableVaultTimeoutActions$() .pipe(map((actions) => actions.includes(VaultTimeoutAction.Lock))); diff --git a/apps/web/src/app/tools/send/add-edit.component.ts b/apps/web/src/app/tools/send/add-edit.component.ts index ee4be41488..cca416db9c 100644 --- a/apps/web/src/app/tools/send/add-edit.component.ts +++ b/apps/web/src/app/tools/send/add-edit.component.ts @@ -5,6 +5,7 @@ import { FormBuilder } from "@angular/forms"; import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/tools/send/add-edit.component"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -40,6 +41,7 @@ export class AddEditComponent extends BaseAddEditComponent { billingAccountProfileStateService: BillingAccountProfileStateService, protected dialogRef: DialogRef, @Inject(DIALOG_DATA) params: { sendId: string }, + accountService: AccountService, ) { super( i18nService, @@ -55,6 +57,7 @@ export class AddEditComponent extends BaseAddEditComponent { dialogService, formBuilder, billingAccountProfileStateService, + accountService, ); this.sendId = params.sendId; diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts b/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts index ad80c9f4e5..41aa766e3a 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts @@ -55,7 +55,6 @@ export default { { provide: StateService, useValue: { - activeAccount$: new BehaviorSubject("1").asObservable(), accounts$: new BehaviorSubject({ "1": { profile: { name: "Foo" } } }).asObservable(), async getShowFavicon() { return true; diff --git a/libs/angular/src/auth/components/lock.component.ts b/libs/angular/src/auth/components/lock.component.ts index 89af31da81..7eb30d759a 100644 --- a/libs/angular/src/auth/components/lock.component.ts +++ b/libs/angular/src/auth/components/lock.component.ts @@ -1,7 +1,7 @@ import { Directive, NgZone, OnDestroy, OnInit } from "@angular/core"; import { Router } from "@angular/router"; import { firstValueFrom, Subject } from "rxjs"; -import { concatMap, take, takeUntil } from "rxjs/operators"; +import { concatMap, map, take, takeUntil } from "rxjs/operators"; import { PinCryptoServiceAbstraction } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -11,10 +11,12 @@ import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abs 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 { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.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 { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request/secret-verification.request"; import { MasterPasswordPolicyResponse } from "@bitwarden/common/auth/models/response/master-password-policy.response"; @@ -30,6 +32,7 @@ import { BiometricStateService } from "@bitwarden/common/platform/biometrics/bio import { HashPurpose, KeySuffixOptions } from "@bitwarden/common/platform/enums"; import { PinLockType } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; +import { UserId } from "@bitwarden/common/types/guid"; import { UserKey } from "@bitwarden/common/types/key"; import { DialogService } from "@bitwarden/components"; @@ -46,6 +49,7 @@ export class LockComponent implements OnInit, OnDestroy { supportsBiometric: boolean; biometricLock: boolean; + private activeUserId: UserId; protected successRoute = "vault"; protected forcePasswordResetRoute = "update-temp-password"; protected onSuccessfulSubmit: () => Promise<void>; @@ -80,14 +84,16 @@ export class LockComponent implements OnInit, OnDestroy { protected pinCryptoService: PinCryptoServiceAbstraction, protected biometricStateService: BiometricStateService, protected accountService: AccountService, + protected authService: AuthService, protected kdfConfigService: KdfConfigService, ) {} async ngOnInit() { - this.stateService.activeAccount$ + this.accountService.activeAccount$ .pipe( - concatMap(async () => { - await this.load(); + concatMap(async (account) => { + this.activeUserId = account?.id; + await this.load(account?.id); }), takeUntil(this.destroy$), ) @@ -116,7 +122,7 @@ export class LockComponent implements OnInit, OnDestroy { }); if (confirmed) { - this.messagingService.send("logout"); + this.messagingService.send("logout", { userId: this.activeUserId }); } } @@ -321,23 +327,35 @@ export class LockComponent implements OnInit, OnDestroy { } } - private async load() { + private async load(userId: UserId) { // TODO: Investigate PM-3515 // The loading of the lock component works as follows: - // 1. First, is locking a valid timeout action? If not, we will log the user out. - // 2. If locking IS a valid timeout action, we proceed to show the user the lock screen. + // 1. If the user is unlocked, we're here in error so we navigate to the home page + // 2. First, is locking a valid timeout action? If not, we will log the user out. + // 3. If locking IS a valid timeout action, we proceed to show the user the lock screen. // The user will be able to unlock as follows: // - If they have a PIN set, they will be presented with the PIN input // - If they have a master password and no PIN, they will be presented with the master password input // - If they have biometrics enabled, they will be presented with the biometric prompt + const isUnlocked = await firstValueFrom( + this.authService + .authStatusFor$(userId) + .pipe(map((status) => status === AuthenticationStatus.Unlocked)), + ); + if (isUnlocked) { + // navigate to home + await this.router.navigate(["/"]); + return; + } + const availableVaultTimeoutActions = await firstValueFrom( - this.vaultTimeoutSettingsService.availableVaultTimeoutActions$(), + this.vaultTimeoutSettingsService.availableVaultTimeoutActions$(userId), ); const supportsLock = availableVaultTimeoutActions.includes(VaultTimeoutAction.Lock); if (!supportsLock) { - return await this.vaultTimeoutService.logOut(); + return await this.vaultTimeoutService.logOut(userId); } this.pinStatus = await this.vaultTimeoutSettingsService.isPinLockSet(); diff --git a/libs/angular/src/pipes/user-name.pipe.ts b/libs/angular/src/pipes/user-name.pipe.ts index 88b088a7e2..f007f4ad87 100644 --- a/libs/angular/src/pipes/user-name.pipe.ts +++ b/libs/angular/src/pipes/user-name.pipe.ts @@ -1,6 +1,6 @@ import { Pipe, PipeTransform } from "@angular/core"; -interface User { +export interface User { name?: string; email?: string; } diff --git a/libs/angular/src/tools/send/add-edit.component.ts b/libs/angular/src/tools/send/add-edit.component.ts index da859e50bf..b4f7ec171a 100644 --- a/libs/angular/src/tools/send/add-edit.component.ts +++ b/libs/angular/src/tools/send/add-edit.component.ts @@ -5,6 +5,7 @@ import { Subject, firstValueFrom, takeUntil, map, BehaviorSubject, concatMap } f import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -118,6 +119,7 @@ export class AddEditComponent implements OnInit, OnDestroy { protected dialogService: DialogService, protected formBuilder: FormBuilder, protected billingAccountProfileStateService: BillingAccountProfileStateService, + protected accountService: AccountService, ) { this.typeOptions = [ { name: i18nService.t("sendTypeFile"), value: SendType.File, premium: true }, @@ -215,7 +217,9 @@ export class AddEditComponent implements OnInit, OnDestroy { } async load() { - this.emailVerified = await this.stateService.getEmailVerified(); + this.emailVerified = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.emailVerified ?? false)), + ); this.type = !this.canAccessPremium || !this.emailVerified ? SendType.Text : SendType.File; if (this.send == null) { 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 a123e30053..0efb9569eb 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 @@ -128,6 +128,7 @@ describe("AuthRequestLoginStrategy", () => { masterPasswordService.masterKeySubject.next(masterKey); cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey); + tokenService.decodeAccessToken.mockResolvedValue({ sub: mockUserId }); await authRequestLoginStrategy.logIn(credentials); 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 3284f6e947..c3a8f61d78 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.spec.ts @@ -218,7 +218,7 @@ describe("LoginStrategy", () => { expect(messagingService.send).toHaveBeenCalledWith("loggedIn"); }); - it("throws if active account isn't found after being initialized", async () => { + it("throws if new account isn't active after being initialized", async () => { const idTokenResponse = identityTokenResponseFactory(); apiService.postIdentityToken.mockResolvedValue(idTokenResponse); @@ -228,7 +228,8 @@ describe("LoginStrategy", () => { stateService.getVaultTimeoutAction.mockResolvedValue(mockVaultTimeoutAction); stateService.getVaultTimeout.mockResolvedValue(mockVaultTimeout); - accountService.activeAccountSubject.next(null); + accountService.switchAccount = jest.fn(); // block internal switch to new account + accountService.activeAccountSubject.next(null); // simulate no active account await expect(async () => await passwordLoginStrategy.logIn(credentials)).rejects.toThrow(); }); diff --git a/libs/auth/src/common/login-strategies/login.strategy.ts b/libs/auth/src/common/login-strategies/login.strategy.ts index fd268d955e..3a3109349e 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.ts @@ -169,6 +169,12 @@ export abstract class LoginStrategy { const vaultTimeoutAction = await this.stateService.getVaultTimeoutAction({ userId }); const vaultTimeout = await this.stateService.getVaultTimeout({ userId }); + await this.accountService.addAccount(userId, { + name: accountInformation.name, + email: accountInformation.email, + emailVerified: accountInformation.email_verified, + }); + // 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( @@ -178,6 +184,8 @@ export abstract class LoginStrategy { tokenResponse.refreshToken, // Note: CLI login via API key sends undefined for refresh token. ); + await this.accountService.switchAccount(userId); + await this.stateService.addAccount( new Account({ profile: { 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 5c1fe9b1fe..c97639f102 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 @@ -164,6 +164,7 @@ describe("PasswordLoginStrategy", () => { masterPasswordService.masterKeySubject.next(masterKey); cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey); + tokenService.decodeAccessToken.mockResolvedValue({ sub: userId }); await passwordLoginStrategy.logIn(credentials); @@ -199,6 +200,7 @@ describe("PasswordLoginStrategy", () => { it("forces the user to update their master password on successful login when it does not meet master password policy requirements", async () => { passwordStrengthService.getPasswordStrength.mockReturnValue({ score: 0 } as any); policyService.evaluateMasterPassword.mockReturnValue(false); + tokenService.decodeAccessToken.mockResolvedValue({ sub: userId }); const result = await passwordLoginStrategy.logIn(credentials); @@ -213,6 +215,7 @@ describe("PasswordLoginStrategy", () => { it("forces the user to update their master password on successful 2FA login when it does not meet master password policy requirements", async () => { passwordStrengthService.getPasswordStrength.mockReturnValue({ score: 0 } as any); policyService.evaluateMasterPassword.mockReturnValue(false); + tokenService.decodeAccessToken.mockResolvedValue({ sub: userId }); const token2FAResponse = new IdentityTwoFactorResponse({ TwoFactorProviders: ["0"], 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 16479f19ea..ae1813d3d7 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 @@ -65,6 +65,7 @@ describe("UserDecryptionOptionsService", () => { await fakeAccountService.addAccount(givenUser, { name: "Test User 1", email: "test1@email.com", + emailVerified: false, }); await fakeStateProvider.setUserState( USER_DECRYPTION_OPTIONS, diff --git a/libs/common/spec/fake-account-service.ts b/libs/common/spec/fake-account-service.ts index a8b09b7417..649a158d75 100644 --- a/libs/common/spec/fake-account-service.ts +++ b/libs/common/spec/fake-account-service.ts @@ -1,5 +1,5 @@ import { mock } from "jest-mock-extended"; -import { ReplaySubject } from "rxjs"; +import { ReplaySubject, combineLatest, map } from "rxjs"; import { AccountInfo, AccountService } from "../src/auth/abstractions/account.service"; import { UserId } from "../src/types/guid"; @@ -7,15 +7,20 @@ import { UserId } from "../src/types/guid"; export function mockAccountServiceWith( userId: UserId, info: Partial<AccountInfo> = {}, + activity: Record<UserId, Date> = {}, ): FakeAccountService { const fullInfo: AccountInfo = { ...info, ...{ name: "name", email: "email", + emailVerified: true, }, }; - const service = new FakeAccountService({ [userId]: fullInfo }); + + const fullActivity = { [userId]: new Date(), ...activity }; + + const service = new FakeAccountService({ [userId]: fullInfo }, fullActivity); service.activeAccountSubject.next({ id: userId, ...fullInfo }); return service; } @@ -26,17 +31,46 @@ export class FakeAccountService implements AccountService { accountsSubject = new ReplaySubject<Record<UserId, AccountInfo>>(1); // eslint-disable-next-line rxjs/no-exposed-subjects -- test class activeAccountSubject = new ReplaySubject<{ id: UserId } & AccountInfo>(1); + // eslint-disable-next-line rxjs/no-exposed-subjects -- test class + accountActivitySubject = new ReplaySubject<Record<UserId, Date>>(1); private _activeUserId: UserId; get activeUserId() { return this._activeUserId; } accounts$ = this.accountsSubject.asObservable(); activeAccount$ = this.activeAccountSubject.asObservable(); + accountActivity$ = this.accountActivitySubject.asObservable(); + get sortedUserIds$() { + return this.accountActivity$.pipe( + map((activity) => { + return Object.entries(activity) + .map(([userId, lastActive]: [UserId, Date]) => ({ userId, lastActive })) + .sort((a, b) => a.lastActive.getTime() - b.lastActive.getTime()) + .map((a) => a.userId); + }), + ); + } + get nextUpAccount$() { + return combineLatest([this.accounts$, this.activeAccount$, this.sortedUserIds$]).pipe( + map(([accounts, activeAccount, sortedUserIds]) => { + const nextId = sortedUserIds.find((id) => id !== activeAccount?.id && accounts[id] != null); + return nextId ? { id: nextId, ...accounts[nextId] } : null; + }), + ); + } - constructor(initialData: Record<UserId, AccountInfo>) { + constructor(initialData: Record<UserId, AccountInfo>, accountActivity?: Record<UserId, Date>) { this.accountsSubject.next(initialData); this.activeAccountSubject.subscribe((data) => (this._activeUserId = data?.id)); this.activeAccountSubject.next(null); + this.accountActivitySubject.next(accountActivity); + } + setAccountActivity(userId: UserId, lastActivity: Date): Promise<void> { + this.accountActivitySubject.next({ + ...this.accountActivitySubject["_buffer"][0], + [userId]: lastActivity, + }); + return this.mock.setAccountActivity(userId, lastActivity); } async addAccount(userId: UserId, accountData: AccountInfo): Promise<void> { @@ -53,10 +87,27 @@ export class FakeAccountService implements AccountService { await this.mock.setAccountEmail(userId, email); } + async setAccountEmailVerified(userId: UserId, emailVerified: boolean): Promise<void> { + await this.mock.setAccountEmailVerified(userId, emailVerified); + } + async switchAccount(userId: UserId): Promise<void> { const next = userId == null ? null : { id: userId, ...this.accountsSubject["_buffer"]?.[0]?.[userId] }; this.activeAccountSubject.next(next); await this.mock.switchAccount(userId); } + + async clean(userId: UserId): Promise<void> { + const current = this.accountsSubject["_buffer"][0] ?? {}; + const updated = { ...current, [userId]: loggedOutInfo }; + this.accountsSubject.next(updated); + await this.mock.clean(userId); + } } + +const loggedOutInfo: AccountInfo = { + name: undefined, + email: "", + emailVerified: false, +}; diff --git a/libs/common/src/auth/abstractions/account.service.ts b/libs/common/src/auth/abstractions/account.service.ts index fa9ad36378..b7fd6d9bb9 100644 --- a/libs/common/src/auth/abstractions/account.service.ts +++ b/libs/common/src/auth/abstractions/account.service.ts @@ -8,18 +8,44 @@ import { UserId } from "../../types/guid"; */ export type AccountInfo = { email: string; + emailVerified: boolean; name: string | undefined; }; export function accountInfoEqual(a: AccountInfo, b: AccountInfo) { - return a?.email === b?.email && a?.name === b?.name; + if (a == null && b == null) { + return true; + } + + if (a == null || b == null) { + return false; + } + + const keys = new Set([...Object.keys(a), ...Object.keys(b)]) as Set<keyof AccountInfo>; + for (const key of keys) { + if (a[key] !== b[key]) { + return false; + } + } + return true; } export abstract class AccountService { accounts$: Observable<Record<UserId, AccountInfo>>; activeAccount$: Observable<{ id: UserId | undefined } & AccountInfo>; + + /** + * Observable of the last activity time for each account. + */ + accountActivity$: Observable<Record<UserId, Date>>; + /** Account list in order of descending recency */ + sortedUserIds$: Observable<UserId[]>; + /** Next account that is not the current active account */ + nextUpAccount$: Observable<{ id: UserId } & AccountInfo>; /** * Updates the `accounts$` observable with the new account data. + * + * @note Also sets the last active date of the account to `now`. * @param userId * @param accountData */ @@ -36,11 +62,30 @@ export abstract class AccountService { * @param email */ abstract setAccountEmail(userId: UserId, email: string): Promise<void>; + /** + * updates the `accounts$` observable with the new email verification status for the account. + * @param userId + * @param emailVerified + */ + abstract setAccountEmailVerified(userId: UserId, emailVerified: boolean): Promise<void>; /** * Updates the `activeAccount$` observable with the new active account. * @param userId */ abstract switchAccount(userId: UserId): Promise<void>; + /** + * Cleans personal information for the given account from the `accounts$` observable. Does not remove the userId from the observable. + * + * @note Also sets the last active date of the account to `null`. + * @param userId + */ + abstract clean(userId: UserId): Promise<void>; + /** + * Updates the given user's last activity time. + * @param userId + * @param lastActivity + */ + abstract setAccountActivity(userId: UserId, lastActivity: Date): Promise<void>; } export abstract class InternalAccountService extends AccountService { diff --git a/libs/common/src/auth/services/account.service.spec.ts b/libs/common/src/auth/services/account.service.spec.ts index a9cec82c51..0ae14b0cc1 100644 --- a/libs/common/src/auth/services/account.service.spec.ts +++ b/libs/common/src/auth/services/account.service.spec.ts @@ -1,3 +1,8 @@ +/** + * need to update test environment so structuredClone works appropriately + * @jest-environment ../../libs/shared/test.environment.ts + */ + import { MockProxy, mock } from "jest-mock-extended"; import { firstValueFrom } from "rxjs"; @@ -6,15 +11,57 @@ import { FakeGlobalStateProvider } from "../../../spec/fake-state-provider"; import { trackEmissions } from "../../../spec/utils"; import { LogService } from "../../platform/abstractions/log.service"; import { MessagingService } from "../../platform/abstractions/messaging.service"; +import { Utils } from "../../platform/misc/utils"; import { UserId } from "../../types/guid"; -import { AccountInfo } from "../abstractions/account.service"; +import { AccountInfo, accountInfoEqual } from "../abstractions/account.service"; import { ACCOUNT_ACCOUNTS, ACCOUNT_ACTIVE_ACCOUNT_ID, + ACCOUNT_ACTIVITY, AccountServiceImplementation, } from "./account.service"; +describe("accountInfoEqual", () => { + const accountInfo: AccountInfo = { name: "name", email: "email", emailVerified: true }; + + it("compares nulls", () => { + expect(accountInfoEqual(null, null)).toBe(true); + expect(accountInfoEqual(null, accountInfo)).toBe(false); + expect(accountInfoEqual(accountInfo, null)).toBe(false); + }); + + it("compares all keys, not just those defined in AccountInfo", () => { + const different = { ...accountInfo, extra: "extra" }; + + expect(accountInfoEqual(accountInfo, different)).toBe(false); + }); + + it("compares name", () => { + const same = { ...accountInfo }; + const different = { ...accountInfo, name: "name2" }; + + expect(accountInfoEqual(accountInfo, same)).toBe(true); + expect(accountInfoEqual(accountInfo, different)).toBe(false); + }); + + it("compares email", () => { + const same = { ...accountInfo }; + const different = { ...accountInfo, email: "email2" }; + + expect(accountInfoEqual(accountInfo, same)).toBe(true); + expect(accountInfoEqual(accountInfo, different)).toBe(false); + }); + + it("compares emailVerified", () => { + const same = { ...accountInfo }; + const different = { ...accountInfo, emailVerified: false }; + + expect(accountInfoEqual(accountInfo, same)).toBe(true); + expect(accountInfoEqual(accountInfo, different)).toBe(false); + }); +}); + describe("accountService", () => { let messagingService: MockProxy<MessagingService>; let logService: MockProxy<LogService>; @@ -22,8 +69,8 @@ describe("accountService", () => { let sut: AccountServiceImplementation; let accountsState: FakeGlobalState<Record<UserId, AccountInfo>>; let activeAccountIdState: FakeGlobalState<UserId>; - const userId = "userId" as UserId; - const userInfo = { email: "email", name: "name" }; + const userId = Utils.newGuid() as UserId; + const userInfo = { email: "email", name: "name", emailVerified: true }; beforeEach(() => { messagingService = mock(); @@ -86,6 +133,25 @@ describe("accountService", () => { expect(currentValue).toEqual({ [userId]: userInfo }); }); + + it("sets the last active date of the account to now", async () => { + const state = globalStateProvider.getFake(ACCOUNT_ACTIVITY); + state.stateSubject.next({}); + await sut.addAccount(userId, userInfo); + + expect(state.nextMock).toHaveBeenCalledWith({ [userId]: expect.any(Date) }); + }); + + it.each([null, undefined, 123, "not a guid"])( + "does not set last active if the userId is not a valid guid", + async (userId) => { + const state = globalStateProvider.getFake(ACCOUNT_ACTIVITY); + state.stateSubject.next({}); + await expect(sut.addAccount(userId as UserId, userInfo)).rejects.toThrow( + "userId is required", + ); + }, + ); }); describe("setAccountName", () => { @@ -134,6 +200,58 @@ describe("accountService", () => { }); }); + describe("setAccountEmailVerified", () => { + const initialState = { [userId]: userInfo }; + initialState[userId].emailVerified = false; + beforeEach(() => { + accountsState.stateSubject.next(initialState); + }); + + it("should update the account", async () => { + await sut.setAccountEmailVerified(userId, true); + const currentState = await firstValueFrom(accountsState.state$); + + expect(currentState).toEqual({ + [userId]: { ...userInfo, emailVerified: true }, + }); + }); + + it("should not update if the email is the same", async () => { + await sut.setAccountEmailVerified(userId, false); + const currentState = await firstValueFrom(accountsState.state$); + + expect(currentState).toEqual(initialState); + }); + }); + + describe("clean", () => { + beforeEach(() => { + accountsState.stateSubject.next({ [userId]: userInfo }); + }); + + it("removes account info of the given user", async () => { + await sut.clean(userId); + const currentState = await firstValueFrom(accountsState.state$); + + expect(currentState).toEqual({ + [userId]: { + email: "", + emailVerified: false, + name: undefined, + }, + }); + }); + + it("removes account activity of the given user", async () => { + const state = globalStateProvider.getFake(ACCOUNT_ACTIVITY); + state.stateSubject.next({ [userId]: new Date() }); + + await sut.clean(userId); + + expect(state.nextMock).toHaveBeenCalledWith({}); + }); + }); + describe("switchAccount", () => { beforeEach(() => { accountsState.stateSubject.next({ [userId]: userInfo }); @@ -152,4 +270,83 @@ describe("accountService", () => { expect(sut.switchAccount("unknown" as UserId)).rejects.toThrowError("Account does not exist"); }); }); + + describe("account activity", () => { + let state: FakeGlobalState<Record<UserId, Date>>; + + beforeEach(() => { + state = globalStateProvider.getFake(ACCOUNT_ACTIVITY); + }); + describe("accountActivity$", () => { + it("returns the account activity state", async () => { + state.stateSubject.next({ + [toId("user1")]: new Date(1), + [toId("user2")]: new Date(2), + }); + + await expect(firstValueFrom(sut.accountActivity$)).resolves.toEqual({ + [toId("user1")]: new Date(1), + [toId("user2")]: new Date(2), + }); + }); + + it("returns an empty object when account activity is null", async () => { + state.stateSubject.next(null); + + await expect(firstValueFrom(sut.accountActivity$)).resolves.toEqual({}); + }); + }); + + describe("sortedUserIds$", () => { + it("returns the sorted user ids by date with most recent first", async () => { + state.stateSubject.next({ + [toId("user1")]: new Date(3), + [toId("user2")]: new Date(2), + [toId("user3")]: new Date(1), + }); + + await expect(firstValueFrom(sut.sortedUserIds$)).resolves.toEqual([ + "user1" as UserId, + "user2" as UserId, + "user3" as UserId, + ]); + }); + + it("returns an empty array when account activity is null", async () => { + state.stateSubject.next(null); + + await expect(firstValueFrom(sut.sortedUserIds$)).resolves.toEqual([]); + }); + }); + + describe("setAccountActivity", () => { + const userId = Utils.newGuid() as UserId; + it("sets the account activity", async () => { + await sut.setAccountActivity(userId, new Date(1)); + + expect(state.nextMock).toHaveBeenCalledWith({ [userId]: new Date(1) }); + }); + + it("does not update if the activity is the same", async () => { + state.stateSubject.next({ [userId]: new Date(1) }); + + await sut.setAccountActivity(userId, new Date(1)); + + expect(state.nextMock).not.toHaveBeenCalled(); + }); + + it.each([null, undefined, 123, "not a guid"])( + "does not set last active if the userId is not a valid guid", + async (userId) => { + await sut.setAccountActivity(userId as UserId, new Date(1)); + + expect(state.nextMock).not.toHaveBeenCalled(); + }, + ); + }); + }); }); + +function toId(userId: string) { + return userId as UserId; +} diff --git a/libs/common/src/auth/services/account.service.ts b/libs/common/src/auth/services/account.service.ts index 77d61fae91..6740387ded 100644 --- a/libs/common/src/auth/services/account.service.ts +++ b/libs/common/src/auth/services/account.service.ts @@ -1,4 +1,4 @@ -import { Subject, combineLatestWith, map, distinctUntilChanged, shareReplay } from "rxjs"; +import { combineLatestWith, map, distinctUntilChanged, shareReplay, combineLatest } from "rxjs"; import { AccountInfo, @@ -7,8 +7,9 @@ import { } from "../../auth/abstractions/account.service"; import { LogService } from "../../platform/abstractions/log.service"; import { MessagingService } from "../../platform/abstractions/messaging.service"; +import { Utils } from "../../platform/misc/utils"; import { - ACCOUNT_MEMORY, + ACCOUNT_DISK, GlobalState, GlobalStateProvider, KeyDefinition, @@ -16,25 +17,36 @@ import { import { UserId } from "../../types/guid"; export const ACCOUNT_ACCOUNTS = KeyDefinition.record<AccountInfo, UserId>( - ACCOUNT_MEMORY, + ACCOUNT_DISK, "accounts", { deserializer: (accountInfo) => accountInfo, }, ); -export const ACCOUNT_ACTIVE_ACCOUNT_ID = new KeyDefinition(ACCOUNT_MEMORY, "activeAccountId", { +export const ACCOUNT_ACTIVE_ACCOUNT_ID = new KeyDefinition(ACCOUNT_DISK, "activeAccountId", { deserializer: (id: UserId) => id, }); +export const ACCOUNT_ACTIVITY = KeyDefinition.record<Date, UserId>(ACCOUNT_DISK, "activity", { + deserializer: (activity) => new Date(activity), +}); + +const LOGGED_OUT_INFO: AccountInfo = { + email: "", + emailVerified: false, + name: undefined, +}; + export class AccountServiceImplementation implements InternalAccountService { - private lock = new Subject<UserId>(); - private logout = new Subject<UserId>(); private accountsState: GlobalState<Record<UserId, AccountInfo>>; private activeAccountIdState: GlobalState<UserId | undefined>; accounts$; activeAccount$; + accountActivity$; + sortedUserIds$; + nextUpAccount$; constructor( private messagingService: MessagingService, @@ -53,14 +65,40 @@ export class AccountServiceImplementation implements InternalAccountService { distinctUntilChanged((a, b) => a?.id === b?.id && accountInfoEqual(a, b)), shareReplay({ bufferSize: 1, refCount: false }), ); + this.accountActivity$ = this.globalStateProvider + .get(ACCOUNT_ACTIVITY) + .state$.pipe(map((activity) => activity ?? {})); + this.sortedUserIds$ = this.accountActivity$.pipe( + map((activity) => { + return Object.entries(activity) + .map(([userId, lastActive]: [UserId, Date]) => ({ userId, lastActive })) + .sort((a, b) => b.lastActive.getTime() - a.lastActive.getTime()) // later dates first + .map((a) => a.userId); + }), + ); + this.nextUpAccount$ = combineLatest([ + this.accounts$, + this.activeAccount$, + this.sortedUserIds$, + ]).pipe( + map(([accounts, activeAccount, sortedUserIds]) => { + const nextId = sortedUserIds.find((id) => id !== activeAccount?.id && accounts[id] != null); + return nextId ? { id: nextId, ...accounts[nextId] } : null; + }), + ); } async addAccount(userId: UserId, accountData: AccountInfo): Promise<void> { + if (!Utils.isGuid(userId)) { + throw new Error("userId is required"); + } + await this.accountsState.update((accounts) => { accounts ||= {}; accounts[userId] = accountData; return accounts; }); + await this.setAccountActivity(userId, new Date()); } async setAccountName(userId: UserId, name: string): Promise<void> { @@ -71,6 +109,15 @@ export class AccountServiceImplementation implements InternalAccountService { await this.setAccountInfo(userId, { email }); } + async setAccountEmailVerified(userId: UserId, emailVerified: boolean): Promise<void> { + await this.setAccountInfo(userId, { emailVerified }); + } + + async clean(userId: UserId) { + await this.setAccountInfo(userId, LOGGED_OUT_INFO); + await this.removeAccountActivity(userId); + } + async switchAccount(userId: UserId): Promise<void> { await this.activeAccountIdState.update( (_, accounts) => { @@ -94,6 +141,37 @@ export class AccountServiceImplementation implements InternalAccountService { ); } + async setAccountActivity(userId: UserId, lastActivity: Date): Promise<void> { + if (!Utils.isGuid(userId)) { + // only store for valid userIds + return; + } + + await this.globalStateProvider.get(ACCOUNT_ACTIVITY).update( + (activity) => { + activity ||= {}; + activity[userId] = lastActivity; + return activity; + }, + { + shouldUpdate: (oldActivity) => oldActivity?.[userId]?.getTime() !== lastActivity?.getTime(), + }, + ); + } + + async removeAccountActivity(userId: UserId): Promise<void> { + await this.globalStateProvider.get(ACCOUNT_ACTIVITY).update( + (activity) => { + if (activity == null) { + return activity; + } + delete activity[userId]; + return activity; + }, + { shouldUpdate: (oldActivity) => oldActivity?.[userId] != null }, + ); + } + // TODO: update to use our own account status settings. Requires inverting direction of state service accounts flow async delete(): Promise<void> { try { diff --git a/libs/common/src/auth/services/auth.service.spec.ts b/libs/common/src/auth/services/auth.service.spec.ts index 3bdf85d3e1..9a93a4207b 100644 --- a/libs/common/src/auth/services/auth.service.spec.ts +++ b/libs/common/src/auth/services/auth.service.spec.ts @@ -56,6 +56,7 @@ describe("AuthService", () => { status: AuthenticationStatus.Unlocked, id: userId, email: "email", + emailVerified: false, name: "name", }; @@ -109,6 +110,7 @@ describe("AuthService", () => { status: AuthenticationStatus.Unlocked, id: Utils.newGuid() as UserId, email: "email2", + emailVerified: false, name: "name2", }; @@ -126,7 +128,11 @@ describe("AuthService", () => { it("requests auth status for all known users", async () => { const userId2 = Utils.newGuid() as UserId; - await accountService.addAccount(userId2, { email: "email2", name: "name2" }); + await accountService.addAccount(userId2, { + email: "email2", + emailVerified: false, + name: "name2", + }); const mockFn = jest.fn().mockReturnValue(of(AuthenticationStatus.Locked)); sut.authStatusFor$ = mockFn; @@ -147,11 +153,14 @@ describe("AuthService", () => { cryptoService.getInMemoryUserKeyFor$.mockReturnValue(of(undefined)); }); - it("emits LoggedOut when userId is null", async () => { - expect(await firstValueFrom(sut.authStatusFor$(null))).toEqual( - AuthenticationStatus.LoggedOut, - ); - }); + it.each([null, undefined, "not a userId"])( + "emits LoggedOut when userId is invalid (%s)", + 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)); diff --git a/libs/common/src/auth/services/auth.service.ts b/libs/common/src/auth/services/auth.service.ts index c9e711b4cc..a4529084a2 100644 --- a/libs/common/src/auth/services/auth.service.ts +++ b/libs/common/src/auth/services/auth.service.ts @@ -2,6 +2,7 @@ import { Observable, combineLatest, distinctUntilChanged, + firstValueFrom, map, of, shareReplay, @@ -12,6 +13,7 @@ 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 { UserId } from "../../types/guid"; import { AccountService } from "../abstractions/account.service"; import { AuthService as AuthServiceAbstraction } from "../abstractions/auth.service"; @@ -39,13 +41,16 @@ export class AuthService implements AuthServiceAbstraction { this.authStatuses$ = this.accountService.accounts$.pipe( map((accounts) => Object.keys(accounts) as UserId[]), - switchMap((entries) => - combineLatest( + switchMap((entries) => { + if (entries.length === 0) { + return of([] as { userId: UserId; status: AuthenticationStatus }[]); + } + return combineLatest( entries.map((userId) => this.authStatusFor$(userId).pipe(map((status) => ({ userId, status }))), ), - ), - ), + ); + }), map((statuses) => { return statuses.reduce( (acc, { userId, status }) => { @@ -59,7 +64,7 @@ export class AuthService implements AuthServiceAbstraction { } authStatusFor$(userId: UserId): Observable<AuthenticationStatus> { - if (userId == null) { + if (!Utils.isGuid(userId)) { return of(AuthenticationStatus.LoggedOut); } @@ -84,17 +89,8 @@ export class AuthService implements AuthServiceAbstraction { } async getAuthStatus(userId?: string): Promise<AuthenticationStatus> { - // If we don't have an access token or userId, we're logged out - const isAuthenticated = await this.stateService.getIsAuthenticated({ userId: userId }); - if (!isAuthenticated) { - return AuthenticationStatus.LoggedOut; - } - - // Note: since we aggresively set the auto user key to memory if it exists on app init (see InitService) - // we only need to check if the user key is in memory. - const hasUserKey = await this.cryptoService.hasUserKeyInMemory(userId as UserId); - - return hasUserKey ? AuthenticationStatus.Unlocked : AuthenticationStatus.Locked; + userId ??= await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))); + return await firstValueFrom(this.authStatusFor$(userId as UserId)); } logOut(callback: () => void) { 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 fc5060af5f..19b29f0593 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 @@ -90,6 +90,7 @@ describe("PasswordResetEnrollmentServiceImplementation", () => { const user1AccountInfo: AccountInfo = { name: "Test User 1", email: "test1@email.com", + emailVerified: true, }; activeAccountSubject.next(Object.assign(user1AccountInfo, { id: "userId" as UserId })); diff --git a/libs/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index 13c33305d1..5ca604b526 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -25,11 +25,10 @@ export type InitOptions = { export abstract class StateService<T extends Account = Account> { accounts$: Observable<{ [userId: string]: T }>; - activeAccount$: Observable<string>; addAccount: (account: T) => Promise<void>; - setActiveUser: (userId: string) => Promise<void>; - clean: (options?: StorageOptions) => Promise<UserId>; + clearDecryptedData: (userId: UserId) => Promise<void>; + clean: (options?: StorageOptions) => Promise<void>; init: (initOptions?: InitOptions) => Promise<void>; /** @@ -122,8 +121,6 @@ export abstract class StateService<T extends Account = Account> { setDuckDuckGoSharedKey: (value: string, options?: StorageOptions) => Promise<void>; getEmail: (options?: StorageOptions) => Promise<string>; setEmail: (value: string, options?: StorageOptions) => Promise<void>; - getEmailVerified: (options?: StorageOptions) => Promise<boolean>; - setEmailVerified: (value: boolean, options?: StorageOptions) => Promise<void>; getEnableBrowserIntegration: (options?: StorageOptions) => Promise<boolean>; setEnableBrowserIntegration: (value: boolean, options?: StorageOptions) => Promise<void>; getEnableBrowserIntegrationFingerprint: (options?: StorageOptions) => Promise<boolean>; @@ -147,8 +144,6 @@ export abstract class StateService<T extends Account = Account> { */ setEncryptedPinProtected: (value: string, options?: StorageOptions) => Promise<void>; getIsAuthenticated: (options?: StorageOptions) => Promise<boolean>; - getLastActive: (options?: StorageOptions) => Promise<number>; - setLastActive: (value: number, options?: StorageOptions) => Promise<void>; getLastSync: (options?: StorageOptions) => Promise<string>; setLastSync: (value: string, options?: StorageOptions) => Promise<void>; getMinimizeOnCopyToClipboard: (options?: StorageOptions) => Promise<boolean>; @@ -180,5 +175,4 @@ export abstract class StateService<T extends Account = Account> { setVaultTimeout: (value: number, options?: StorageOptions) => Promise<void>; getVaultTimeoutAction: (options?: StorageOptions) => Promise<string>; setVaultTimeoutAction: (value: string, options?: StorageOptions) => Promise<void>; - nextUpActiveUser: () => Promise<UserId>; } diff --git a/libs/common/src/platform/misc/utils.spec.ts b/libs/common/src/platform/misc/utils.spec.ts index a7a520a77c..964a2a1941 100644 --- a/libs/common/src/platform/misc/utils.spec.ts +++ b/libs/common/src/platform/misc/utils.spec.ts @@ -3,6 +3,33 @@ import * as path from "path"; import { Utils } from "./utils"; describe("Utils Service", () => { + describe("isGuid", () => { + it("is false when null", () => { + expect(Utils.isGuid(null)).toBe(false); + }); + + it("is false when undefined", () => { + expect(Utils.isGuid(undefined)).toBe(false); + }); + + it("is false when empty", () => { + expect(Utils.isGuid("")).toBe(false); + }); + + it("is false when not a string", () => { + expect(Utils.isGuid(123 as any)).toBe(false); + }); + + it("is false when not a guid", () => { + expect(Utils.isGuid("not a guid")).toBe(false); + }); + + it("is true when a guid", () => { + // we use a limited guid scope in which all zeroes is invalid + expect(Utils.isGuid("00000000-0000-1000-8000-000000000000")).toBe(true); + }); + }); + describe("getDomain", () => { it("should fail for invalid urls", () => { expect(Utils.getDomain(null)).toBeNull(); diff --git a/libs/common/src/platform/models/domain/state.ts b/libs/common/src/platform/models/domain/state.ts index 95557e082a..5dde49f99d 100644 --- a/libs/common/src/platform/models/domain/state.ts +++ b/libs/common/src/platform/models/domain/state.ts @@ -9,9 +9,6 @@ export class State< > { accounts: { [userId: string]: TAccount } = {}; globals: TGlobalState; - activeUserId: string; - authenticatedAccounts: string[] = []; - accountActivity: { [userId: string]: number } = {}; constructor(globals: TGlobalState) { this.globals = globals; 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 dd504dc302..7d266e93fc 100644 --- a/libs/common/src/platform/services/default-environment.service.spec.ts +++ b/libs/common/src/platform/services/default-environment.service.spec.ts @@ -31,10 +31,12 @@ describe("EnvironmentService", () => { [testUser]: { name: "name", email: "email", + emailVerified: false, }, [alternateTestUser]: { name: "name", email: "email", + emailVerified: false, }, }); stateProvider = new FakeStateProvider(accountService); @@ -47,6 +49,7 @@ describe("EnvironmentService", () => { id: userId, email: "test@example.com", name: `Test Name ${userId}`, + emailVerified: false, }); await awaitAsync(); }; diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index cab5768d2a..9479d64710 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -1,4 +1,4 @@ -import { BehaviorSubject } from "rxjs"; +import { BehaviorSubject, firstValueFrom, map } from "rxjs"; import { Jsonify, JsonValue } from "type-fest"; import { AccountService } from "../../auth/abstractions/account.service"; @@ -33,10 +33,7 @@ const keys = { state: "state", stateVersion: "stateVersion", global: "global", - authenticatedAccounts: "authenticatedAccounts", - activeUserId: "activeUserId", tempAccountSettings: "tempAccountSettings", // used to hold account specific settings (i.e clear clipboard) between initial migration and first account authentication - accountActivity: "accountActivity", }; const partialKeys = { @@ -58,9 +55,6 @@ export class StateService< protected accountsSubject = new BehaviorSubject<{ [userId: string]: TAccount }>({}); accounts$ = this.accountsSubject.asObservable(); - protected activeAccountSubject = new BehaviorSubject<string | null>(null); - activeAccount$ = this.activeAccountSubject.asObservable(); - private hasBeenInited = false; protected isRecoveredSession = false; @@ -112,36 +106,16 @@ export class StateService< } // Get all likely authenticated accounts - const authenticatedAccounts = ( - (await this.storageService.get<string[]>(keys.authenticatedAccounts)) ?? [] - ).filter((account) => account != null); + const authenticatedAccounts = await firstValueFrom( + this.accountService.accounts$.pipe(map((accounts) => Object.keys(accounts))), + ); await this.updateState(async (state) => { for (const i in authenticatedAccounts) { state = await this.syncAccountFromDisk(authenticatedAccounts[i]); } - // After all individual accounts have been added - state.authenticatedAccounts = authenticatedAccounts; - - const storedActiveUser = await this.storageService.get<string>(keys.activeUserId); - if (storedActiveUser != null) { - state.activeUserId = storedActiveUser; - } await this.pushAccounts(); - this.activeAccountSubject.next(state.activeUserId); - // TODO: Temporary update to avoid routing all account status changes through account service for now. - // account service tracks logged out accounts, but State service does not, so we need to add the active account - // if it's not in the accounts list. - if (state.activeUserId != null && this.accountsSubject.value[state.activeUserId] == null) { - const activeDiskAccount = await this.getAccountFromDisk({ userId: state.activeUserId }); - await this.accountService.addAccount(state.activeUserId as UserId, { - name: activeDiskAccount.profile.name, - email: activeDiskAccount.profile.email, - }); - } - await this.accountService.switchAccount(state.activeUserId as UserId); - // End TODO return state; }); @@ -161,61 +135,25 @@ export class StateService< return state; }); - // 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. - await this.accountService.addAccount(userId as UserId, { - name: diskAccount.profile.name, - email: diskAccount.profile.email, - }); - return state; } async addAccount(account: TAccount) { await this.environmentService.seedUserEnvironment(account.profile.userId as UserId); await this.updateState(async (state) => { - state.authenticatedAccounts.push(account.profile.userId); - await this.storageService.save(keys.authenticatedAccounts, state.authenticatedAccounts); state.accounts[account.profile.userId] = account; return state; }); await this.scaffoldNewAccountStorage(account); - 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, { - name: account.profile.name, - email: account.profile.email, - }); - await this.setActiveUser(account.profile.userId); } - async setActiveUser(userId: string): Promise<void> { - await this.clearDecryptedDataForActiveUser(); - await this.updateState(async (state) => { - state.activeUserId = userId; - await this.storageService.save(keys.activeUserId, userId); - this.activeAccountSubject.next(state.activeUserId); - // TODO: temporary update to avoid routing all account status changes through account service for now. - await this.accountService.switchAccount(userId as UserId); - - return state; - }); - - await this.pushAccounts(); - } - - async clean(options?: StorageOptions): Promise<UserId> { + async clean(options?: StorageOptions): Promise<void> { options = this.reconcileOptions(options, await this.defaultInMemoryOptions()); await this.deAuthenticateAccount(options.userId); - let currentUser = (await this.state())?.activeUserId; - if (options.userId === currentUser) { - currentUser = await this.dynamicallySetActiveUser(); - } await this.removeAccountFromDisk(options?.userId); await this.removeAccountFromMemory(options?.userId); await this.pushAccounts(); - return currentUser as UserId; } /** @@ -515,24 +453,6 @@ export class StateService< ); } - async getEmailVerified(options?: StorageOptions): Promise<boolean> { - return ( - (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) - ?.profile.emailVerified ?? false - ); - } - - async setEmailVerified(value: boolean, options?: StorageOptions): Promise<void> { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.profile.emailVerified = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - async getEnableBrowserIntegration(options?: StorageOptions): Promise<boolean> { return ( (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) @@ -642,35 +562,6 @@ export class StateService< ); } - async getLastActive(options?: StorageOptions): Promise<number> { - options = this.reconcileOptions(options, await this.defaultOnDiskOptions()); - - const accountActivity = await this.storageService.get<{ [userId: string]: number }>( - keys.accountActivity, - options, - ); - - if (accountActivity == null || Object.keys(accountActivity).length < 1) { - return null; - } - - return accountActivity[options.userId]; - } - - async setLastActive(value: number, options?: StorageOptions): Promise<void> { - options = this.reconcileOptions(options, await this.defaultOnDiskOptions()); - if (options.userId == null) { - return; - } - const accountActivity = - (await this.storageService.get<{ [userId: string]: number }>( - keys.accountActivity, - options, - )) ?? {}; - accountActivity[options.userId] = value; - await this.storageService.save(keys.accountActivity, accountActivity, options); - } - async getLastSync(options?: StorageOptions): Promise<string> { return ( await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions())) @@ -910,24 +801,28 @@ export class StateService< } protected async getAccountFromMemory(options: StorageOptions): Promise<TAccount> { + const userId = + options.userId ?? + (await firstValueFrom( + this.accountService.activeAccount$.pipe(map((account) => account?.id)), + )); + return await this.state().then(async (state) => { if (state.accounts == null) { return null; } - return state.accounts[await this.getUserIdFromMemory(options)]; - }); - } - - protected async getUserIdFromMemory(options: StorageOptions): Promise<string> { - return await this.state().then((state) => { - return options?.userId != null - ? state.accounts[options.userId]?.profile?.userId - : state.activeUserId; + return state.accounts[userId]; }); } protected async getAccountFromDisk(options: StorageOptions): Promise<TAccount> { - if (options?.userId == null && (await this.state())?.activeUserId == null) { + const userId = + options.userId ?? + (await firstValueFrom( + this.accountService.activeAccount$.pipe(map((account) => account?.id)), + )); + + if (userId == null) { return null; } @@ -1086,53 +981,76 @@ export class StateService< } protected async defaultInMemoryOptions(): Promise<StorageOptions> { + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((account) => account?.id)), + ); + return { storageLocation: StorageLocation.Memory, - userId: (await this.state()).activeUserId, + userId, }; } protected async defaultOnDiskOptions(): Promise<StorageOptions> { + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((account) => account?.id)), + ); + return { storageLocation: StorageLocation.Disk, htmlStorageLocation: HtmlStorageLocation.Session, - userId: (await this.state())?.activeUserId ?? (await this.getActiveUserIdFromStorage()), + userId, useSecureStorage: false, }; } protected async defaultOnDiskLocalOptions(): Promise<StorageOptions> { + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((account) => account?.id)), + ); + return { storageLocation: StorageLocation.Disk, htmlStorageLocation: HtmlStorageLocation.Local, - userId: (await this.state())?.activeUserId ?? (await this.getActiveUserIdFromStorage()), + userId, useSecureStorage: false, }; } protected async defaultOnDiskMemoryOptions(): Promise<StorageOptions> { + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((account) => account?.id)), + ); + return { storageLocation: StorageLocation.Disk, htmlStorageLocation: HtmlStorageLocation.Memory, - userId: (await this.state())?.activeUserId ?? (await this.getUserId()), + userId, useSecureStorage: false, }; } protected async defaultSecureStorageOptions(): Promise<StorageOptions> { + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((account) => account?.id)), + ); + return { storageLocation: StorageLocation.Disk, useSecureStorage: true, - userId: (await this.state())?.activeUserId ?? (await this.getActiveUserIdFromStorage()), + userId, }; } protected async getActiveUserIdFromStorage(): Promise<string> { - return await this.storageService.get<string>(keys.activeUserId); + return await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))); } protected async removeAccountFromLocalStorage(userId: string = null): Promise<void> { - userId = userId ?? (await this.state())?.activeUserId; + userId ??= await firstValueFrom( + this.accountService.activeAccount$.pipe(map((account) => account?.id)), + ); + const storedAccount = await this.getAccount( this.reconcileOptions({ userId: userId }, await this.defaultOnDiskLocalOptions()), ); @@ -1143,7 +1061,10 @@ export class StateService< } protected async removeAccountFromSessionStorage(userId: string = null): Promise<void> { - userId = userId ?? (await this.state())?.activeUserId; + userId ??= await firstValueFrom( + this.accountService.activeAccount$.pipe(map((account) => account?.id)), + ); + const storedAccount = await this.getAccount( this.reconcileOptions({ userId: userId }, await this.defaultOnDiskOptions()), ); @@ -1154,7 +1075,10 @@ export class StateService< } protected async removeAccountFromSecureStorage(userId: string = null): Promise<void> { - userId = userId ?? (await this.state())?.activeUserId; + userId ??= await firstValueFrom( + this.accountService.activeAccount$.pipe(map((account) => account?.id)), + ); + await this.setUserKeyAutoUnlock(null, { userId: userId }); await this.setUserKeyBiometric(null, { userId: userId }); await this.setCryptoMasterKeyAuto(null, { userId: userId }); @@ -1163,8 +1087,11 @@ export class StateService< } protected async removeAccountFromMemory(userId: string = null): Promise<void> { + userId ??= await firstValueFrom( + this.accountService.activeAccount$.pipe(map((account) => account?.id)), + ); + await this.updateState(async (state) => { - userId = userId ?? state.activeUserId; delete state.accounts[userId]; return state; }); @@ -1178,15 +1105,16 @@ export class StateService< return Object.assign(this.createAccount(), persistentAccountInformation); } - protected async clearDecryptedDataForActiveUser(): Promise<void> { + async clearDecryptedData(userId: UserId): Promise<void> { await this.updateState(async (state) => { - const userId = state?.activeUserId; if (userId != null && state?.accounts[userId]?.data != null) { state.accounts[userId].data = new AccountData(); } return state; }); + + await this.pushAccounts(); } protected createAccount(init: Partial<TAccount> = null): TAccount { @@ -1201,14 +1129,6 @@ export class StateService< // 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); - - await this.storageService.save(keys.authenticatedAccounts, state.authenticatedAccounts); - - return state; - }); } protected async removeAccountFromDisk(userId: string) { @@ -1217,32 +1137,6 @@ export class StateService< await this.removeAccountFromSecureStorage(userId); } - async nextUpActiveUser() { - const accounts = (await this.state())?.accounts; - if (accounts == null || Object.keys(accounts).length < 1) { - return null; - } - - let newActiveUser; - for (const userId in accounts) { - if (userId == null) { - continue; - } - if (await this.getIsAuthenticated({ userId: userId })) { - newActiveUser = userId; - break; - } - newActiveUser = null; - } - return newActiveUser as UserId; - } - - protected async dynamicallySetActiveUser() { - const newActiveUser = await this.nextUpActiveUser(); - await this.setActiveUser(newActiveUser); - return newActiveUser; - } - protected async saveSecureStorageKey<T extends JsonValue>( key: string, value: T, diff --git a/libs/common/src/platform/services/system.service.ts b/libs/common/src/platform/services/system.service.ts index d19390c45e..80053673d8 100644 --- a/libs/common/src/platform/services/system.service.ts +++ b/libs/common/src/platform/services/system.service.ts @@ -1,10 +1,12 @@ -import { firstValueFrom, timeout } from "rxjs"; +import { firstValueFrom, map, timeout } from "rxjs"; import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.service"; +import { AccountService } from "../../auth/abstractions/account.service"; import { AuthService } from "../../auth/abstractions/auth.service"; import { AuthenticationStatus } from "../../auth/enums/authentication-status"; import { AutofillSettingsServiceAbstraction } from "../../autofill/services/autofill-settings.service"; import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; +import { UserId } from "../../types/guid"; import { MessagingService } from "../abstractions/messaging.service"; import { PlatformUtilsService } from "../abstractions/platform-utils.service"; import { StateService } from "../abstractions/state.service"; @@ -25,15 +27,18 @@ export class SystemService implements SystemServiceAbstraction { private autofillSettingsService: AutofillSettingsServiceAbstraction, private vaultTimeoutSettingsService: VaultTimeoutSettingsService, private biometricStateService: BiometricStateService, + private accountService: AccountService, ) {} async startProcessReload(authService: AuthService): Promise<void> { - const accounts = await firstValueFrom(this.stateService.accounts$); + const accounts = await firstValueFrom(this.accountService.accounts$); if (accounts != null) { const keys = Object.keys(accounts); if (keys.length > 0) { for (const userId of keys) { - if ((await authService.getAuthStatus(userId)) === AuthenticationStatus.Unlocked) { + let status = await firstValueFrom(authService.authStatusFor$(userId as UserId)); + status = await authService.getAuthStatus(userId); + if (status === AuthenticationStatus.Unlocked) { return; } } @@ -63,15 +68,24 @@ export class SystemService implements SystemServiceAbstraction { clearInterval(this.reloadInterval); this.reloadInterval = null; - const currentUser = await firstValueFrom(this.stateService.activeAccount$.pipe(timeout(500))); + const currentUser = await firstValueFrom( + this.accountService.activeAccount$.pipe( + map((a) => a?.id), + timeout(500), + ), + ); // Replace current active user if they will be logged out on reload if (currentUser != null) { const timeoutAction = await firstValueFrom( this.vaultTimeoutSettingsService.vaultTimeoutAction$().pipe(timeout(500)), ); if (timeoutAction === VaultTimeoutAction.LogOut) { - const nextUser = await this.stateService.nextUpActiveUser(); - await this.stateService.setActiveUser(nextUser); + const nextUser = await firstValueFrom( + this.accountService.nextUpAccount$.pipe(map((account) => account?.id ?? null)), + ); + // Can be removed once we migrate password generation history to state providers + await this.stateService.clearDecryptedData(currentUser); + await this.accountService.switchAccount(nextUser); } } diff --git a/libs/common/src/platform/state/implementations/default-active-user-state.provider.spec.ts b/libs/common/src/platform/state/implementations/default-active-user-state.provider.spec.ts index c1cc15a176..681963f823 100644 --- a/libs/common/src/platform/state/implementations/default-active-user-state.provider.spec.ts +++ b/libs/common/src/platform/state/implementations/default-active-user-state.provider.spec.ts @@ -1,7 +1,6 @@ import { mock } from "jest-mock-extended"; import { mockAccountServiceWith, trackEmissions } from "../../../../spec"; -import { AuthenticationStatus } from "../../../auth/enums/authentication-status"; import { UserId } from "../../../types/guid"; import { SingleUserStateProvider } from "../user-state.provider"; @@ -14,7 +13,7 @@ describe("DefaultActiveUserStateProvider", () => { id: userId, name: "name", email: "email", - status: AuthenticationStatus.Locked, + emailVerified: false, }; const accountService = mockAccountServiceWith(userId, accountInfo); let sut: DefaultActiveUserStateProvider; 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 51a972a9dc..c652136a0d 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 @@ -82,6 +82,7 @@ describe("DefaultActiveUserState", () => { activeAccountSubject.next({ id: userId, email: `test${id}@example.com`, + emailVerified: false, name: `Test User ${id}`, }); await awaitAsync(); diff --git a/libs/common/src/platform/state/implementations/default-state.provider.spec.ts b/libs/common/src/platform/state/implementations/default-state.provider.spec.ts index 3243b53d67..98d423cf48 100644 --- a/libs/common/src/platform/state/implementations/default-state.provider.spec.ts +++ b/libs/common/src/platform/state/implementations/default-state.provider.spec.ts @@ -69,7 +69,12 @@ describe("DefaultStateProvider", () => { userId?: UserId, ) => Observable<string>, ) => { - const accountInfo = { email: "email", name: "name", status: AuthenticationStatus.LoggedOut }; + const accountInfo = { + email: "email", + emailVerified: false, + name: "name", + status: AuthenticationStatus.LoggedOut, + }; const keyDefinition = new KeyDefinition<string>(new StateDefinition("test", "disk"), "test", { deserializer: (s) => s, }); @@ -114,7 +119,12 @@ describe("DefaultStateProvider", () => { ); describe("getUserState$", () => { - const accountInfo = { email: "email", name: "name", status: AuthenticationStatus.LoggedOut }; + const accountInfo = { + email: "email", + emailVerified: false, + name: "name", + status: AuthenticationStatus.LoggedOut, + }; const keyDefinition = new KeyDefinition<string>(new StateDefinition("test", "disk"), "test", { deserializer: (s) => s, }); diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index ee5005202f..6b309ecfb9 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -38,6 +38,7 @@ export const BILLING_DISK = new StateDefinition("billing", "disk"); export const KDF_CONFIG_DISK = new StateDefinition("kdfConfig", "disk"); export const KEY_CONNECTOR_DISK = new StateDefinition("keyConnector", "disk"); export const ACCOUNT_MEMORY = new StateDefinition("account", "memory"); +export const ACCOUNT_DISK = new StateDefinition("account", "disk"); export const MASTER_PASSWORD_MEMORY = new StateDefinition("masterPassword", "memory"); export const MASTER_PASSWORD_DISK = new StateDefinition("masterPassword", "disk"); export const TWO_FACTOR_MEMORY = new StateDefinition("twoFactor", "memory"); 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 5344093a25..12c24dcdef 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,9 +1,10 @@ import { MockProxy, any, mock } from "jest-mock-extended"; -import { BehaviorSubject } from "rxjs"; +import { BehaviorSubject, of } 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 { AccountInfo } from "../../auth/abstractions/account.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"; @@ -13,7 +14,6 @@ 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"; @@ -39,7 +39,6 @@ describe("VaultTimeoutService", () => { let lockedCallback: jest.Mock<Promise<void>, [userId: string]>; let loggedOutCallback: jest.Mock<Promise<void>, [expired: boolean, userId?: string]>; - let accountsSubject: BehaviorSubject<Record<string, Account>>; let vaultTimeoutActionSubject: BehaviorSubject<VaultTimeoutAction>; let availableVaultTimeoutActionsSubject: BehaviorSubject<VaultTimeoutAction[]>; @@ -65,10 +64,6 @@ describe("VaultTimeoutService", () => { lockedCallback = jest.fn(); loggedOutCallback = jest.fn(); - accountsSubject = new BehaviorSubject(null); - - stateService.accounts$ = accountsSubject; - vaultTimeoutActionSubject = new BehaviorSubject(VaultTimeoutAction.Lock); vaultTimeoutSettingsService.vaultTimeoutAction$.mockReturnValue(vaultTimeoutActionSubject); @@ -127,21 +122,39 @@ describe("VaultTimeoutService", () => { return Promise.resolve(accounts[userId]?.vaultTimeout); }); - stateService.getLastActive.mockImplementation((options) => { - return Promise.resolve(accounts[options.userId]?.lastActive); - }); - stateService.getUserId.mockResolvedValue(globalSetups?.userId); - stateService.activeAccount$ = new BehaviorSubject<string>(globalSetups?.userId); - + // Set desired user active and known users on accounts service : note the only thing that matters here is that the ID are set if (globalSetups?.userId) { accountService.activeAccountSubject.next({ id: globalSetups.userId as UserId, email: null, + emailVerified: false, name: null, }); } + accountService.accounts$ = of( + Object.entries(accounts).reduce( + (agg, [id]) => { + agg[id] = { + email: "", + emailVerified: true, + name: "", + }; + return agg; + }, + {} as Record<string, AccountInfo>, + ), + ); + accountService.accountActivity$ = of( + Object.entries(accounts).reduce( + (agg, [id, info]) => { + agg[id] = info.lastActive ? new Date(info.lastActive) : null; + return agg; + }, + {} as Record<string, Date>, + ), + ); platformUtilsService.isViewOpen.mockResolvedValue(globalSetups?.isViewOpen ?? false); @@ -158,16 +171,6 @@ describe("VaultTimeoutService", () => { ], ); }); - - const accountsSubjectValue: Record<string, Account> = Object.keys(accounts).reduce( - (agg, key) => { - const newPartial: Record<string, unknown> = {}; - newPartial[key] = null; // No values actually matter on this other than the key - return Object.assign(agg, newPartial); - }, - {} as Record<string, Account>, - ); - accountsSubject.next(accountsSubjectValue); }; const expectUserToHaveLocked = (userId: string) => { 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 8baf6c04c4..8e0978d07d 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout.service.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout.service.ts @@ -1,4 +1,4 @@ -import { firstValueFrom, timeout } from "rxjs"; +import { combineLatest, firstValueFrom, switchMap } from "rxjs"; import { SearchService } from "../../abstractions/search.service"; import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.service"; @@ -64,14 +64,25 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { // Get whether or not the view is open a single time so it can be compared for each user const isViewOpen = await this.platformUtilsService.isViewOpen(); - const activeUserId = await firstValueFrom(this.stateService.activeAccount$.pipe(timeout(500))); - - const accounts = await firstValueFrom(this.stateService.accounts$); - for (const userId in accounts) { - if (userId != null && (await this.shouldLock(userId, activeUserId, isViewOpen))) { - await this.executeTimeoutAction(userId); - } - } + await firstValueFrom( + combineLatest([ + this.accountService.activeAccount$, + this.accountService.accountActivity$, + ]).pipe( + switchMap(async ([activeAccount, accountActivity]) => { + const activeUserId = activeAccount?.id; + for (const userIdString in accountActivity) { + const userId = userIdString as UserId; + if ( + userId != null && + (await this.shouldLock(userId, accountActivity[userId], activeUserId, isViewOpen)) + ) { + await this.executeTimeoutAction(userId); + } + } + }), + ), + ); } async lock(userId?: string): Promise<void> { @@ -123,6 +134,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { private async shouldLock( userId: string, + lastActive: Date, activeUserId: string, isViewOpen: boolean, ): Promise<boolean> { @@ -146,13 +158,12 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { return false; } - const lastActive = await this.stateService.getLastActive({ userId: userId }); if (lastActive == null) { return false; } const vaultTimeoutSeconds = vaultTimeout * 60; - const diffSeconds = (new Date().getTime() - lastActive) / 1000; + const diffSeconds = (new Date().getTime() - lastActive.getTime()) / 1000; return diffSeconds >= vaultTimeoutSeconds; } diff --git a/libs/common/src/state-migrations/migrate.ts b/libs/common/src/state-migrations/migrate.ts index 31bc5460b4..0a1f4b1d11 100644 --- a/libs/common/src/state-migrations/migrate.ts +++ b/libs/common/src/state-migrations/migrate.ts @@ -57,13 +57,14 @@ import { CipherServiceMigrator } from "./migrations/57-move-cipher-service-to-st import { RemoveRefreshTokenMigratedFlagMigrator } from "./migrations/58-remove-refresh-token-migrated-state-provider-flag"; import { KdfConfigMigrator } from "./migrations/59-move-kdf-config-to-state-provider"; import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key"; +import { KnownAccountsMigrator } from "./migrations/60-known-accounts"; import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account"; import { MoveStateVersionMigrator } from "./migrations/8-move-state-version"; import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-settings-to-global"; import { MinVersionMigrator } from "./migrations/min-version"; export const MIN_VERSION = 3; -export const CURRENT_VERSION = 59; +export const CURRENT_VERSION = 60; export type MinVersion = typeof MIN_VERSION; export function createMigrationBuilder() { @@ -124,7 +125,8 @@ export function createMigrationBuilder() { .with(AuthRequestMigrator, 55, 56) .with(CipherServiceMigrator, 56, 57) .with(RemoveRefreshTokenMigratedFlagMigrator, 57, 58) - .with(KdfConfigMigrator, 58, CURRENT_VERSION); + .with(KdfConfigMigrator, 58, 59) + .with(KnownAccountsMigrator, 59, CURRENT_VERSION); } export async function currentVersion( diff --git a/libs/common/src/state-migrations/migration-helper.spec.ts b/libs/common/src/state-migrations/migration-helper.spec.ts index 5f366f2597..162fac2fab 100644 --- a/libs/common/src/state-migrations/migration-helper.spec.ts +++ b/libs/common/src/state-migrations/migration-helper.spec.ts @@ -27,6 +27,14 @@ const exampleJSON = { }, global_serviceName_key: "global_serviceName_key", user_userId_serviceName_key: "user_userId_serviceName_key", + global_account_accounts: { + "c493ed01-4e08-4e88-abc7-332f380ca760": { + otherStuff: "otherStuff3", + }, + "23e61a5f-2ece-4f5e-b499-f0bc489482a9": { + otherStuff: "otherStuff4", + }, + }, }; describe("RemoveLegacyEtmKeyMigrator", () => { @@ -81,6 +89,41 @@ describe("RemoveLegacyEtmKeyMigrator", () => { const accounts = await sut.getAccounts(); expect(accounts).toEqual([]); }); + + it("handles global scoped known accounts for version 60 and after", async () => { + sut.currentVersion = 60; + const accounts = await sut.getAccounts(); + expect(accounts).toEqual([ + // Note, still gets values stored in state service objects, just grabs user ids from global + { + userId: "c493ed01-4e08-4e88-abc7-332f380ca760", + account: { otherStuff: "otherStuff1" }, + }, + { + userId: "23e61a5f-2ece-4f5e-b499-f0bc489482a9", + account: { otherStuff: "otherStuff2" }, + }, + ]); + }); + }); + + describe("getKnownUserIds", () => { + it("returns all user ids", async () => { + const userIds = await sut.getKnownUserIds(); + expect(userIds).toEqual([ + "c493ed01-4e08-4e88-abc7-332f380ca760", + "23e61a5f-2ece-4f5e-b499-f0bc489482a9", + ]); + }); + + it("returns all user ids when version is 60 or greater", async () => { + sut.currentVersion = 60; + const userIds = await sut.getKnownUserIds(); + expect(userIds).toEqual([ + "c493ed01-4e08-4e88-abc7-332f380ca760", + "23e61a5f-2ece-4f5e-b499-f0bc489482a9", + ]); + }); }); describe("getFromGlobal", () => { diff --git a/libs/common/src/state-migrations/migration-helper.ts b/libs/common/src/state-migrations/migration-helper.ts index 2505e2b264..5d1de8dd49 100644 --- a/libs/common/src/state-migrations/migration-helper.ts +++ b/libs/common/src/state-migrations/migration-helper.ts @@ -162,7 +162,7 @@ export class MigrationHelper { async getAccounts<ExpectedAccountType>(): Promise< { userId: string; account: ExpectedAccountType }[] > { - const userIds = (await this.get<string[]>("authenticatedAccounts")) ?? []; + const userIds = await this.getKnownUserIds(); return Promise.all( userIds.map(async (userId) => ({ userId, @@ -171,6 +171,17 @@ export class MigrationHelper { ); } + /** + * Helper method to read known users ids. + */ + async getKnownUserIds(): Promise<string[]> { + if (this.currentVersion < 61) { + return knownAccountUserIdsBuilderPre61(this.storageService); + } else { + return knownAccountUserIdsBuilder(this.storageService); + } + } + /** * Builds a user storage key appropriate for the current version. * @@ -233,3 +244,18 @@ function globalKeyBuilder(keyDefinition: KeyDefinitionLike): string { function globalKeyBuilderPre9(): string { throw Error("No key builder should be used for versions prior to 9."); } + +async function knownAccountUserIdsBuilderPre61( + storageService: AbstractStorageService, +): Promise<string[]> { + return (await storageService.get<string[]>("authenticatedAccounts")) ?? []; +} + +async function knownAccountUserIdsBuilder( + storageService: AbstractStorageService, +): Promise<string[]> { + const accounts = await storageService.get<Record<string, unknown>>( + globalKeyBuilder({ stateDefinition: { name: "account" }, key: "accounts" }), + ); + return Object.keys(accounts ?? {}); +} diff --git a/libs/common/src/state-migrations/migrations/60-known-accounts.spec.ts b/libs/common/src/state-migrations/migrations/60-known-accounts.spec.ts new file mode 100644 index 0000000000..28dedb3c39 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/60-known-accounts.spec.ts @@ -0,0 +1,145 @@ +import { MockProxy } from "jest-mock-extended"; + +import { MigrationHelper } from "../migration-helper"; +import { mockMigrationHelper } from "../migration-helper.spec"; + +import { + ACCOUNT_ACCOUNTS, + ACCOUNT_ACTIVE_ACCOUNT_ID, + ACCOUNT_ACTIVITY, + KnownAccountsMigrator, +} from "./60-known-accounts"; + +const migrateJson = () => { + return { + authenticatedAccounts: ["user1", "user2"], + activeUserId: "user1", + user1: { + profile: { + email: "user1", + name: "User 1", + emailVerified: true, + }, + }, + user2: { + profile: { + email: "", + emailVerified: false, + }, + }, + accountActivity: { + user1: 1609459200000, // 2021-01-01 + user2: 1609545600000, // 2021-01-02 + }, + }; +}; + +const rollbackJson = () => { + return { + user1: { + profile: { + email: "user1", + name: "User 1", + emailVerified: true, + }, + }, + user2: { + profile: { + email: "", + emailVerified: false, + }, + }, + global_account_accounts: { + user1: { + profile: { + email: "user1", + name: "User 1", + emailVerified: true, + }, + }, + user2: { + profile: { + email: "", + emailVerified: false, + }, + }, + }, + global_account_activeAccountId: "user1", + global_account_activity: { + user1: "2021-01-01T00:00:00.000Z", + user2: "2021-01-02T00:00:00.000Z", + }, + }; +}; + +describe("ReplicateKnownAccounts", () => { + let helper: MockProxy<MigrationHelper>; + let sut: KnownAccountsMigrator; + + describe("migrate", () => { + beforeEach(() => { + helper = mockMigrationHelper(migrateJson(), 59); + sut = new KnownAccountsMigrator(59, 60); + }); + + it("migrates accounts", async () => { + await sut.migrate(helper); + expect(helper.setToGlobal).toHaveBeenCalledWith(ACCOUNT_ACCOUNTS, { + user1: { + email: "user1", + name: "User 1", + emailVerified: true, + }, + user2: { + email: "", + emailVerified: false, + name: undefined, + }, + }); + expect(helper.remove).toHaveBeenCalledWith("authenticatedAccounts"); + }); + + it("migrates active account it", async () => { + await sut.migrate(helper); + expect(helper.setToGlobal).toHaveBeenCalledWith(ACCOUNT_ACTIVE_ACCOUNT_ID, "user1"); + expect(helper.remove).toHaveBeenCalledWith("activeUserId"); + }); + + it("migrates account activity", async () => { + await sut.migrate(helper); + expect(helper.setToGlobal).toHaveBeenCalledWith(ACCOUNT_ACTIVITY, { + user1: '"2021-01-01T00:00:00.000Z"', + user2: '"2021-01-02T00:00:00.000Z"', + }); + expect(helper.remove).toHaveBeenCalledWith("accountActivity"); + }); + }); + + describe("rollback", () => { + beforeEach(() => { + helper = mockMigrationHelper(rollbackJson(), 60); + sut = new KnownAccountsMigrator(59, 60); + }); + + it("rolls back authenticated accounts", async () => { + await sut.rollback(helper); + expect(helper.set).toHaveBeenCalledWith("authenticatedAccounts", ["user1", "user2"]); + expect(helper.removeFromGlobal).toHaveBeenCalledWith(ACCOUNT_ACCOUNTS); + }); + + it("rolls back active account id", async () => { + await sut.rollback(helper); + expect(helper.set).toHaveBeenCalledWith("activeUserId", "user1"); + expect(helper.removeFromGlobal).toHaveBeenCalledWith(ACCOUNT_ACTIVE_ACCOUNT_ID); + }); + + it("rolls back account activity", async () => { + await sut.rollback(helper); + expect(helper.set).toHaveBeenCalledWith("accountActivity", { + user1: 1609459200000, + user2: 1609545600000, + }); + expect(helper.removeFromGlobal).toHaveBeenCalledWith(ACCOUNT_ACTIVITY); + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/60-known-accounts.ts b/libs/common/src/state-migrations/migrations/60-known-accounts.ts new file mode 100644 index 0000000000..75117da5b4 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/60-known-accounts.ts @@ -0,0 +1,111 @@ +import { KeyDefinitionLike, MigrationHelper } from "../migration-helper"; +import { Migrator } from "../migrator"; + +export const ACCOUNT_ACCOUNTS: KeyDefinitionLike = { + stateDefinition: { + name: "account", + }, + key: "accounts", +}; + +export const ACCOUNT_ACTIVE_ACCOUNT_ID: KeyDefinitionLike = { + stateDefinition: { + name: "account", + }, + key: "activeAccountId", +}; + +export const ACCOUNT_ACTIVITY: KeyDefinitionLike = { + stateDefinition: { + name: "account", + }, + key: "activity", +}; + +type ExpectedAccountType = { + profile?: { + email?: string; + name?: string; + emailVerified?: boolean; + }; +}; + +export class KnownAccountsMigrator extends Migrator<59, 60> { + async migrate(helper: MigrationHelper): Promise<void> { + await this.migrateAuthenticatedAccounts(helper); + await this.migrateActiveAccountId(helper); + await this.migrateAccountActivity(helper); + } + async rollback(helper: MigrationHelper): Promise<void> { + // authenticated account are removed, but the accounts record also contains logged out accounts. Best we can do is to add them all back + const accounts = (await helper.getFromGlobal<Record<string, unknown>>(ACCOUNT_ACCOUNTS)) ?? {}; + await helper.set("authenticatedAccounts", Object.keys(accounts)); + await helper.removeFromGlobal(ACCOUNT_ACCOUNTS); + + // Active Account Id + const activeAccountId = await helper.getFromGlobal<string>(ACCOUNT_ACTIVE_ACCOUNT_ID); + if (activeAccountId) { + await helper.set("activeUserId", activeAccountId); + } + await helper.removeFromGlobal(ACCOUNT_ACTIVE_ACCOUNT_ID); + + // Account Activity + const accountActivity = await helper.getFromGlobal<Record<string, string>>(ACCOUNT_ACTIVITY); + if (accountActivity) { + const toStore = Object.entries(accountActivity).reduce( + (agg, [userId, dateString]) => { + agg[userId] = new Date(dateString).getTime(); + return agg; + }, + {} as Record<string, number>, + ); + await helper.set("accountActivity", toStore); + } + await helper.removeFromGlobal(ACCOUNT_ACTIVITY); + } + + private async migrateAuthenticatedAccounts(helper: MigrationHelper) { + const authenticatedAccounts = (await helper.get<string[]>("authenticatedAccounts")) ?? []; + const accounts = await Promise.all( + authenticatedAccounts.map(async (userId) => { + const account = await helper.get<ExpectedAccountType>(userId); + return { userId, account }; + }), + ); + const accountsToStore = accounts.reduce( + (agg, { userId, account }) => { + if (account?.profile) { + agg[userId] = { + email: account.profile.email ?? "", + emailVerified: account.profile.emailVerified ?? false, + name: account.profile.name, + }; + } + return agg; + }, + {} as Record<string, { email: string; emailVerified: boolean; name: string | undefined }>, + ); + + await helper.setToGlobal(ACCOUNT_ACCOUNTS, accountsToStore); + await helper.remove("authenticatedAccounts"); + } + + private async migrateAccountActivity(helper: MigrationHelper) { + const stored = await helper.get<Record<string, Date>>("accountActivity"); + const accountActivity = Object.entries(stored ?? {}).reduce( + (agg, [userId, dateMs]) => { + agg[userId] = JSON.stringify(new Date(dateMs)); + return agg; + }, + {} as Record<string, string>, + ); + await helper.setToGlobal(ACCOUNT_ACTIVITY, accountActivity); + await helper.remove("accountActivity"); + } + + private async migrateActiveAccountId(helper: MigrationHelper) { + const activeAccountId = await helper.get<string>("activeUserId"); + await helper.setToGlobal(ACCOUNT_ACTIVE_ACCOUNT_ID, activeAccountId); + await helper.remove("activeUserId"); + } +} 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 41183c42af..2f0f50c616 100644 --- a/libs/common/src/tools/send/services/send.service.spec.ts +++ b/libs/common/src/tools/send/services/send.service.spec.ts @@ -62,6 +62,7 @@ describe("SendService", () => { accountService.activeAccountSubject.next({ id: mockUserId, email: "email", + emailVerified: false, name: "name", }); diff --git a/libs/common/src/vault/services/sync/sync.service.ts b/libs/common/src/vault/services/sync/sync.service.ts index 73869ff488..995ab7319b 100644 --- a/libs/common/src/vault/services/sync/sync.service.ts +++ b/libs/common/src/vault/services/sync/sync.service.ts @@ -326,7 +326,10 @@ export class SyncService implements SyncServiceAbstraction { await this.cryptoService.setOrgKeys(response.organizations, response.providerOrganizations); await this.avatarService.setSyncAvatarColor(response.id as UserId, response.avatarColor); await this.tokenService.setSecurityStamp(response.securityStamp, response.id as UserId); - await this.stateService.setEmailVerified(response.emailVerified); + await this.accountService.setAccountEmailVerified( + response.id as UserId, + response.emailVerified, + ); await this.billingAccountProfileStateService.setHasPremium( response.premiumPersonally, From e7416384dcb1b1b242a4672366bf5a6e81d1ee09 Mon Sep 17 00:00:00 2001 From: Will Martin <contact@willmartian.com> Date: Tue, 30 Apr 2024 10:27:47 -0400 Subject: [PATCH 322/351] [CL-220] item components (#8870) --- .../popup/layout/popup-layout.stories.ts | 41 ++- .../src/a11y/a11y-cell.directive.ts | 33 ++ .../src/a11y/a11y-grid.directive.ts | 145 ++++++++ .../components/src/a11y/a11y-row.directive.ts | 31 ++ libs/components/src/badge/badge.directive.ts | 9 +- .../src/icon-button/icon-button.component.ts | 16 +- libs/components/src/index.ts | 1 + .../src/input/autofocus.directive.ts | 9 +- libs/components/src/item/index.ts | 1 + .../src/item/item-action.component.ts | 12 + .../src/item/item-content.component.html | 16 + .../src/item/item-content.component.ts | 15 + .../src/item/item-group.component.ts | 13 + libs/components/src/item/item.component.html | 21 ++ libs/components/src/item/item.component.ts | 29 ++ libs/components/src/item/item.mdx | 141 ++++++++ libs/components/src/item/item.module.ts | 12 + libs/components/src/item/item.stories.ts | 326 ++++++++++++++++++ .../components/src/search/search.component.ts | 6 +- .../src/shared/focusable-element.ts | 8 + libs/components/src/styles.scss | 2 +- 21 files changed, 858 insertions(+), 29 deletions(-) create mode 100644 libs/components/src/a11y/a11y-cell.directive.ts create mode 100644 libs/components/src/a11y/a11y-grid.directive.ts create mode 100644 libs/components/src/a11y/a11y-row.directive.ts create mode 100644 libs/components/src/item/index.ts create mode 100644 libs/components/src/item/item-action.component.ts create mode 100644 libs/components/src/item/item-content.component.html create mode 100644 libs/components/src/item/item-content.component.ts create mode 100644 libs/components/src/item/item-group.component.ts create mode 100644 libs/components/src/item/item.component.html create mode 100644 libs/components/src/item/item.component.ts create mode 100644 libs/components/src/item/item.mdx create mode 100644 libs/components/src/item/item.module.ts create mode 100644 libs/components/src/item/item.stories.ts create mode 100644 libs/components/src/shared/focusable-element.ts diff --git a/apps/browser/src/platform/popup/layout/popup-layout.stories.ts b/apps/browser/src/platform/popup/layout/popup-layout.stories.ts index 1b10e50c0c..77530d06e5 100644 --- a/apps/browser/src/platform/popup/layout/popup-layout.stories.ts +++ b/apps/browser/src/platform/popup/layout/popup-layout.stories.ts @@ -6,9 +6,11 @@ import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/an import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { AvatarModule, + BadgeModule, ButtonModule, I18nMockService, IconButtonModule, + ItemModule, } from "@bitwarden/components"; import { PopupFooterComponent } from "./popup-footer.component"; @@ -30,23 +32,34 @@ class ExtensionContainerComponent {} @Component({ selector: "vault-placeholder", template: ` - <div class="tw-mb-8 tw-text-main">vault item</div> - <div class="tw-my-8 tw-text-main">vault item</div> - <div class="tw-my-8 tw-text-main">vault item</div> - <div class="tw-my-8 tw-text-main">vault item</div> - <div class="tw-my-8 tw-text-main">vault item</div> - <div class="tw-my-8 tw-text-main">vault item</div> - <div class="tw-my-8 tw-text-main">vault item</div> - <div class="tw-my-8 tw-text-main">vault item</div> - <div class="tw-my-8 tw-text-main">vault item</div> - <div class="tw-my-8 tw-text-main">vault item</div> - <div class="tw-my-8 tw-text-main">vault item</div> - <div class="tw-my-8 tw-text-main">vault item</div> - <div class="tw-my-8 tw-text-main">vault item last item</div> + <bit-item-group aria-label="Mock Vault Items"> + <bit-item *ngFor="let item of data; index as i"> + <button bit-item-content> + <i slot="start" class="bwi bwi-globe tw-text-3xl tw-text-muted" aria-hidden="true"></i> + {{ i }} of {{ data.length - 1 }} + <span slot="secondary">Bar</span> + </button> + + <ng-container slot="end"> + <bit-item-action> + <button type="button" bitBadge variant="primary">Auto-fill</button> + </bit-item-action> + <bit-item-action> + <button type="button" bitIconButton="bwi-clone" aria-label="Copy item"></button> + </bit-item-action> + <bit-item-action> + <button type="button" bitIconButton="bwi-ellipsis-v" aria-label="More options"></button> + </bit-item-action> + </ng-container> + </bit-item> + </bit-item-group> `, standalone: true, + imports: [CommonModule, ItemModule, BadgeModule, IconButtonModule], }) -class VaultComponent {} +class VaultComponent { + protected data = Array.from(Array(20).keys()); +} @Component({ selector: "generator-placeholder", diff --git a/libs/components/src/a11y/a11y-cell.directive.ts b/libs/components/src/a11y/a11y-cell.directive.ts new file mode 100644 index 0000000000..fdd75c076f --- /dev/null +++ b/libs/components/src/a11y/a11y-cell.directive.ts @@ -0,0 +1,33 @@ +import { ContentChild, Directive, ElementRef, HostBinding } from "@angular/core"; + +import { FocusableElement } from "../shared/focusable-element"; + +@Directive({ + selector: "bitA11yCell", + standalone: true, + providers: [{ provide: FocusableElement, useExisting: A11yCellDirective }], +}) +export class A11yCellDirective implements FocusableElement { + @HostBinding("attr.role") + role: "gridcell" | null; + + @ContentChild(FocusableElement) + private focusableChild: FocusableElement; + + getFocusTarget() { + let focusTarget: HTMLElement; + if (this.focusableChild) { + focusTarget = this.focusableChild.getFocusTarget(); + } else { + focusTarget = this.elementRef.nativeElement.querySelector("button, a"); + } + + if (!focusTarget) { + return this.elementRef.nativeElement; + } + + return focusTarget; + } + + constructor(private elementRef: ElementRef<HTMLElement>) {} +} diff --git a/libs/components/src/a11y/a11y-grid.directive.ts b/libs/components/src/a11y/a11y-grid.directive.ts new file mode 100644 index 0000000000..c632376f4f --- /dev/null +++ b/libs/components/src/a11y/a11y-grid.directive.ts @@ -0,0 +1,145 @@ +import { + AfterViewInit, + ContentChildren, + Directive, + HostBinding, + HostListener, + Input, + QueryList, +} from "@angular/core"; + +import type { A11yCellDirective } from "./a11y-cell.directive"; +import { A11yRowDirective } from "./a11y-row.directive"; + +@Directive({ + selector: "bitA11yGrid", + standalone: true, +}) +export class A11yGridDirective implements AfterViewInit { + @HostBinding("attr.role") + role = "grid"; + + @ContentChildren(A11yRowDirective) + rows: QueryList<A11yRowDirective>; + + /** The number of pages to navigate on `PageUp` and `PageDown` */ + @Input() pageSize = 5; + + private grid: A11yCellDirective[][]; + + /** The row that currently has focus */ + private activeRow = 0; + + /** The cell that currently has focus */ + private activeCol = 0; + + @HostListener("keydown", ["$event"]) + onKeyDown(event: KeyboardEvent) { + switch (event.code) { + case "ArrowUp": + this.updateCellFocusByDelta(-1, 0); + break; + case "ArrowRight": + this.updateCellFocusByDelta(0, 1); + break; + case "ArrowDown": + this.updateCellFocusByDelta(1, 0); + break; + case "ArrowLeft": + this.updateCellFocusByDelta(0, -1); + break; + case "Home": + this.updateCellFocusByDelta(-this.activeRow, -this.activeCol); + break; + case "End": + this.updateCellFocusByDelta(this.grid.length, this.grid[this.grid.length - 1].length); + break; + case "PageUp": + this.updateCellFocusByDelta(-this.pageSize, 0); + break; + case "PageDown": + this.updateCellFocusByDelta(this.pageSize, 0); + break; + default: + return; + } + + /** Prevent default scrolling behavior */ + event.preventDefault(); + } + + ngAfterViewInit(): void { + this.initializeGrid(); + } + + private initializeGrid(): void { + try { + this.grid = this.rows.map((listItem) => { + listItem.role = "row"; + return [...listItem.cells]; + }); + this.grid.flat().forEach((cell) => { + cell.role = "gridcell"; + cell.getFocusTarget().tabIndex = -1; + }); + + this.getActiveCellContent().tabIndex = 0; + } catch (error) { + // eslint-disable-next-line no-console + console.error("Unable to initialize grid"); + } + } + + /** Get the focusable content of the active cell */ + private getActiveCellContent(): HTMLElement { + return this.grid[this.activeRow][this.activeCol].getFocusTarget(); + } + + /** Move focus via a delta against the currently active gridcell */ + private updateCellFocusByDelta(rowDelta: number, colDelta: number) { + const prevActive = this.getActiveCellContent(); + + this.activeCol += colDelta; + this.activeRow += rowDelta; + + // Row upper bound + if (this.activeRow >= this.grid.length) { + this.activeRow = this.grid.length - 1; + } + + // Row lower bound + if (this.activeRow < 0) { + this.activeRow = 0; + } + + // Column upper bound + if (this.activeCol >= this.grid[this.activeRow].length) { + if (this.activeRow < this.grid.length - 1) { + // Wrap to next row on right arrow + this.activeCol = 0; + this.activeRow += 1; + } else { + this.activeCol = this.grid[this.activeRow].length - 1; + } + } + + // Column lower bound + if (this.activeCol < 0) { + if (this.activeRow > 0) { + // Wrap to prev row on left arrow + this.activeRow -= 1; + this.activeCol = this.grid[this.activeRow].length - 1; + } else { + this.activeCol = 0; + } + } + + const nextActive = this.getActiveCellContent(); + nextActive.tabIndex = 0; + nextActive.focus(); + + if (nextActive !== prevActive) { + prevActive.tabIndex = -1; + } + } +} diff --git a/libs/components/src/a11y/a11y-row.directive.ts b/libs/components/src/a11y/a11y-row.directive.ts new file mode 100644 index 0000000000..e062eb2b5a --- /dev/null +++ b/libs/components/src/a11y/a11y-row.directive.ts @@ -0,0 +1,31 @@ +import { + AfterViewInit, + ContentChildren, + Directive, + HostBinding, + QueryList, + ViewChildren, +} from "@angular/core"; + +import { A11yCellDirective } from "./a11y-cell.directive"; + +@Directive({ + selector: "bitA11yRow", + standalone: true, +}) +export class A11yRowDirective implements AfterViewInit { + @HostBinding("attr.role") + role: "row" | null; + + cells: A11yCellDirective[]; + + @ViewChildren(A11yCellDirective) + private viewCells: QueryList<A11yCellDirective>; + + @ContentChildren(A11yCellDirective) + private contentCells: QueryList<A11yCellDirective>; + + ngAfterViewInit(): void { + this.cells = [...this.viewCells, ...this.contentCells]; + } +} diff --git a/libs/components/src/badge/badge.directive.ts b/libs/components/src/badge/badge.directive.ts index b81b9f80e2..acce4a18aa 100644 --- a/libs/components/src/badge/badge.directive.ts +++ b/libs/components/src/badge/badge.directive.ts @@ -1,5 +1,7 @@ import { Directive, ElementRef, HostBinding, Input } from "@angular/core"; +import { FocusableElement } from "../shared/focusable-element"; + export type BadgeVariant = "primary" | "secondary" | "success" | "danger" | "warning" | "info"; const styles: Record<BadgeVariant, string[]> = { @@ -22,8 +24,9 @@ const hoverStyles: Record<BadgeVariant, string[]> = { @Directive({ selector: "span[bitBadge], a[bitBadge], button[bitBadge]", + providers: [{ provide: FocusableElement, useExisting: BadgeDirective }], }) -export class BadgeDirective { +export class BadgeDirective implements FocusableElement { @HostBinding("class") get classList() { return [ "tw-inline-block", @@ -62,6 +65,10 @@ export class BadgeDirective { */ @Input() truncate = true; + getFocusTarget() { + return this.el.nativeElement; + } + private hasHoverEffects = false; constructor(private el: ElementRef<HTMLElement>) { diff --git a/libs/components/src/icon-button/icon-button.component.ts b/libs/components/src/icon-button/icon-button.component.ts index 53e8032795..54f6dfda96 100644 --- a/libs/components/src/icon-button/icon-button.component.ts +++ b/libs/components/src/icon-button/icon-button.component.ts @@ -1,6 +1,7 @@ -import { Component, HostBinding, Input } from "@angular/core"; +import { Component, ElementRef, HostBinding, Input } from "@angular/core"; import { ButtonLikeAbstraction, ButtonType } from "../shared/button-like.abstraction"; +import { FocusableElement } from "../shared/focusable-element"; export type IconButtonType = ButtonType | "contrast" | "main" | "muted" | "light"; @@ -123,9 +124,12 @@ const sizes: Record<IconButtonSize, string[]> = { @Component({ selector: "button[bitIconButton]:not(button[bitButton])", templateUrl: "icon-button.component.html", - providers: [{ provide: ButtonLikeAbstraction, useExisting: BitIconButtonComponent }], + providers: [ + { provide: ButtonLikeAbstraction, useExisting: BitIconButtonComponent }, + { provide: FocusableElement, useExisting: BitIconButtonComponent }, + ], }) -export class BitIconButtonComponent implements ButtonLikeAbstraction { +export class BitIconButtonComponent implements ButtonLikeAbstraction, FocusableElement { @Input("bitIconButton") icon: string; @Input() buttonType: IconButtonType; @@ -162,4 +166,10 @@ export class BitIconButtonComponent implements ButtonLikeAbstraction { setButtonType(value: "primary" | "secondary" | "danger" | "unstyled") { this.buttonType = value; } + + getFocusTarget() { + return this.elementRef.nativeElement; + } + + constructor(private elementRef: ElementRef) {} } diff --git a/libs/components/src/index.ts b/libs/components/src/index.ts index 36185911a6..1e4a3a86ff 100644 --- a/libs/components/src/index.ts +++ b/libs/components/src/index.ts @@ -16,6 +16,7 @@ export * from "./form-field"; export * from "./icon-button"; export * from "./icon"; export * from "./input"; +export * from "./item"; export * from "./layout"; export * from "./link"; export * from "./menu"; diff --git a/libs/components/src/input/autofocus.directive.ts b/libs/components/src/input/autofocus.directive.ts index f8161ee6e0..625e7fbc92 100644 --- a/libs/components/src/input/autofocus.directive.ts +++ b/libs/components/src/input/autofocus.directive.ts @@ -3,12 +3,7 @@ import { take } from "rxjs/operators"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -/** - * Interface for implementing focusable components. Used by the AutofocusDirective. - */ -export abstract class FocusableElement { - focus: () => void; -} +import { FocusableElement } from "../shared/focusable-element"; /** * Directive to focus an element. @@ -46,7 +41,7 @@ export class AutofocusDirective { private focus() { if (this.focusableElement) { - this.focusableElement.focus(); + this.focusableElement.getFocusTarget().focus(); } else { this.el.nativeElement.focus(); } diff --git a/libs/components/src/item/index.ts b/libs/components/src/item/index.ts new file mode 100644 index 0000000000..56896cdc3c --- /dev/null +++ b/libs/components/src/item/index.ts @@ -0,0 +1 @@ +export * from "./item.module"; diff --git a/libs/components/src/item/item-action.component.ts b/libs/components/src/item/item-action.component.ts new file mode 100644 index 0000000000..8cabf5c5c2 --- /dev/null +++ b/libs/components/src/item/item-action.component.ts @@ -0,0 +1,12 @@ +import { Component } from "@angular/core"; + +import { A11yCellDirective } from "../a11y/a11y-cell.directive"; + +@Component({ + selector: "bit-item-action", + standalone: true, + imports: [], + template: `<ng-content></ng-content>`, + providers: [{ provide: A11yCellDirective, useExisting: ItemActionComponent }], +}) +export class ItemActionComponent extends A11yCellDirective {} diff --git a/libs/components/src/item/item-content.component.html b/libs/components/src/item/item-content.component.html new file mode 100644 index 0000000000..d034a4a001 --- /dev/null +++ b/libs/components/src/item/item-content.component.html @@ -0,0 +1,16 @@ +<div class="tw-flex tw-gap-2 tw-items-center"> + <ng-content select="[slot=start]"></ng-content> + + <div class="tw-flex tw-flex-col tw-items-start tw-text-start tw-w-full [&_p]:tw-mb-0"> + <div class="tw-text-main tw-text-base"> + <ng-content></ng-content> + </div> + <div class="tw-text-muted tw-text-sm"> + <ng-content select="[slot=secondary]"></ng-content> + </div> + </div> +</div> + +<div class="tw-flex tw-gap-2 tw-items-center"> + <ng-content select="[slot=end]"></ng-content> +</div> diff --git a/libs/components/src/item/item-content.component.ts b/libs/components/src/item/item-content.component.ts new file mode 100644 index 0000000000..58a1198512 --- /dev/null +++ b/libs/components/src/item/item-content.component.ts @@ -0,0 +1,15 @@ +import { CommonModule } from "@angular/common"; +import { ChangeDetectionStrategy, Component } from "@angular/core"; + +@Component({ + selector: "bit-item-content, [bit-item-content]", + standalone: true, + imports: [CommonModule], + templateUrl: `item-content.component.html`, + host: { + class: + "fvw-target tw-outline-none tw-text-main hover:tw-text-main hover:tw-no-underline tw-text-base tw-py-2 tw-px-4 tw-bg-transparent tw-w-full tw-border-none tw-flex tw-gap-4 tw-items-center tw-justify-between", + }, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ItemContentComponent {} diff --git a/libs/components/src/item/item-group.component.ts b/libs/components/src/item/item-group.component.ts new file mode 100644 index 0000000000..2a9a8275cc --- /dev/null +++ b/libs/components/src/item/item-group.component.ts @@ -0,0 +1,13 @@ +import { ChangeDetectionStrategy, Component } from "@angular/core"; + +@Component({ + selector: "bit-item-group", + standalone: true, + imports: [], + template: `<ng-content></ng-content>`, + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + class: "tw-block", + }, +}) +export class ItemGroupComponent {} diff --git a/libs/components/src/item/item.component.html b/libs/components/src/item/item.component.html new file mode 100644 index 0000000000..0c91c6848e --- /dev/null +++ b/libs/components/src/item/item.component.html @@ -0,0 +1,21 @@ +<!-- TODO: Colors will be finalized in the extension refresh feature branch --> +<div + class="tw-box-border tw-overflow-auto tw-flex tw-bg-background [&:has(.item-main-content_button:hover,.item-main-content_a:hover)]:tw-bg-primary-300/20 tw-text-main tw-border-solid tw-border-b tw-border-0 tw-rounded-lg tw-mb-1.5" + [ngClass]=" + focusVisibleWithin() + ? 'tw-z-10 tw-rounded tw-outline-none tw-ring tw-ring-primary-600 tw-border-transparent' + : 'tw-border-b-secondary-300 [&:has(.item-main-content_button:hover,.item-main-content_a:hover)]:tw-border-b-transparent' + " +> + <bit-item-action class="item-main-content tw-block tw-w-full"> + <ng-content></ng-content> + </bit-item-action> + + <div + #endSlot + class="tw-p-2 tw-flex tw-gap-1 tw-items-center" + [hidden]="endSlot.childElementCount === 0" + > + <ng-content select="[slot=end]"></ng-content> + </div> +</div> diff --git a/libs/components/src/item/item.component.ts b/libs/components/src/item/item.component.ts new file mode 100644 index 0000000000..4b7b57fa9f --- /dev/null +++ b/libs/components/src/item/item.component.ts @@ -0,0 +1,29 @@ +import { CommonModule } from "@angular/common"; +import { ChangeDetectionStrategy, Component, HostListener, signal } from "@angular/core"; + +import { A11yRowDirective } from "../a11y/a11y-row.directive"; + +import { ItemActionComponent } from "./item-action.component"; + +@Component({ + selector: "bit-item", + standalone: true, + imports: [CommonModule, ItemActionComponent], + changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: "item.component.html", + providers: [{ provide: A11yRowDirective, useExisting: ItemComponent }], +}) +export class ItemComponent extends A11yRowDirective { + /** + * We have `:focus-within` and `:focus-visible` but no `:focus-visible-within` + */ + protected focusVisibleWithin = signal(false); + @HostListener("focusin", ["$event.target"]) + onFocusIn(target: HTMLElement) { + this.focusVisibleWithin.set(target.matches(".fvw-target:focus-visible")); + } + @HostListener("focusout") + onFocusOut() { + this.focusVisibleWithin.set(false); + } +} diff --git a/libs/components/src/item/item.mdx b/libs/components/src/item/item.mdx new file mode 100644 index 0000000000..8506de72bb --- /dev/null +++ b/libs/components/src/item/item.mdx @@ -0,0 +1,141 @@ +import { Meta, Story, Primary, Controls, Canvas } from "@storybook/addon-docs"; + +import * as stories from "./item.stories"; + +<Meta of={stories} /> + +```ts +import { ItemModule } from "@bitwarden/components"; +``` + +# Item + +`<bit-item>` is a horizontal card that contains one or more interactive actions. + +It is a generic container that can be used for either standalone content, an alternative to tables, +or to list nav links. + +<Canvas> + <Story of={stories.Default} /> +</Canvas> + +## Primary Content + +The primary content of an item is supplied by `bit-item-content`. + +### Content Types + +The content can be a button, anchor, or static container. + +```html +<bit-item> + <a bit-item-content routerLink="..."> Hi, I am a link. </a> +</bit-item> + +<bit-item> + <button bit-item-content (click)="...">And I am a button.</button> +</bit-item> + +<bit-item> + <bit-item-content> I'm just static :( </bit-item-content> +</bit-item> +``` + +<Canvas> + <Story of={stories.ContentTypes} /> +</Canvas> + +### Content Slots + +`bit-item-content` contains the following slots to help position the content: + +| Slot | Description | +| ------------------ | --------------------------------------------------- | +| default | primary text or arbitrary content; fan favorite | +| `slot="secondary"` | supporting text; under the default slot | +| `slot="start"` | commonly an icon or avatar; before the default slot | +| `slot="end"` | commonly an icon; after the default slot | + +- Note: There is also an `end` slot within `bit-item` itself. Place + [interactive secondary actions](#secondary-actions) there, and place non-interactive content (such + as icons) in `bit-item-content` + +```html +<bit-item> + <button bit-item-content type="button"> + <bit-avatar slot="start" text="Foo"></bit-avatar> + foo@bitwarden.com + <ng-container slot="secondary"> + <div>Bitwarden.com</div> + <div><em>locked</em></div> + </ng-container> + <i slot="end" class="bwi bwi-lock" aria-hidden="true"></i> + </button> +</bit-item> +``` + +<Canvas> + <Story of={stories.ContentSlots} /> +</Canvas> + +## Secondary Actions + +Secondary interactive actions can be placed in the item through the `"end"` slot, outside of +`bit-item-content`. + +Each action must be wrapped by `<bit-item-action>`. + +Actions are commonly icon buttons or badge buttons. + +```html +<bit-item> + <button bit-item-content>...</button> + + <ng-container slot="end"> + <bit-item-action> + <button type="button" bitBadge variant="primary">Auto-fill</button> + </bit-item-action> + <bit-item-action> + <button type="button" bitIconButton="bwi-clone" aria-label="Copy"></button> + </bit-item-action> + <bit-item-action> + <button type="button" bitIconButton="bwi-ellipsis-v" aria-label="Options"></button> + </bit-item-action> + </ng-container> +</bit-item> +``` + +## Item Groups + +Groups of items can be associated by wrapping them in the `<bit-item-group>`. + +<Canvas> + <Story of={stories.MultipleActionList} /> +</Canvas> + +<Canvas> + <Story of={stories.SingleActionList} /> +</Canvas> + +### A11y + +Keyboard nav is currently disabled due to a bug when used within a virtual scroll viewport. + +Item groups utilize arrow-based keyboard navigation +([further reading here](https://www.w3.org/WAI/ARIA/apg/patterns/grid/examples/layout-grids/#kbd_label)). + +Use `aria-label` or `aria-labelledby` to give groups an accessible name. + +```html +<bit-item-group aria-label="My Items"> + <bit-item>...</bit-item> + <bit-item>...</bit-item> + <bit-item>...</bit-item> +</bit-item-group> +``` + +### Virtual Scrolling + +<Canvas> + <Story of={stories.VirtualScrolling} /> +</Canvas> diff --git a/libs/components/src/item/item.module.ts b/libs/components/src/item/item.module.ts new file mode 100644 index 0000000000..226fed11d8 --- /dev/null +++ b/libs/components/src/item/item.module.ts @@ -0,0 +1,12 @@ +import { NgModule } from "@angular/core"; + +import { ItemActionComponent } from "./item-action.component"; +import { ItemContentComponent } from "./item-content.component"; +import { ItemGroupComponent } from "./item-group.component"; +import { ItemComponent } from "./item.component"; + +@NgModule({ + imports: [ItemComponent, ItemContentComponent, ItemActionComponent, ItemGroupComponent], + exports: [ItemComponent, ItemContentComponent, ItemActionComponent, ItemGroupComponent], +}) +export class ItemModule {} diff --git a/libs/components/src/item/item.stories.ts b/libs/components/src/item/item.stories.ts new file mode 100644 index 0000000000..b9d8d6cc2e --- /dev/null +++ b/libs/components/src/item/item.stories.ts @@ -0,0 +1,326 @@ +import { ScrollingModule } from "@angular/cdk/scrolling"; +import { CommonModule } from "@angular/common"; +import { Meta, StoryObj, componentWrapperDecorator, moduleMetadata } from "@storybook/angular"; + +import { A11yGridDirective } from "../a11y/a11y-grid.directive"; +import { AvatarModule } from "../avatar"; +import { BadgeModule } from "../badge"; +import { IconButtonModule } from "../icon-button"; +import { TypographyModule } from "../typography"; + +import { ItemActionComponent } from "./item-action.component"; +import { ItemContentComponent } from "./item-content.component"; +import { ItemGroupComponent } from "./item-group.component"; +import { ItemComponent } from "./item.component"; + +export default { + title: "Component Library/Item", + component: ItemComponent, + decorators: [ + moduleMetadata({ + imports: [ + CommonModule, + ItemGroupComponent, + AvatarModule, + IconButtonModule, + BadgeModule, + TypographyModule, + ItemActionComponent, + ItemContentComponent, + A11yGridDirective, + ScrollingModule, + ], + }), + componentWrapperDecorator((story) => `<div class="tw-bg-background-alt tw-p-2">${story}</div>`), + ], +} as Meta; + +type Story = StoryObj<ItemGroupComponent>; + +export const Default: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` + <bit-item> + <button bit-item-content> + <i slot="start" class="bwi bwi-globe tw-text-3xl tw-text-muted" aria-hidden="true"></i> + Foo + <span slot="secondary">Bar</span> + </button> + + <ng-container slot="end"> + <bit-item-action> + <button type="button" bitBadge variant="primary">Auto-fill</button> + </bit-item-action> + <bit-item-action> + <button type="button" bitIconButton="bwi-clone"></button> + </bit-item-action> + <bit-item-action> + <button type="button" bitIconButton="bwi-ellipsis-v"></button> + </bit-item-action> + </ng-container> + </bit-item> + `, + }), +}; + +export const ContentSlots: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` + <bit-item> + <button bit-item-content type="button"> + <bit-avatar + slot="start" + [text]="'Foo'" + ></bit-avatar> + foo@bitwarden.com + <ng-container slot="secondary"> + <div>Bitwarden.com</div> + <div><em>locked</em></div> + </ng-container> + <i slot="end" class="bwi bwi-lock" aria-hidden="true"></i> + </button> + </bit-item> + `, + }), +}; + +export const ContentTypes: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` + <bit-item> + <a bit-item-content href="#"> + Hi, I am a link. + </a> + </bit-item> + <bit-item> + <button bit-item-content href="#"> + And I am a button. + </button> + </bit-item> + <bit-item> + <bit-item-content> + I'm just static :( + </bit-item-content> + </bit-item> + `, + }), +}; + +export const TextOverflow: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` + <div class="tw-text-main tw-mb-4">TODO: Fix truncation</div> + <bit-item> + <bit-item-content> + Helloooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo + </bit-item-content> + </bit-item> + `, + }), +}; + +export const MultipleActionList: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` + <bit-item-group aria-label="Multiple Action List"> + <bit-item> + <button bit-item-content> + <i slot="start" class="bwi bwi-globe tw-text-3xl tw-text-muted" aria-hidden="true"></i> + Foo + <span slot="secondary">Bar</span> + </button> + + <ng-container slot="end"> + <bit-item-action> + <button type="button" bitBadge variant="primary">Auto-fill</button> + </bit-item-action> + <bit-item-action> + <button type="button" bitIconButton="bwi-clone"></button> + </bit-item-action> + <bit-item-action> + <button type="button" bitIconButton="bwi-ellipsis-v"></button> + </bit-item-action> + </ng-container> + </bit-item> + <bit-item> + <button bit-item-content> + <i slot="start" class="bwi bwi-globe tw-text-3xl tw-text-muted" aria-hidden="true"></i> + Foo + <span slot="secondary">Bar</span> + </button> + + <ng-container slot="end"> + <bit-item-action> + <button type="button" bitBadge variant="primary">Auto-fill</button> + </bit-item-action> + <bit-item-action> + <button type="button" bitIconButton="bwi-clone"></button> + </bit-item-action> + <bit-item-action> + <button type="button" bitIconButton="bwi-ellipsis-v"></button> + </bit-item-action> + </ng-container> + </bit-item> + <bit-item> + <button bit-item-content> + <i slot="start" class="bwi bwi-globe tw-text-3xl tw-text-muted" aria-hidden="true"></i> + Foo + <span slot="secondary">Bar</span> + </button> + + <ng-container slot="end"> + <bit-item-action> + <button type="button" bitBadge variant="primary">Auto-fill</button> + </bit-item-action> + <bit-item-action> + <button type="button" bitIconButton="bwi-clone"></button> + </bit-item-action> + <bit-item-action> + <button type="button" bitIconButton="bwi-ellipsis-v"></button> + </bit-item-action> + </ng-container> + </bit-item> + <bit-item> + <button bit-item-content> + <i slot="start" class="bwi bwi-globe tw-text-3xl tw-text-muted" aria-hidden="true"></i> + Foo + <span slot="secondary">Bar</span> + </button> + + <ng-container slot="end"> + <bit-item-action> + <button type="button" bitBadge variant="primary">Auto-fill</button> + </bit-item-action> + <bit-item-action> + <button type="button" bitIconButton="bwi-clone"></button> + </bit-item-action> + <bit-item-action> + <button type="button" bitIconButton="bwi-ellipsis-v"></button> + </bit-item-action> + </ng-container> + </bit-item> + <bit-item> + <button bit-item-content> + <i slot="start" class="bwi bwi-globe tw-text-3xl tw-text-muted" aria-hidden="true"></i> + Foo + <span slot="secondary">Bar</span> + </button> + + <ng-container slot="end"> + <bit-item-action> + <button type="button" bitBadge variant="primary">Auto-fill</button> + </bit-item-action> + <bit-item-action> + <button type="button" bitIconButton="bwi-clone"></button> + </bit-item-action> + <bit-item-action> + <button type="button" bitIconButton="bwi-ellipsis-v"></button> + </bit-item-action> + </ng-container> + </bit-item> + <bit-item> + <button bit-item-content> + <i slot="start" class="bwi bwi-globe tw-text-3xl tw-text-muted" aria-hidden="true"></i> + Foo + <span slot="secondary">Bar</span> + </button> + + <ng-container slot="end"> + <bit-item-action> + <button type="button" bitBadge variant="primary">Auto-fill</button> + </bit-item-action> + <bit-item-action> + <button type="button" bitIconButton="bwi-clone"></button> + </bit-item-action> + <bit-item-action> + <button type="button" bitIconButton="bwi-ellipsis-v"></button> + </bit-item-action> + </ng-container> + </bit-item> + </bit-item-group> + `, + }), +}; + +export const SingleActionList: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` + <bit-item-group aria-label="Single Action List"> + <bit-item> + <a bit-item-content href="#"> + Foobar + <i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i> + </a> + </bit-item> + <bit-item> + <a bit-item-content href="#"> + Foobar + <i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i> + </a> + </bit-item> + <bit-item> + <a bit-item-content href="#"> + Foobar + <i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i> + </a> + </bit-item> + <bit-item> + <a bit-item-content href="#"> + Foobar + <i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i> + </a> + </bit-item> + <bit-item> + <a bit-item-content href="#"> + Foobar + <i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i> + </a> + </bit-item> + <bit-item> + <a bit-item-content href="#"> + Foobar + <i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i> + </a> + </bit-item> + </bit-item-group> + `, + }), +}; + +export const VirtualScrolling: Story = { + render: (_args) => ({ + props: { + data: Array.from(Array(100000).keys()), + }, + template: /*html*/ ` + <cdk-virtual-scroll-viewport [itemSize]="46" class="tw-h-[500px]"> + <bit-item-group aria-label="Single Action List"> + <bit-item *cdkVirtualFor="let item of data"> + <button bit-item-content> + <i slot="start" class="bwi bwi-globe tw-text-3xl tw-text-muted" aria-hidden="true"></i> + {{ item }} + </button> + + <ng-container slot="end"> + <bit-item-action> + <button type="button" bitBadge variant="primary">Auto-fill</button> + </bit-item-action> + <bit-item-action> + <button type="button" bitIconButton="bwi-clone"></button> + </bit-item-action> + <bit-item-action> + <button type="button" bitIconButton="bwi-ellipsis-v"></button> + </bit-item-action> + </ng-container> + </bit-item> + </bit-item-group> + </cdk-virtual-scroll-viewport> + `, + }), +}; diff --git a/libs/components/src/search/search.component.ts b/libs/components/src/search/search.component.ts index a0f3eb363f..27170d5d7b 100644 --- a/libs/components/src/search/search.component.ts +++ b/libs/components/src/search/search.component.ts @@ -1,7 +1,7 @@ import { Component, ElementRef, Input, ViewChild } from "@angular/core"; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms"; -import { FocusableElement } from "../input/autofocus.directive"; +import { FocusableElement } from "../shared/focusable-element"; let nextId = 0; @@ -32,8 +32,8 @@ export class SearchComponent implements ControlValueAccessor, FocusableElement { @Input() disabled: boolean; @Input() placeholder: string; - focus() { - this.input.nativeElement.focus(); + getFocusTarget() { + return this.input.nativeElement; } onChange(searchText: string) { diff --git a/libs/components/src/shared/focusable-element.ts b/libs/components/src/shared/focusable-element.ts new file mode 100644 index 0000000000..1ea422aa6f --- /dev/null +++ b/libs/components/src/shared/focusable-element.ts @@ -0,0 +1,8 @@ +/** + * Interface for implementing focusable components. + * + * Used by the `AutofocusDirective` and `A11yGridDirective`. + */ +export abstract class FocusableElement { + getFocusTarget: () => HTMLElement; +} diff --git a/libs/components/src/styles.scss b/libs/components/src/styles.scss index ae97838e09..7ddcb1b64b 100644 --- a/libs/components/src/styles.scss +++ b/libs/components/src/styles.scss @@ -49,6 +49,6 @@ $card-icons-base: "../../src/billing/images/cards/"; @import "multi-select/scss/bw.theme.scss"; // Workaround for https://bitwarden.atlassian.net/browse/CL-110 -#storybook-docs pre.prismjs { +.sbdocs-preview pre.prismjs { color: white; } From 418d4642da81e09de6a4b445042a26592a017c98 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Tue, 30 Apr 2024 10:55:00 -0400 Subject: [PATCH 323/351] Hide grace period note when in self-serve trial (#8768) --- ...organization-subscription-selfhost.component.html | 5 ++++- .../self-hosted-organization-subscription.view.ts | 12 ++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.html b/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.html index 6d6691f336..b4c1224db9 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.html +++ b/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.html @@ -42,7 +42,10 @@ : subscription.expirationWithGracePeriod ) | date: "mediumDate" }} - <div *ngIf="subscription.hasSeparateGracePeriod" class="tw-text-muted"> + <div + *ngIf="subscription.hasSeparateGracePeriod && !subscription.isInTrial" + class="tw-text-muted" + > {{ "selfHostGracePeriodHelp" | i18n: (subscription.expirationWithGracePeriod | date: "mediumDate") diff --git a/libs/common/src/billing/models/view/self-hosted-organization-subscription.view.ts b/libs/common/src/billing/models/view/self-hosted-organization-subscription.view.ts index c1f5640207..7b49688294 100644 --- a/libs/common/src/billing/models/view/self-hosted-organization-subscription.view.ts +++ b/libs/common/src/billing/models/view/self-hosted-organization-subscription.view.ts @@ -58,4 +58,16 @@ export class SelfHostedOrganizationSubscriptionView implements View { get isExpiredAndOutsideGracePeriod() { return this.hasExpiration && this.expirationWithGracePeriod < new Date(); } + + /** + * In the case of a trial, where there is no grace period, the expirationWithGracePeriod and expirationWithoutGracePeriod will + * be exactly the same. This can be used to hide the grace period note. + */ + get isInTrial() { + return ( + this.expirationWithGracePeriod && + this.expirationWithoutGracePeriod && + this.expirationWithGracePeriod.getTime() === this.expirationWithoutGracePeriod.getTime() + ); + } } From 04decd1c09a69bde07c389c481264badba3355e9 Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Tue, 30 Apr 2024 16:35:39 +0100 Subject: [PATCH 324/351] [AC-2265] As a Provider Admin, I shouldn't be able to use my client organizations' billing pages (#8981) * initial commit * add the feature flag * Resolve pr comments --- .../icons/manage-billing.icon.ts | 25 +++++++++++++++++++ ...nization-subscription-cloud.component.html | 12 ++++++++- ...ganization-subscription-cloud.component.ts | 16 +++++++++++- apps/web/src/locales/en/messages.json | 3 +++ 4 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/app/billing/organizations/icons/manage-billing.icon.ts diff --git a/apps/web/src/app/billing/organizations/icons/manage-billing.icon.ts b/apps/web/src/app/billing/organizations/icons/manage-billing.icon.ts new file mode 100644 index 0000000000..6f583bf2e8 --- /dev/null +++ b/apps/web/src/app/billing/organizations/icons/manage-billing.icon.ts @@ -0,0 +1,25 @@ +import { svgIcon } from "@bitwarden/components"; + +export const ManageBilling = svgIcon` +<svg width="213" height="231" viewBox="0 0 213 231" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M130.089 85.6617C129.868 85.4299 129.604 85.2456 129.31 85.1197C129.016 84.9937 128.7 84.9299 128.381 84.9317H84.5811C84.2617 84.9299 83.9441 84.9937 83.6503 85.1197C83.3565 85.2456 83.0919 85.4299 82.8729 85.6617C82.6411 85.8807 82.4568 86.1471 82.3308 86.441C82.2049 86.7348 82.141 87.0505 82.1429 87.3699V116.57C82.152 118.793 82.5827 120.994 83.4131 123.056C84.2033 125.091 85.2654 127.011 86.5703 128.761C87.9117 130.515 89.4137 132.137 91.0562 133.612C92.58 135.01 94.186 136.318 95.8632 137.528C97.3232 138.565 98.8562 139.547 100.462 140.474C102.068 141.401 103.202 142.027 103.864 142.353C104.532 142.682 105.072 142.941 105.474 143.113C105.788 143.264 106.132 143.339 106.481 143.332C106.824 143.337 107.164 143.257 107.47 143.102C107.879 142.923 108.412 142.671 109.087 142.343C109.762 142.014 110.912 141.386 112.489 140.463C114.066 139.539 115.617 138.554 117.088 137.517C118.767 136.305 120.375 134.999 121.902 133.601C123.547 132.128 125.049 130.504 126.388 128.75C127.691 126.998 128.753 125.08 129.545 123.045C130.378 120.983 130.808 118.782 130.816 116.559V87.3589C130.817 87.0414 130.754 86.7275 130.628 86.4355C130.502 86.1435 130.319 85.8807 130.089 85.6617ZM124.443 116.836C124.443 127.421 106.481 136.513 106.481 136.513V91.1878H124.443V116.836Z" fill="#212529"/> +<path d="M62.7328 163.392C62.7328 168.149 51.6616 166.263 46.761 166.263C41.8605 166.263 22.5074 161.096 20.7328 153.058C23.6946 151.005 16.0004 143.298 31.9722 142.724C33.1529 141.759 44.9083 148.712 46.761 149.039C51.6616 149.039 62.7328 158.636 62.7328 163.392Z" fill="#E5E5E5"/> +<path d="M21.3544 122.3C21.4472 123.4 22.4147 124.217 23.5153 124.125C24.616 124.032 25.433 123.064 25.3402 121.964L21.3544 122.3ZM148.234 45.7444C149.303 45.4678 149.946 44.3767 149.669 43.3073L145.162 25.8808C144.885 24.8114 143.794 24.1687 142.725 24.4453C141.655 24.7219 141.013 25.813 141.289 26.8824L145.296 42.3726L129.805 46.3792C128.736 46.6558 128.093 47.7469 128.37 48.8163C128.647 49.8857 129.738 50.5283 130.807 50.2517L148.234 45.7444ZM25.3402 121.964C23.4116 99.0873 31.1986 75.5542 48.6989 58.0539L45.8705 55.2255C27.5023 73.5937 19.331 98.2998 21.3544 122.3L25.3402 121.964ZM48.6989 58.0539C75.2732 31.4796 115.769 27.3025 146.718 45.5314L148.748 42.0848C116.267 22.9532 73.7654 27.3305 45.8705 55.2255L48.6989 58.0539Z" fill="#212529"/> +<path d="M64.2075 185.062C63.1417 185.352 62.5129 186.451 62.8029 187.517L67.5298 204.885C67.8199 205.951 68.919 206.58 69.9848 206.29C71.0507 205.999 71.6795 204.9 71.3895 203.834L67.1878 188.396L82.6262 184.194C83.692 183.904 84.3209 182.805 84.0308 181.739C83.7408 180.674 82.6416 180.045 81.5758 180.335L64.2075 185.062ZM189.211 100.283C189.018 99.1952 187.98 98.4697 186.893 98.6625C185.805 98.8552 185.08 99.8931 185.272 100.981L189.211 100.283ZM162.871 172.225C136.546 198.55 96.5599 202.897 65.726 185.255L63.7396 188.727C96.0997 207.242 138.066 202.687 165.699 175.054L162.871 172.225ZM185.272 100.981C189.718 126.07 182.249 152.847 162.871 172.225L165.699 175.054C186.04 154.713 193.875 126.603 189.211 100.283L185.272 100.981Z" fill="#212529"/> +<path d="M34.4588 108.132C36.0159 92.1931 42.8984 76.6765 55.1062 64.4686C72.0222 47.5527 95.2911 40.8618 117.233 44.396" stroke="#212529" stroke-width="2" stroke-linecap="round"/> +<path d="M177.328 119.132C176.386 136.119 169.426 152.834 156.449 165.811C141.173 181.088 120.715 188.025 100.733 186.623" stroke="#212529" stroke-width="2" stroke-linecap="round"/> +<rect x="150.233" y="56.1318" width="49" height="34" rx="2.5" stroke="#212529" stroke-width="3"/> +<path d="M150.233 63.6318V63.6318C150.233 66.9455 152.919 69.6318 156.233 69.6318H169.242M199.233 63.6318V63.6318C199.233 66.9455 196.546 69.6318 193.233 69.6318H180.224" stroke="#212529" stroke-width="3"/> +<mask id="path-9-inside-1_873_6447" fill="white"> +<rect x="168.733" y="65.6318" width="12" height="9" rx="1.25"/> +</mask> +<rect x="168.733" y="65.6318" width="12" height="9" rx="1.25" stroke="#212529" stroke-width="6" mask="url(#path-9-inside-1_873_6447)"/> +<path d="M183.733 54.6318C183.733 54.6318 183.733 53.6318 183.733 52.6318C183.733 51.6318 182.785 50.6318 181.838 50.6318C180.891 50.6318 168.575 50.6318 167.628 50.6318C166.68 50.6318 165.733 51.6318 165.733 52.6318C165.733 53.6318 165.733 54.6318 165.733 54.6318" stroke="#212529" stroke-width="3"/> +<circle cx="48.7328" cy="142.632" r="10.5" fill="white" stroke="#212529" stroke-width="3"/> +<path d="M65.7263 170.132H65.6454H65.5646H65.484H65.4036H65.3233H65.2432H65.1632H65.0834H65.0037H64.9242H64.8449H64.7657H64.6866H64.6077H64.529H64.4504H64.372H64.2937H64.2155H64.1376H64.0597H63.982H63.9045H63.8271H63.7498H63.6727H63.5957H63.5189H63.4422H63.3657H63.2893H63.213H63.1369H63.0609H62.985H62.9093H62.8338H62.7583H62.683H62.6079H62.5329H62.458H62.3832H62.3086H62.2341H62.1597H62.0855H62.0114H61.9374H61.8636H61.7899H61.7163H61.6428H61.5695H61.4963H61.4232H61.3503H61.2774H61.2047H61.1321H61.0597H60.9873H60.9151H60.843H60.771H60.6992H60.6274H60.5558H60.4843H60.4129H60.3416H60.2704H60.1994H60.1284H60.0576H59.9869H59.9163H59.8458H59.7754H59.7052H59.635H59.5649H59.495H59.4252H59.3554H59.2858H59.2163H59.1469H59.0776H59.0084H58.9393H58.8703H58.8013H58.7325H58.6638H58.5952H58.5267H58.4583H58.39H58.3218H58.2537H58.1857H58.1178H58.0499H57.9822H57.9146H57.847H57.7796H57.7122H57.6449H57.5777H57.5106H57.4436H57.3767H57.3099H57.2431H57.1765H57.1099H57.0434H56.977H56.9107H56.8444H56.7783H56.7122H56.6462H56.5803H56.5145H56.4487H56.383H56.3174H56.2519H56.1865H56.1211H56.0558H55.9906H55.9254H55.8603H55.7953H55.7304H55.6655H55.6008H55.536H55.4714H55.4068H55.3423H55.2778H55.2135H55.1492H55.0849H55.0207H54.9566H54.8925H54.8286H54.7646H54.7008H54.6369H54.5732H54.5095H54.4459H54.3823H54.3188H54.2553H54.1919H54.1286H54.0653H54.0021H53.9389H53.8758H53.8127H53.7497H53.6867H53.6238H53.5609H53.4981H53.4353H53.3726H53.3099H53.2473H53.1847H53.1222H53.0597H52.9972H52.9348H52.8725H52.8102H52.7479H52.6856H52.6234H52.5613H52.4992H52.4371H52.375H52.313H52.2511H52.1891H52.1272H52.0654H52.0036H51.9418H51.88H51.8183H51.7566H51.6949H51.6333H51.5717H51.5101H51.4485H51.387H51.3255H51.264H51.2026H51.1412H51.0798H51.0184H50.9571H50.8957H50.8344H50.7731H50.7119H50.6506H50.5894H50.5282H50.467H50.4058H50.3447H50.2836H50.2224H50.1613H50.1002H50.0392H49.9781H49.917H49.856H49.795H49.7339H49.6729H49.6119H49.5509H49.4899H49.429H49.368H49.307H49.246H49.1851H49.1241H49.0632H49.0022H48.9413H48.8803H48.8194H48.7584H48.6975H48.6365H48.5756H48.5146H48.4537H48.3927H48.3318H48.2708H48.2098H48.1488H48.0878H48.0268H47.9658H47.9048H47.8438H47.7828H47.7217H47.6607H47.5996H47.5385H47.4774H47.4163H47.3552H47.294H47.2329H47.1717H47.1105H47.0493H46.9881H46.9268H46.8656H46.8043H46.743H46.6816H46.6203H46.5589H46.4975H46.4361H46.3746H46.3132H46.2517H46.1901H46.1286H46.067H46.0054H45.9437H45.8821H45.8203H45.7586H45.6968H45.635H45.5732H45.5113H45.4494H45.3875H45.3255H45.2635H45.2015H45.1394H45.0772H45.0151H44.9529H44.8906H44.8283H44.766H44.7036H44.6412H44.5788H44.5163H44.4537H44.3911H44.3285H44.2658H44.2031H44.1403H44.0775H44.0146H43.9517H43.8887H43.8256H43.7626H43.6994H43.6362H43.573H43.5097H43.4463H43.3829H43.3195H43.2559H43.1924H43.1287H43.065H43.0013H42.9374H42.8736H42.8096H42.7456H42.6815H42.6174H42.5532H42.4889H42.4246H42.3602H42.2958H42.2312H42.1666H42.102H42.0373H41.9724H41.9076H41.8426H41.7776H41.7125H41.6474H41.5821H41.5168H41.4514H41.386H41.3204H41.2548H41.1891H41.1233H41.0575H40.9916H40.9255H40.8594H40.7933H40.727H40.6607H40.5943H40.5277H40.4612H40.3945H40.3277H40.2609H40.1939H40.1269H40.0598H39.9926H39.9253H39.8579H39.7904H39.7229H39.6552H39.5874H39.5196H39.4517H39.3836H39.3155H39.2473H39.1789H39.1105H39.042H38.9734H38.9046H38.8358H38.7669H38.6979H38.6288H38.5595H38.4902H38.4208H38.3512H38.2816H38.2118H38.142H38.072H38.0019H37.9317H37.8615H37.7911H37.7205H37.6499H37.5792H37.5083H37.4374H37.3663H37.2951H37.2238H37.1524H37.0809H37.0092H36.9374H36.8655H36.7935H36.7214H36.6492H36.5768H36.5043H36.4317H36.359H36.2861H36.2131H36.14H36.0668H35.9934H35.9199H35.8463H35.7726H35.6987H35.6247H35.5506H35.4764H35.402H35.3274H35.2528H35.178H35.1031H35.028H34.9528H34.8775H34.8021H34.7265H34.6507H34.5749H34.4989H34.4227H34.3464H34.27H34.1934H34.1167H34.0398H33.9628H33.8857H33.8084H33.731H33.6534H33.5757H33.4978H33.4198H33.3416H33.2633H33.1848H33.1062H33.0274H32.9485H32.8694H32.7902H32.7108H32.6313H32.5516H32.4718H32.3918H32.3116H32.2313H32.1508H32.0702H31.9894H31.9085H31.8273H31.7461H31.6646H31.583H31.5013H31.4194H31.3373H31.255H31.1726H31.09H31.0073H30.9243C30.7817 170.132 30.7021 170.098 30.6492 170.065C30.5881 170.026 30.5107 169.954 30.4348 169.823C30.2689 169.538 30.1936 169.112 30.2525 168.743C31.6563 159.954 39.3802 153.206 48.7252 153.206C58.0703 153.206 65.7943 159.954 67.198 168.743C67.3079 169.431 67.1364 169.686 67.0452 169.781C66.9216 169.91 66.5692 170.132 65.7263 170.132Z" fill="white" stroke="#212529" stroke-width="3"/> +<circle cx="20.7328" cy="142.632" r="10.5" fill="white" stroke="#212529" stroke-width="3"/> +<path d="M37.7263 170.132H37.6454H37.5646H37.484H37.4036H37.3233H37.2432H37.1632H37.0834H37.0037H36.9242H36.8449H36.7657H36.6866H36.6077H36.529H36.4504H36.372H36.2937H36.2155H36.1376H36.0597H35.982H35.9045H35.8271H35.7498H35.6727H35.5957H35.5189H35.4422H35.3657H35.2893H35.213H35.1369H35.0609H34.985H34.9093H34.8338H34.7583H34.683H34.6079H34.5329H34.458H34.3832H34.3086H34.2341H34.1597H34.0855H34.0114H33.9374H33.8636H33.7899H33.7163H33.6428H33.5695H33.4963H33.4232H33.3503H33.2774H33.2047H33.1321H33.0597H32.9873H32.9151H32.843H32.771H32.6992H32.6274H32.5558H32.4843H32.4129H32.3416H32.2704H32.1994H32.1284H32.0576H31.9869H31.9163H31.8458H31.7754H31.7052H31.635H31.5649H31.495H31.4252H31.3554H31.2858H31.2163H31.1469H31.0776H31.0084H30.9393H30.8703H30.8013H30.7325H30.6638H30.5952H30.5267H30.4583H30.39H30.3218H30.2537H30.1857H30.1178H30.0499H29.9822H29.9146H29.847H29.7796H29.7122H29.6449H29.5777H29.5106H29.4436H29.3767H29.3099H29.2431H29.1765H29.1099H29.0434H28.977H28.9107H28.8444H28.7783H28.7122H28.6462H28.5803H28.5145H28.4487H28.383H28.3174H28.2519H28.1865H28.1211H28.0558H27.9906H27.9254H27.8603H27.7953H27.7304H27.6655H27.6008H27.536H27.4714H27.4068H27.3423H27.2778H27.2135H27.1492H27.0849H27.0207H26.9566H26.8925H26.8286H26.7646H26.7008H26.6369H26.5732H26.5095H26.4459H26.3823H26.3188H26.2553H26.1919H26.1286H26.0653H26.0021H25.9389H25.8758H25.8127H25.7497H25.6867H25.6238H25.5609H25.4981H25.4353H25.3726H25.3099H25.2473H25.1847H25.1222H25.0597H24.9972H24.9348H24.8725H24.8102H24.7479H24.6856H24.6234H24.5613H24.4992H24.4371H24.375H24.313H24.2511H24.1891H24.1272H24.0654H24.0036H23.9418H23.88H23.8183H23.7566H23.6949H23.6333H23.5717H23.5101H23.4485H23.387H23.3255H23.264H23.2026H23.1412H23.0798H23.0184H22.9571H22.8957H22.8344H22.7731H22.7119H22.6506H22.5894H22.5282H22.467H22.4058H22.3447H22.2836H22.2224H22.1613H22.1002H22.0392H21.9781H21.917H21.856H21.795H21.7339H21.6729H21.6119H21.5509H21.4899H21.429H21.368H21.307H21.246H21.1851H21.1241H21.0632H21.0022H20.9413H20.8803H20.8194H20.7584H20.6975H20.6365H20.5756H20.5146H20.4537H20.3927H20.3318H20.2708H20.2098H20.1488H20.0878H20.0268H19.9658H19.9048H19.8438H19.7828H19.7217H19.6607H19.5996H19.5385H19.4774H19.4163H19.3552H19.294H19.2329H19.1717H19.1105H19.0493H18.9881H18.9268H18.8656H18.8043H18.743H18.6816H18.6203H18.5589H18.4975H18.4361H18.3746H18.3132H18.2517H18.1901H18.1286H18.067H18.0054H17.9437H17.8821H17.8203H17.7586H17.6968H17.635H17.5732H17.5113H17.4494H17.3875H17.3255H17.2635H17.2015H17.1394H17.0772H17.0151H16.9529H16.8906H16.8283H16.766H16.7036H16.6412H16.5788H16.5163H16.4537H16.3911H16.3285H16.2658H16.2031H16.1403H16.0775H16.0146H15.9517H15.8887H15.8256H15.7626H15.6994H15.6362H15.573H15.5097H15.4463H15.3829H15.3195H15.2559H15.1924H15.1287H15.065H15.0013H14.9374H14.8736H14.8096H14.7456H14.6815H14.6174H14.5532H14.4889H14.4246H14.3602H14.2958H14.2312H14.1666H14.102H14.0373H13.9724H13.9076H13.8426H13.7776H13.7125H13.6474H13.5821H13.5168H13.4514H13.386H13.3204H13.2548H13.1891H13.1233H13.0575H12.9916H12.9255H12.8594H12.7933H12.727H12.6607H12.5943H12.5277H12.4612H12.3945H12.3277H12.2609H12.1939H12.1269H12.0598H11.9926H11.9253H11.8579H11.7904H11.7229H11.6552H11.5874H11.5196H11.4517H11.3836H11.3155H11.2473H11.1789H11.1105H11.042H10.9734H10.9046H10.8358H10.7669H10.6979H10.6288H10.5595H10.4902H10.4208H10.3512H10.2816H10.2118H10.142H10.072H10.0019H9.93175H9.86145H9.79105H9.72054H9.64992H9.57918H9.50834H9.43738H9.3663H9.29511H9.22381H9.15239H9.08085H9.0092H8.93743H8.86554H8.79354H8.72141H8.64916H8.5768H8.50431H8.4317H8.35896H8.28611H8.21312H8.14002H8.06679H7.99343H7.91995H7.84634H7.7726H7.69873H7.62473H7.55061H7.47635H7.40196H7.32744H7.25279H7.17801H7.10309H7.02804H6.95285H6.87753H6.80207H6.72647H6.65074H6.57487H6.49886H6.42271H6.34642H6.26998H6.19341H6.1167H6.03984H5.96284H5.8857H5.80841H5.73098H5.6534H5.57567H5.4978H5.41978H5.34161H5.26329H5.18482H5.1062H5.02743H4.94851H4.86944H4.79021H4.71083H4.6313H4.55161H4.47177H4.39177H4.31161H4.2313H4.15082H4.07019H3.9894H3.90845H3.82734H3.74607H3.66464H3.58304H3.50128H3.41936H3.33727H3.25501H3.1726H3.09001H3.00726H2.92434C2.78171 170.132 2.70206 170.098 2.64924 170.065C2.5881 170.026 2.51071 169.954 2.43479 169.823C2.26892 169.538 2.19357 169.112 2.25253 168.743C3.65626 159.954 11.3802 153.206 20.7252 153.206C30.0703 153.206 37.7943 159.954 39.198 168.743C39.3079 169.431 39.1364 169.686 39.0452 169.781C38.9216 169.91 38.5692 170.132 37.7263 170.132Z" fill="white" stroke="#212529" stroke-width="3"/> +<circle cx="34.7328" cy="155.632" r="10.5" fill="white" stroke="#212529" stroke-width="3"/> +<path d="M51.7263 183.132H51.6454H51.5646H51.484H51.4036H51.3233H51.2432H51.1632H51.0834H51.0037H50.9242H50.8449H50.7657H50.6866H50.6077H50.529H50.4504H50.372H50.2937H50.2155H50.1376H50.0597H49.982H49.9045H49.8271H49.7498H49.6727H49.5957H49.5189H49.4422H49.3657H49.2893H49.213H49.1369H49.0609H48.985H48.9093H48.8338H48.7583H48.683H48.6079H48.5329H48.458H48.3832H48.3086H48.2341H48.1597H48.0855H48.0114H47.9374H47.8636H47.7899H47.7163H47.6428H47.5695H47.4963H47.4232H47.3503H47.2774H47.2047H47.1321H47.0597H46.9873H46.9151H46.843H46.771H46.6992H46.6274H46.5558H46.4843H46.4129H46.3416H46.2704H46.1994H46.1284H46.0576H45.9869H45.9163H45.8458H45.7754H45.7052H45.635H45.5649H45.495H45.4252H45.3554H45.2858H45.2163H45.1469H45.0776H45.0084H44.9393H44.8703H44.8013H44.7325H44.6638H44.5952H44.5267H44.4583H44.39H44.3218H44.2537H44.1857H44.1178H44.0499H43.9822H43.9146H43.847H43.7796H43.7122H43.6449H43.5777H43.5106H43.4436H43.3767H43.3099H43.2431H43.1765H43.1099H43.0434H42.977H42.9107H42.8444H42.7783H42.7122H42.6462H42.5803H42.5145H42.4487H42.383H42.3174H42.2519H42.1865H42.1211H42.0558H41.9906H41.9254H41.8603H41.7953H41.7304H41.6655H41.6008H41.536H41.4714H41.4068H41.3423H41.2778H41.2135H41.1492H41.0849H41.0207H40.9566H40.8925H40.8286H40.7646H40.7008H40.6369H40.5732H40.5095H40.4459H40.3823H40.3188H40.2553H40.1919H40.1286H40.0653H40.0021H39.9389H39.8758H39.8127H39.7497H39.6867H39.6238H39.5609H39.4981H39.4353H39.3726H39.3099H39.2473H39.1847H39.1222H39.0597H38.9972H38.9348H38.8725H38.8102H38.7479H38.6856H38.6234H38.5613H38.4992H38.4371H38.375H38.313H38.2511H38.1891H38.1272H38.0654H38.0036H37.9418H37.88H37.8183H37.7566H37.6949H37.6333H37.5717H37.5101H37.4485H37.387H37.3255H37.264H37.2026H37.1412H37.0798H37.0184H36.9571H36.8957H36.8344H36.7731H36.7119H36.6506H36.5894H36.5282H36.467H36.4058H36.3447H36.2836H36.2224H36.1613H36.1002H36.0392H35.9781H35.917H35.856H35.795H35.7339H35.6729H35.6119H35.5509H35.4899H35.429H35.368H35.307H35.246H35.1851H35.1241H35.0632H35.0022H34.9413H34.8803H34.8194H34.7584H34.6975H34.6365H34.5756H34.5146H34.4537H34.3927H34.3318H34.2708H34.2098H34.1488H34.0878H34.0268H33.9658H33.9048H33.8438H33.7828H33.7217H33.6607H33.5996H33.5385H33.4774H33.4163H33.3552H33.294H33.2329H33.1717H33.1105H33.0493H32.9881H32.9268H32.8656H32.8043H32.743H32.6816H32.6203H32.5589H32.4975H32.4361H32.3746H32.3132H32.2517H32.1901H32.1286H32.067H32.0054H31.9437H31.8821H31.8203H31.7586H31.6968H31.635H31.5732H31.5113H31.4494H31.3875H31.3255H31.2635H31.2015H31.1394H31.0772H31.0151H30.9529H30.8906H30.8283H30.766H30.7036H30.6412H30.5788H30.5163H30.4537H30.3911H30.3285H30.2658H30.2031H30.1403H30.0775H30.0146H29.9517H29.8887H29.8256H29.7626H29.6994H29.6362H29.573H29.5097H29.4463H29.3829H29.3195H29.2559H29.1924H29.1287H29.065H29.0013H28.9374H28.8736H28.8096H28.7456H28.6815H28.6174H28.5532H28.4889H28.4246H28.3602H28.2958H28.2312H28.1666H28.102H28.0373H27.9724H27.9076H27.8426H27.7776H27.7125H27.6474H27.5821H27.5168H27.4514H27.386H27.3204H27.2548H27.1891H27.1233H27.0575H26.9916H26.9255H26.8594H26.7933H26.727H26.6607H26.5943H26.5277H26.4612H26.3945H26.3277H26.2609H26.1939H26.1269H26.0598H25.9926H25.9253H25.8579H25.7904H25.7229H25.6552H25.5874H25.5196H25.4517H25.3836H25.3155H25.2473H25.1789H25.1105H25.042H24.9734H24.9046H24.8358H24.7669H24.6979H24.6288H24.5595H24.4902H24.4208H24.3512H24.2816H24.2118H24.142H24.072H24.0019H23.9317H23.8615H23.7911H23.7205H23.6499H23.5792H23.5083H23.4374H23.3663H23.2951H23.2238H23.1524H23.0809H23.0092H22.9374H22.8655H22.7935H22.7214H22.6492H22.5768H22.5043H22.4317H22.359H22.2861H22.2131H22.14H22.0668H21.9934H21.9199H21.8463H21.7726H21.6987H21.6247H21.5506H21.4764H21.402H21.3274H21.2528H21.178H21.1031H21.028H20.9528H20.8775H20.8021H20.7265H20.6507H20.5749H20.4989H20.4227H20.3464H20.27H20.1934H20.1167H20.0398H19.9628H19.8857H19.8084H19.731H19.6534H19.5757H19.4978H19.4198H19.3416H19.2633H19.1848H19.1062H19.0274H18.9485H18.8694H18.7902H18.7108H18.6313H18.5516H18.4718H18.3918H18.3116H18.2313H18.1508H18.0702H17.9894H17.9085H17.8273H17.7461H17.6646H17.583H17.5013H17.4194H17.3373H17.255H17.1726H17.09H17.0073H16.9243C16.7778 183.132 16.6956 183.097 16.642 183.064C16.5807 183.026 16.5047 182.955 16.4306 182.829C16.2682 182.553 16.1944 182.141 16.2521 181.785C17.6523 173.127 25.3653 166.455 34.7252 166.455C44.0852 166.455 51.7982 173.127 53.1984 181.785C53.3068 182.454 53.138 182.695 53.0518 182.784C52.929 182.91 52.5741 183.132 51.7263 183.132Z" fill="white" stroke="#212529" stroke-width="3"/> +</svg> + `; 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 16641c0d52..38903bab19 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 @@ -1,6 +1,6 @@ <app-header></app-header> -<bit-container> +<bit-container *ngIf="!IsProviderManaged"> <ng-container *ngIf="!firstLoaded && loading"> <i class="bwi bwi-spinner bwi-spin text-muted" title="{{ 'loading' | i18n }}"></i> <span class="sr-only">{{ "loading" | i18n }}</span> @@ -256,3 +256,13 @@ </ng-container> </ng-container> </bit-container> +<bit-container *ngIf="IsProviderManaged"> + <div + class="tw-mx-auto tw-flex tw-flex-col tw-items-center tw-justify-center tw-pt-24 tw-text-center tw-font-bold" + > + <bit-icon [icon]="manageBillingFromProviderPortal"></bit-icon> + <ng-container slot="description">{{ + "manageBillingFromProviderPortalMessage" | i18n + }}</ng-container> + </div> +</bit-container> 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 477032deba..7acb108808 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 @@ -5,7 +5,7 @@ import { concatMap, firstValueFrom, lastValueFrom, Observable, Subject, takeUnti 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 { OrganizationApiKeyType } from "@bitwarden/common/admin-console/enums"; +import { OrganizationApiKeyType, ProviderType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { PlanType } from "@bitwarden/common/billing/enums"; import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; @@ -28,6 +28,7 @@ import { } from "../shared/offboarding-survey.component"; import { BillingSyncApiKeyComponent } from "./billing-sync-api-key.component"; +import { ManageBilling } from "./icons/manage-billing.icon"; import { SecretsManagerSubscriptionOptions } from "./sm-adjust-subscription.component"; @Component({ @@ -47,11 +48,17 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy loading: boolean; locale: string; showUpdatedSubscriptionStatusSection$: Observable<boolean>; + manageBillingFromProviderPortal = ManageBilling; + IsProviderManaged = false; protected readonly teamsStarter = ProductType.TeamsStarter; private destroy$ = new Subject<void>(); + protected enableConsolidatedBilling$ = this.configService.getFeatureFlag$( + FeatureFlag.EnableConsolidatedBilling, + ); + constructor( private apiService: ApiService, private platformUtilsService: PlatformUtilsService, @@ -99,6 +106,13 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy this.loading = true; this.locale = await firstValueFrom(this.i18nService.locale$); this.userOrg = await this.organizationService.get(this.organizationId); + const enableConsolidatedBilling = await firstValueFrom(this.enableConsolidatedBilling$); + this.IsProviderManaged = + this.userOrg.hasProvider && + this.userOrg.providerType == ProviderType.Msp && + enableConsolidatedBilling + ? true + : false; if (this.userOrg.canViewSubscription) { this.sub = await this.organizationApiService.getSubscription(this.organizationId); this.lineItems = this.sub?.subscription?.items; diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 63069a83de..3a590622b8 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -8049,5 +8049,8 @@ }, "collectionItemSelect": { "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } From be50a174def9763b88086c60552cc31320c34f78 Mon Sep 17 00:00:00 2001 From: Colton Hurst <colton@coltonhurst.com> Date: Tue, 30 Apr 2024 12:31:09 -0400 Subject: [PATCH 325/351] SM-1196: Update export file name (#8865) --- .../event-logs/service-accounts-events.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 554e7fa37d..0547c4fcba 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 @@ -17,7 +17,7 @@ import { ServiceAccountEventLogApiService } from "./service-account-event-log-ap templateUrl: "./service-accounts-events.component.html", }) export class ServiceAccountEventsComponent extends BaseEventsComponent implements OnDestroy { - exportFileName = "service-account-events"; + exportFileName = "machine-account-events"; private destroy$ = new Subject<void>(); private serviceAccountId: string; From 3acbffa0726d8b4bda5c410d2ad98cf226a8ab7a Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Tue, 30 Apr 2024 12:35:36 -0400 Subject: [PATCH 326/351] [PM-6144] Basic auth autofill in Manifest v3 (#8975) * Add Support for autofilling Basic Auth to MV3 * Remove `any` --- .../background/web-request.background.ts | 34 ++++++------------- .../browser/src/background/main.background.ts | 7 ++-- apps/browser/src/manifest.v3.json | 4 ++- 3 files changed, 16 insertions(+), 29 deletions(-) diff --git a/apps/browser/src/autofill/background/web-request.background.ts b/apps/browser/src/autofill/background/web-request.background.ts index 8cdfa0f027..2eb976529f 100644 --- a/apps/browser/src/autofill/background/web-request.background.ts +++ b/apps/browser/src/autofill/background/web-request.background.ts @@ -4,40 +4,29 @@ import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { BrowserApi } from "../../platform/browser/browser-api"; - export default class WebRequestBackground { - private pendingAuthRequests: any[] = []; - private webRequest: any; + private pendingAuthRequests: Set<string> = new Set<string>([]); private isFirefox: boolean; constructor( platformUtilsService: PlatformUtilsService, private cipherService: CipherService, private authService: AuthService, + private readonly webRequest: typeof chrome.webRequest, ) { - if (BrowserApi.isManifestVersion(2)) { - this.webRequest = chrome.webRequest; - } this.isFirefox = platformUtilsService.isFirefox(); } - async init() { - if (!this.webRequest || !this.webRequest.onAuthRequired) { - return; - } - + startListening() { this.webRequest.onAuthRequired.addListener( - async (details: any, callback: any) => { - if (!details.url || this.pendingAuthRequests.indexOf(details.requestId) !== -1) { + async (details, callback) => { + if (!details.url || this.pendingAuthRequests.has(details.requestId)) { if (callback) { - callback(); + callback(null); } return; } - - this.pendingAuthRequests.push(details.requestId); - + this.pendingAuthRequests.add(details.requestId); if (this.isFirefox) { // eslint-disable-next-line return new Promise(async (resolve, reject) => { @@ -51,7 +40,7 @@ export default class WebRequestBackground { [this.isFirefox ? "blocking" : "asyncBlocking"], ); - this.webRequest.onCompleted.addListener((details: any) => this.completeAuthRequest(details), { + this.webRequest.onCompleted.addListener((details) => this.completeAuthRequest(details), { urls: ["http://*/*"], }); this.webRequest.onErrorOccurred.addListener( @@ -91,10 +80,7 @@ export default class WebRequestBackground { } } - private completeAuthRequest(details: any) { - const i = this.pendingAuthRequests.indexOf(details.requestId); - if (i > -1) { - this.pendingAuthRequests.splice(i, 1); - } + private completeAuthRequest(details: chrome.webRequest.WebResponseCacheDetails) { + this.pendingAuthRequests.delete(details.requestId); } } diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 01e325ad51..adcb4a21ba 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1056,11 +1056,12 @@ export default class MainBackground { this.cipherService, ); - if (BrowserApi.isManifestVersion(2)) { + if (chrome.webRequest != null && chrome.webRequest.onAuthRequired != null) { this.webRequestBackground = new WebRequestBackground( this.platformUtilsService, this.cipherService, this.authService, + chrome.webRequest, ); } } @@ -1106,9 +1107,7 @@ export default class MainBackground { await this.tabsBackground.init(); this.contextMenusBackground?.init(); await this.idleBackground.init(); - if (BrowserApi.isManifestVersion(2)) { - await this.webRequestBackground.init(); - } + this.webRequestBackground?.startListening(); if (this.platformUtilsService.isFirefox() && !this.isPrivateMode) { // Set Private Mode windows to the default icon - they do not share state with the background page diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index c0c88706b8..b7aaba2e0e 100644 --- a/apps/browser/src/manifest.v3.json +++ b/apps/browser/src/manifest.v3.json @@ -60,7 +60,9 @@ "clipboardWrite", "idle", "scripting", - "offscreen" + "offscreen", + "webRequest", + "webRequestAuthProvider" ], "optional_permissions": ["nativeMessaging", "privacy"], "host_permissions": ["<all_urls>"], From 200b0f75341dd38186f1ea05dab399ab12c4d771 Mon Sep 17 00:00:00 2001 From: Matt Gibson <mgibson@bitwarden.com> Date: Tue, 30 Apr 2024 12:46:01 -0400 Subject: [PATCH 327/351] Correct and test changeover point for userId source in storage migration (#8990) --- .../src/state-migrations/migration-helper.spec.ts | 5 +++++ .../src/state-migrations/migration-helper.ts | 6 +++--- .../migrations/60-known-accounts.spec.ts | 14 +++++--------- .../migrations/60-known-accounts.ts | 4 ++-- 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/libs/common/src/state-migrations/migration-helper.spec.ts b/libs/common/src/state-migrations/migration-helper.spec.ts index 162fac2fab..21c5c72a18 100644 --- a/libs/common/src/state-migrations/migration-helper.spec.ts +++ b/libs/common/src/state-migrations/migration-helper.spec.ts @@ -235,6 +235,11 @@ export function mockMigrationHelper( helper.setToUser(userId, keyDefinition, value), ); mockHelper.getAccounts.mockImplementation(() => helper.getAccounts()); + mockHelper.getKnownUserIds.mockImplementation(() => helper.getKnownUserIds()); + mockHelper.removeFromGlobal.mockImplementation((keyDefinition) => + helper.removeFromGlobal(keyDefinition), + ); + mockHelper.remove.mockImplementation((key) => helper.remove(key)); mockHelper.type = helper.type; diff --git a/libs/common/src/state-migrations/migration-helper.ts b/libs/common/src/state-migrations/migration-helper.ts index 5d1de8dd49..b377df8ef9 100644 --- a/libs/common/src/state-migrations/migration-helper.ts +++ b/libs/common/src/state-migrations/migration-helper.ts @@ -175,8 +175,8 @@ export class MigrationHelper { * Helper method to read known users ids. */ async getKnownUserIds(): Promise<string[]> { - if (this.currentVersion < 61) { - return knownAccountUserIdsBuilderPre61(this.storageService); + if (this.currentVersion < 60) { + return knownAccountUserIdsBuilderPre60(this.storageService); } else { return knownAccountUserIdsBuilder(this.storageService); } @@ -245,7 +245,7 @@ function globalKeyBuilderPre9(): string { throw Error("No key builder should be used for versions prior to 9."); } -async function knownAccountUserIdsBuilderPre61( +async function knownAccountUserIdsBuilderPre60( storageService: AbstractStorageService, ): Promise<string[]> { return (await storageService.get<string[]>("authenticatedAccounts")) ?? []; diff --git a/libs/common/src/state-migrations/migrations/60-known-accounts.spec.ts b/libs/common/src/state-migrations/migrations/60-known-accounts.spec.ts index 28dedb3c39..01be4adb6a 100644 --- a/libs/common/src/state-migrations/migrations/60-known-accounts.spec.ts +++ b/libs/common/src/state-migrations/migrations/60-known-accounts.spec.ts @@ -51,17 +51,13 @@ const rollbackJson = () => { }, global_account_accounts: { user1: { - profile: { - email: "user1", - name: "User 1", - emailVerified: true, - }, + email: "user1", + name: "User 1", + emailVerified: true, }, user2: { - profile: { - email: "", - emailVerified: false, - }, + email: "", + emailVerified: false, }, }, global_account_activeAccountId: "user1", diff --git a/libs/common/src/state-migrations/migrations/60-known-accounts.ts b/libs/common/src/state-migrations/migrations/60-known-accounts.ts index 75117da5b4..3b02a5acc4 100644 --- a/libs/common/src/state-migrations/migrations/60-known-accounts.ts +++ b/libs/common/src/state-migrations/migrations/60-known-accounts.ts @@ -38,8 +38,8 @@ export class KnownAccountsMigrator extends Migrator<59, 60> { } async rollback(helper: MigrationHelper): Promise<void> { // authenticated account are removed, but the accounts record also contains logged out accounts. Best we can do is to add them all back - const accounts = (await helper.getFromGlobal<Record<string, unknown>>(ACCOUNT_ACCOUNTS)) ?? {}; - await helper.set("authenticatedAccounts", Object.keys(accounts)); + const userIds = (await helper.getKnownUserIds()) ?? []; + await helper.set("authenticatedAccounts", userIds); await helper.removeFromGlobal(ACCOUNT_ACCOUNTS); // Active Account Id From b4631b0dd164ee34de9f5dff43a1bf559880ebd0 Mon Sep 17 00:00:00 2001 From: Matt Gibson <mgibson@bitwarden.com> Date: Tue, 30 Apr 2024 12:58:16 -0400 Subject: [PATCH 328/351] Ps/improve-log-service (#8989) * Match console method signatures in logService abstraction * Add a few usages of improved signature * Remove reality check test * Improve electron logging --- .../src/background/runtime.background.ts | 3 +- .../offscreen-document.spec.ts | 4 +- .../offscreen-document/offscreen-document.ts | 2 +- .../services/console-log.service.spec.ts | 36 ++++++------ .../platform/services/console-log.service.ts | 6 +- apps/desktop/src/platform/preload.ts | 3 +- .../services/electron-log.main.service.ts | 14 ++--- .../services/electron-log.renderer.service.ts | 14 +++-- .../services/logging-error-handler.ts | 2 +- libs/common/spec/intercept-console.ts | 23 +++----- .../src/platform/abstractions/log.service.ts | 10 ++-- .../services/console-log.service.spec.ts | 57 ++++++++++--------- .../platform/services/console-log.service.ts | 26 ++++----- 13 files changed, 104 insertions(+), 96 deletions(-) diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts index 98b1df9c80..d8f3cf840f 100644 --- a/apps/browser/src/background/runtime.background.ts +++ b/apps/browser/src/background/runtime.background.ts @@ -76,7 +76,8 @@ export default class RuntimeBackground { void this.processMessageWithSender(msg, sender).catch((err) => this.logService.error( - `Error while processing message in RuntimeBackground '${msg?.command}'. Error: ${err?.message ?? "Unknown Error"}`, + `Error while processing message in RuntimeBackground '${msg?.command}'.`, + err, ), ); return false; diff --git a/apps/browser/src/platform/offscreen-document/offscreen-document.spec.ts b/apps/browser/src/platform/offscreen-document/offscreen-document.spec.ts index 1cbcc7a94c..933cd08c2e 100644 --- a/apps/browser/src/platform/offscreen-document/offscreen-document.spec.ts +++ b/apps/browser/src/platform/offscreen-document/offscreen-document.spec.ts @@ -28,6 +28,7 @@ describe("OffscreenDocument", () => { }); it("shows a console message if the handler throws an error", async () => { + const error = new Error("test error"); browserClipboardServiceCopySpy.mockRejectedValueOnce(new Error("test error")); sendExtensionRuntimeMessage({ command: "offscreenCopyToClipboard", text: "test" }); @@ -35,7 +36,8 @@ describe("OffscreenDocument", () => { expect(browserClipboardServiceCopySpy).toHaveBeenCalled(); expect(consoleErrorSpy).toHaveBeenCalledWith( - "Error resolving extension message response: Error: test error", + "Error resolving extension message response", + error, ); }); diff --git a/apps/browser/src/platform/offscreen-document/offscreen-document.ts b/apps/browser/src/platform/offscreen-document/offscreen-document.ts index 627036b80b..4994a6e9ba 100644 --- a/apps/browser/src/platform/offscreen-document/offscreen-document.ts +++ b/apps/browser/src/platform/offscreen-document/offscreen-document.ts @@ -71,7 +71,7 @@ class OffscreenDocument implements OffscreenDocumentInterface { Promise.resolve(messageResponse) .then((response) => sendResponse(response)) .catch((error) => - this.consoleLogService.error(`Error resolving extension message response: ${error}`), + this.consoleLogService.error("Error resolving extension message response", error), ); return true; }; diff --git a/apps/cli/src/platform/services/console-log.service.spec.ts b/apps/cli/src/platform/services/console-log.service.spec.ts index 10a0ad8cca..03598b16e6 100644 --- a/apps/cli/src/platform/services/console-log.service.spec.ts +++ b/apps/cli/src/platform/services/console-log.service.spec.ts @@ -2,13 +2,18 @@ import { interceptConsole, restoreConsole } from "@bitwarden/common/spec"; import { ConsoleLogService } from "./console-log.service"; -let caughtMessage: any = {}; - describe("CLI Console log service", () => { + const error = new Error("this is an error"); + const obj = { a: 1, b: 2 }; let logService: ConsoleLogService; + let consoleSpy: { + log: jest.Mock<any, any>; + warn: jest.Mock<any, any>; + error: jest.Mock<any, any>; + }; + beforeEach(() => { - caughtMessage = {}; - interceptConsole(caughtMessage); + consoleSpy = interceptConsole(); logService = new ConsoleLogService(true); }); @@ -19,24 +24,21 @@ describe("CLI Console log service", () => { it("should redirect all console to error if BW_RESPONSE env is true", () => { process.env.BW_RESPONSE = "true"; - logService.debug("this is a debug message"); - expect(caughtMessage).toMatchObject({ - error: { 0: "this is a debug message" }, - }); + logService.debug("this is a debug message", error, obj); + expect(consoleSpy.error).toHaveBeenCalledWith("this is a debug message", error, obj); }); it("should not redirect console to error if BW_RESPONSE != true", () => { process.env.BW_RESPONSE = "false"; - logService.debug("debug"); - logService.info("info"); - logService.warning("warning"); - logService.error("error"); + logService.debug("debug", error, obj); + logService.info("info", error, obj); + logService.warning("warning", error, obj); + logService.error("error", error, obj); - expect(caughtMessage).toMatchObject({ - log: { 0: "info" }, - warn: { 0: "warning" }, - error: { 0: "error" }, - }); + expect(consoleSpy.log).toHaveBeenCalledWith("debug", error, obj); + expect(consoleSpy.log).toHaveBeenCalledWith("info", error, obj); + expect(consoleSpy.warn).toHaveBeenCalledWith("warning", error, obj); + expect(consoleSpy.error).toHaveBeenCalledWith("error", error, obj); }); }); diff --git a/apps/cli/src/platform/services/console-log.service.ts b/apps/cli/src/platform/services/console-log.service.ts index a35dae71fc..5bdc0b4015 100644 --- a/apps/cli/src/platform/services/console-log.service.ts +++ b/apps/cli/src/platform/services/console-log.service.ts @@ -6,17 +6,17 @@ export class ConsoleLogService extends BaseConsoleLogService { super(isDev, filter); } - write(level: LogLevelType, message: string) { + write(level: LogLevelType, message?: any, ...optionalParams: any[]) { if (this.filter != null && this.filter(level)) { return; } if (process.env.BW_RESPONSE === "true") { // eslint-disable-next-line - console.error(message); + console.error(message, ...optionalParams); return; } - super.write(level, message); + super.write(level, message, ...optionalParams); } } diff --git a/apps/desktop/src/platform/preload.ts b/apps/desktop/src/platform/preload.ts index 771d25ef0a..d81d647652 100644 --- a/apps/desktop/src/platform/preload.ts +++ b/apps/desktop/src/platform/preload.ts @@ -103,7 +103,8 @@ export default { isMacAppStore: isMacAppStore(), isWindowsStore: isWindowsStore(), reloadProcess: () => ipcRenderer.send("reload-process"), - log: (level: LogLevelType, message: string) => ipcRenderer.invoke("ipc.log", { level, message }), + log: (level: LogLevelType, message?: any, ...optionalParams: any[]) => + ipcRenderer.invoke("ipc.log", { level, message, optionalParams }), openContextMenu: ( menu: { diff --git a/apps/desktop/src/platform/services/electron-log.main.service.ts b/apps/desktop/src/platform/services/electron-log.main.service.ts index 832365785c..0725de3dc9 100644 --- a/apps/desktop/src/platform/services/electron-log.main.service.ts +++ b/apps/desktop/src/platform/services/electron-log.main.service.ts @@ -25,28 +25,28 @@ export class ElectronLogMainService extends BaseLogService { } log.initialize(); - ipcMain.handle("ipc.log", (_event, { level, message }) => { - this.write(level, message); + ipcMain.handle("ipc.log", (_event, { level, message, optionalParams }) => { + this.write(level, message, ...optionalParams); }); } - write(level: LogLevelType, message: string) { + write(level: LogLevelType, message?: any, ...optionalParams: any[]) { if (this.filter != null && this.filter(level)) { return; } switch (level) { case LogLevelType.Debug: - log.debug(message); + log.debug(message, ...optionalParams); break; case LogLevelType.Info: - log.info(message); + log.info(message, ...optionalParams); break; case LogLevelType.Warning: - log.warn(message); + log.warn(message, ...optionalParams); break; case LogLevelType.Error: - log.error(message); + log.error(message, ...optionalParams); break; default: break; diff --git a/apps/desktop/src/platform/services/electron-log.renderer.service.ts b/apps/desktop/src/platform/services/electron-log.renderer.service.ts index e0e0757e6a..cea939f160 100644 --- a/apps/desktop/src/platform/services/electron-log.renderer.service.ts +++ b/apps/desktop/src/platform/services/electron-log.renderer.service.ts @@ -6,27 +6,29 @@ export class ElectronLogRendererService extends BaseLogService { super(ipc.platform.isDev, filter); } - write(level: LogLevelType, message: string) { + write(level: LogLevelType, message?: any, ...optionalParams: any[]) { if (this.filter != null && this.filter(level)) { return; } /* eslint-disable no-console */ - ipc.platform.log(level, message).catch((e) => console.log("Error logging", e)); + ipc.platform + .log(level, message, ...optionalParams) + .catch((e) => console.log("Error logging", e)); /* eslint-disable no-console */ switch (level) { case LogLevelType.Debug: - console.debug(message); + console.debug(message, ...optionalParams); break; case LogLevelType.Info: - console.info(message); + console.info(message, ...optionalParams); break; case LogLevelType.Warning: - console.warn(message); + console.warn(message, ...optionalParams); break; case LogLevelType.Error: - console.error(message); + console.error(message, ...optionalParams); break; default: break; diff --git a/libs/angular/src/platform/services/logging-error-handler.ts b/libs/angular/src/platform/services/logging-error-handler.ts index 522412dd28..5644272d35 100644 --- a/libs/angular/src/platform/services/logging-error-handler.ts +++ b/libs/angular/src/platform/services/logging-error-handler.ts @@ -14,7 +14,7 @@ export class LoggingErrorHandler extends ErrorHandler { override handleError(error: any): void { try { const logService = this.injector.get(LogService, null); - logService.error(error); + logService.error("Unhandled error in angular", error); } catch { super.handleError(error); } diff --git a/libs/common/spec/intercept-console.ts b/libs/common/spec/intercept-console.ts index 01c4063e7a..565d475cae 100644 --- a/libs/common/spec/intercept-console.ts +++ b/libs/common/spec/intercept-console.ts @@ -2,22 +2,17 @@ const originalConsole = console; declare let console: any; -export function interceptConsole(interceptions: any): object { +export function interceptConsole(): { + log: jest.Mock<any, any>; + warn: jest.Mock<any, any>; + error: jest.Mock<any, any>; +} { console = { - log: function () { - // eslint-disable-next-line - interceptions.log = arguments; - }, - warn: function () { - // eslint-disable-next-line - interceptions.warn = arguments; - }, - error: function () { - // eslint-disable-next-line - interceptions.error = arguments; - }, + log: jest.fn(), + warn: jest.fn(), + error: jest.fn(), }; - return interceptions; + return console; } export function restoreConsole() { diff --git a/libs/common/src/platform/abstractions/log.service.ts b/libs/common/src/platform/abstractions/log.service.ts index dffa3ca8d3..d77a4f6990 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 { - 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; + abstract debug(message?: any, ...optionalParams: any[]): void; + abstract info(message?: any, ...optionalParams: any[]): void; + abstract warning(message?: any, ...optionalParams: any[]): void; + abstract error(message?: any, ...optionalParams: any[]): void; + abstract write(level: LogLevelType, message?: any, ...optionalParams: any[]): void; } diff --git a/libs/common/src/platform/services/console-log.service.spec.ts b/libs/common/src/platform/services/console-log.service.spec.ts index 129969bbc4..508ca4eb32 100644 --- a/libs/common/src/platform/services/console-log.service.spec.ts +++ b/libs/common/src/platform/services/console-log.service.spec.ts @@ -2,13 +2,18 @@ import { interceptConsole, restoreConsole } from "../../../spec"; import { ConsoleLogService } from "./console-log.service"; -let caughtMessage: any; - describe("ConsoleLogService", () => { + const error = new Error("this is an error"); + const obj = { a: 1, b: 2 }; + let consoleSpy: { + log: jest.Mock<any, any>; + warn: jest.Mock<any, any>; + error: jest.Mock<any, any>; + }; let logService: ConsoleLogService; + beforeEach(() => { - caughtMessage = {}; - interceptConsole(caughtMessage); + consoleSpy = interceptConsole(); logService = new ConsoleLogService(true); }); @@ -18,41 +23,41 @@ describe("ConsoleLogService", () => { it("filters messages below the set threshold", () => { logService = new ConsoleLogService(true, () => true); - logService.debug("debug"); - logService.info("info"); - logService.warning("warning"); - logService.error("error"); + logService.debug("debug", error, obj); + logService.info("info", error, obj); + logService.warning("warning", error, obj); + logService.error("error", error, obj); - expect(caughtMessage).toEqual({}); + expect(consoleSpy.log).not.toHaveBeenCalled(); + expect(consoleSpy.warn).not.toHaveBeenCalled(); + expect(consoleSpy.error).not.toHaveBeenCalled(); }); + it("only writes debug messages in dev mode", () => { logService = new ConsoleLogService(false); logService.debug("debug message"); - expect(caughtMessage.log).toBeUndefined(); + expect(consoleSpy.log).not.toHaveBeenCalled(); }); it("writes debug/info messages to console.log", () => { - logService.debug("this is a debug message"); - expect(caughtMessage).toMatchObject({ - log: { "0": "this is a debug message" }, - }); + logService.debug("this is a debug message", error, obj); + logService.info("this is an info message", error, obj); - logService.info("this is an info message"); - expect(caughtMessage).toMatchObject({ - log: { "0": "this is an info message" }, - }); + expect(consoleSpy.log).toHaveBeenCalledTimes(2); + expect(consoleSpy.log).toHaveBeenCalledWith("this is a debug message", error, obj); + expect(consoleSpy.log).toHaveBeenCalledWith("this is an info message", error, obj); }); + it("writes warning messages to console.warn", () => { - logService.warning("this is a warning message"); - expect(caughtMessage).toMatchObject({ - warn: { 0: "this is a warning message" }, - }); + logService.warning("this is a warning message", error, obj); + + expect(consoleSpy.warn).toHaveBeenCalledWith("this is a warning message", error, obj); }); + it("writes error messages to console.error", () => { - logService.error("this is an error message"); - expect(caughtMessage).toMatchObject({ - error: { 0: "this is an error message" }, - }); + logService.error("this is an error message", error, obj); + + expect(consoleSpy.error).toHaveBeenCalledWith("this is an error message", error, obj); }); }); diff --git a/libs/common/src/platform/services/console-log.service.ts b/libs/common/src/platform/services/console-log.service.ts index 3eb3ad1881..a1480a0c26 100644 --- a/libs/common/src/platform/services/console-log.service.ts +++ b/libs/common/src/platform/services/console-log.service.ts @@ -9,26 +9,26 @@ export class ConsoleLogService implements LogServiceAbstraction { protected filter: (level: LogLevelType) => boolean = null, ) {} - debug(message: string) { + debug(message?: any, ...optionalParams: any[]) { if (!this.isDev) { return; } - this.write(LogLevelType.Debug, message); + this.write(LogLevelType.Debug, message, ...optionalParams); } - info(message: string) { - this.write(LogLevelType.Info, message); + info(message?: any, ...optionalParams: any[]) { + this.write(LogLevelType.Info, message, ...optionalParams); } - warning(message: string) { - this.write(LogLevelType.Warning, message); + warning(message?: any, ...optionalParams: any[]) { + this.write(LogLevelType.Warning, message, ...optionalParams); } - error(message: string) { - this.write(LogLevelType.Error, message); + error(message?: any, ...optionalParams: any[]) { + this.write(LogLevelType.Error, message, ...optionalParams); } - write(level: LogLevelType, message: string) { + write(level: LogLevelType, message?: any, ...optionalParams: any[]) { if (this.filter != null && this.filter(level)) { return; } @@ -36,19 +36,19 @@ export class ConsoleLogService implements LogServiceAbstraction { switch (level) { case LogLevelType.Debug: // eslint-disable-next-line - console.log(message); + console.log(message, ...optionalParams); break; case LogLevelType.Info: // eslint-disable-next-line - console.log(message); + console.log(message, ...optionalParams); break; case LogLevelType.Warning: // eslint-disable-next-line - console.warn(message); + console.warn(message, ...optionalParams); break; case LogLevelType.Error: // eslint-disable-next-line - console.error(message); + console.error(message, ...optionalParams); break; default: break; From 7e9ab6a15b90a67bf375015e125b007317330718 Mon Sep 17 00:00:00 2001 From: Matt Gibson <mgibson@bitwarden.com> Date: Wed, 1 May 2024 07:59:30 -0400 Subject: [PATCH 329/351] [PM-7807][PM-7617] [PM-6185] Firefox private mode out of experimentation (#8921) * Remove getbgService for crypto service * Remove special authentication for state service * Use synced memory storage popup contexts use foreground, background contexts use background. Simple * Remove private mode warnings --- apps/browser/src/_locales/en/messages.json | 3 -- .../src/auth/popup/lock.component.html | 1 - .../src/auth/popup/login.component.html | 1 - .../browser/src/background/main.background.ts | 34 +++---------- .../src/platform/browser/browser-api.ts | 4 -- .../popup/browser-popup-utils.spec.ts | 22 -------- .../src/platform/popup/browser-popup-utils.ts | 7 --- .../services/default-browser-state.service.ts | 9 ---- apps/browser/src/popup/app.module.ts | 2 - .../private-mode-warning.component.html | 6 --- .../private-mode-warning.component.ts | 15 ------ apps/browser/src/popup/scss/pages.scss | 5 -- .../src/popup/services/services.module.ts | 50 +++++++++++++++++-- 13 files changed, 53 insertions(+), 106 deletions(-) delete mode 100644 apps/browser/src/popup/components/private-mode-warning.component.html delete mode 100644 apps/browser/src/popup/components/private-mode-warning.component.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 1c0b178895..bd62b825e7 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Lock the vault" }, - "privateModeWarning": { - "message": "Private mode support is experimental and some features are limited." - }, "customFields": { "message": "Custom fields" }, diff --git a/apps/browser/src/auth/popup/lock.component.html b/apps/browser/src/auth/popup/lock.component.html index 9892503a7b..5ea839470b 100644 --- a/apps/browser/src/auth/popup/lock.component.html +++ b/apps/browser/src/auth/popup/lock.component.html @@ -89,7 +89,6 @@ <p class="text-center" *ngIf="!fido2Data.isFido2Session"> <button type="button" appStopClick (click)="logOut()">{{ "logOut" | i18n }}</button> </p> - <app-private-mode-warning></app-private-mode-warning> <app-callout *ngIf="biometricError" type="error">{{ biometricError }}</app-callout> <p class="text-center text-muted" *ngIf="pendingBiometric"> <i class="bwi bwi-spinner bwi-spin" aria-hidden="true"></i> {{ "awaitDesktop" | i18n }} diff --git a/apps/browser/src/auth/popup/login.component.html b/apps/browser/src/auth/popup/login.component.html index b24a25a0f1..7a4211a5cc 100644 --- a/apps/browser/src/auth/popup/login.component.html +++ b/apps/browser/src/auth/popup/login.component.html @@ -57,7 +57,6 @@ </button> </div> </div> - <app-private-mode-warning></app-private-mode-warning> <div class="content login-buttons"> <button type="submit" class="btn primary block" [disabled]="form.loading"> <span [hidden]="form.loading" diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index adcb4a21ba..16b8c8beea 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -109,7 +109,6 @@ import { EncryptServiceImplementation } from "@bitwarden/common/platform/service import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation"; import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.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"; import { SystemService } from "@bitwarden/common/platform/services/system.service"; @@ -356,10 +355,7 @@ export default class MainBackground { private isSafari: boolean; private nativeMessagingBackground: NativeMessagingBackground; - constructor( - public isPrivateMode: boolean = false, - public popupOnlyContext: boolean = false, - ) { + constructor(public popupOnlyContext: boolean = false) { // Services const lockedCallback = async (userId?: string) => { if (this.notificationsService != null) { @@ -443,10 +439,14 @@ export default class MainBackground { this.secureStorageService = this.storageService; // secure storage is not supported in browsers, so we use local storage and warn users when it is used this.memoryStorageForStateProviders = BrowserApi.isManifestVersion(3) ? new BrowserMemoryStorageService() // mv3 stores to storage.session - : new BackgroundMemoryStorageService(); // mv2 stores to memory + : popupOnlyContext + ? new ForegroundMemoryStorageService() + : new BackgroundMemoryStorageService(); // mv2 stores to memory this.memoryStorageService = BrowserApi.isManifestVersion(3) ? this.memoryStorageForStateProviders // manifest v3 can reuse the same storage. They are split for v2 due to lacking a good sync mechanism, which isn't true for v3 - : new MemoryStorageService(); + : popupOnlyContext + ? new ForegroundMemoryStorageService() + : new BackgroundMemoryStorageService(); this.largeObjectMemoryStorageForStateProviders = BrowserApi.isManifestVersion(3) ? mv3MemoryStorageCreator() // mv3 stores to local-backed session storage : this.memoryStorageForStateProviders; // mv2 stores to the same location @@ -1109,27 +1109,9 @@ export default class MainBackground { await this.idleBackground.init(); this.webRequestBackground?.startListening(); - if (this.platformUtilsService.isFirefox() && !this.isPrivateMode) { - // Set Private Mode windows to the default icon - they do not share state with the background page - const privateWindows = await BrowserApi.getPrivateModeWindows(); - privateWindows.forEach(async (win) => { - await new UpdateBadge(self).setBadgeIcon("", win.id); - }); - - // 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.onWindowCreated(async (win) => { - if (win.incognito) { - await new UpdateBadge(self).setBadgeIcon("", win.id); - } - }); - } - return new Promise<void>((resolve) => { setTimeout(async () => { - if (!this.isPrivateMode) { - await this.refreshBadge(); - } + await this.refreshBadge(); await this.fullSync(true); setTimeout(() => this.notificationsService.init(), 2500); resolve(); diff --git a/apps/browser/src/platform/browser/browser-api.ts b/apps/browser/src/platform/browser/browser-api.ts index f536eb8312..e804cf2b8d 100644 --- a/apps/browser/src/platform/browser/browser-api.ts +++ b/apps/browser/src/platform/browser/browser-api.ts @@ -204,10 +204,6 @@ export class BrowserApi { chrome.tabs.sendMessage<TabMessage, T>(tabId, message, options, responseCallback); } - static async getPrivateModeWindows(): Promise<browser.windows.Window[]> { - return (await browser.windows.getAll()).filter((win) => win.incognito); - } - static async onWindowCreated(callback: (win: chrome.windows.Window) => any) { // FIXME: Make sure that is does not cause a memory leak in Safari or use BrowserApi.AddListener // and test that it doesn't break. diff --git a/apps/browser/src/platform/popup/browser-popup-utils.spec.ts b/apps/browser/src/platform/popup/browser-popup-utils.spec.ts index c2d33369bd..73f0d23f4f 100644 --- a/apps/browser/src/platform/popup/browser-popup-utils.spec.ts +++ b/apps/browser/src/platform/popup/browser-popup-utils.spec.ts @@ -138,28 +138,6 @@ describe("BrowserPopupUtils", () => { }); }); - describe("inPrivateMode", () => { - it("returns false if the background requires initialization", () => { - jest.spyOn(BrowserPopupUtils, "backgroundInitializationRequired").mockReturnValue(false); - - expect(BrowserPopupUtils.inPrivateMode()).toBe(false); - }); - - it("returns false if the manifest version is for version 3", () => { - jest.spyOn(BrowserPopupUtils, "backgroundInitializationRequired").mockReturnValue(true); - jest.spyOn(BrowserApi, "manifestVersion", "get").mockReturnValue(3); - - expect(BrowserPopupUtils.inPrivateMode()).toBe(false); - }); - - it("returns true if the background does not require initalization and the manifest version is version 2", () => { - jest.spyOn(BrowserPopupUtils, "backgroundInitializationRequired").mockReturnValue(true); - jest.spyOn(BrowserApi, "manifestVersion", "get").mockReturnValue(2); - - expect(BrowserPopupUtils.inPrivateMode()).toBe(true); - }); - }); - describe("openPopout", () => { beforeEach(() => { jest.spyOn(BrowserApi, "getWindow").mockResolvedValueOnce({ diff --git a/apps/browser/src/platform/popup/browser-popup-utils.ts b/apps/browser/src/platform/popup/browser-popup-utils.ts index 2e087db83e..a2249d466c 100644 --- a/apps/browser/src/platform/popup/browser-popup-utils.ts +++ b/apps/browser/src/platform/popup/browser-popup-utils.ts @@ -89,13 +89,6 @@ class BrowserPopupUtils { return !BrowserApi.getBackgroundPage(); } - /** - * Identifies if the popup is loading in private mode. - */ - static inPrivateMode() { - return BrowserPopupUtils.backgroundInitializationRequired() && !BrowserApi.isManifestVersion(3); - } - /** * Opens a popout window of any extension page. If the popout window is already open, it will be focused. * diff --git a/apps/browser/src/platform/services/default-browser-state.service.ts b/apps/browser/src/platform/services/default-browser-state.service.ts index f717ab96d8..b0b9e3748f 100644 --- a/apps/browser/src/platform/services/default-browser-state.service.ts +++ b/apps/browser/src/platform/services/default-browser-state.service.ts @@ -62,15 +62,6 @@ export class DefaultBrowserStateService await super.addAccount(account); } - async getIsAuthenticated(options?: StorageOptions): Promise<boolean> { - // Firefox Private Mode can clash with non-Private Mode because they both read from the same onDiskOptions - // Check that there is an account in memory before considering the user authenticated - return ( - (await super.getIsAuthenticated(options)) && - (await this.getAccount(await this.defaultInMemoryOptions())) != null - ); - } - // Overriding the base class to prevent deleting the cache on save. We register a storage listener // to delete the cache in the constructor above. protected override async saveAccountToDisk( diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index 2fb582d693..0862c2da52 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -70,7 +70,6 @@ import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit. import { AppRoutingModule } from "./app-routing.module"; import { AppComponent } from "./app.component"; import { PopOutComponent } from "./components/pop-out.component"; -import { PrivateModeWarningComponent } from "./components/private-mode-warning.component"; import { UserVerificationComponent } from "./components/user-verification.component"; import { ServicesModule } from "./services/services.module"; import { ExcludedDomainsComponent } from "./settings/excluded-domains.component"; @@ -150,7 +149,6 @@ import "../platform/popup/locales"; PasswordHistoryComponent, PopOutComponent, PremiumComponent, - PrivateModeWarningComponent, RegisterComponent, SendAddEditComponent, SendGroupingsComponent, diff --git a/apps/browser/src/popup/components/private-mode-warning.component.html b/apps/browser/src/popup/components/private-mode-warning.component.html deleted file mode 100644 index 9cd53ea1be..0000000000 --- a/apps/browser/src/popup/components/private-mode-warning.component.html +++ /dev/null @@ -1,6 +0,0 @@ -<app-callout class="app-private-mode-warning" type="warning" *ngIf="showWarning"> - {{ "privateModeWarning" | i18n }} - <a href="https://bitwarden.com/help/article/private-mode/" target="_blank" rel="noreferrer">{{ - "learnMore" | i18n - }}</a> -</app-callout> diff --git a/apps/browser/src/popup/components/private-mode-warning.component.ts b/apps/browser/src/popup/components/private-mode-warning.component.ts deleted file mode 100644 index ff6292bdbf..0000000000 --- a/apps/browser/src/popup/components/private-mode-warning.component.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Component, OnInit } from "@angular/core"; - -import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; - -@Component({ - selector: "app-private-mode-warning", - templateUrl: "private-mode-warning.component.html", -}) -export class PrivateModeWarningComponent implements OnInit { - showWarning = false; - - ngOnInit() { - this.showWarning = BrowserPopupUtils.inPrivateMode(); - } -} diff --git a/apps/browser/src/popup/scss/pages.scss b/apps/browser/src/popup/scss/pages.scss index 3c651682c1..3ae3647299 100644 --- a/apps/browser/src/popup/scss/pages.scss +++ b/apps/browser/src/popup/scss/pages.scss @@ -111,11 +111,6 @@ app-home { } } -.app-private-mode-warning { - display: block; - padding-top: 1rem; -} - body.body-sm, body.body-xs { app-home { diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index bec278aeeb..163b2f1edb 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -28,7 +28,9 @@ import { AccountService as AccountServiceAbstraction } from "@bitwarden/common/a import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; @@ -53,6 +55,7 @@ import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt. 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"; +import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; @@ -61,6 +64,7 @@ import { AbstractStorageService, ObservableStorageService, } from "@bitwarden/common/platform/abstractions/storage.service"; +import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging"; // eslint-disable-next-line no-restricted-imports -- Used for dependency injection @@ -100,6 +104,7 @@ import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; import { BrowserFileDownloadService } from "../../platform/popup/services/browser-file-download.service"; import { BrowserStateService as StateServiceAbstraction } from "../../platform/services/abstractions/browser-state.service"; import { ScriptInjectorService } from "../../platform/services/abstractions/script-injector.service"; +import { BrowserCryptoService } from "../../platform/services/browser-crypto.service"; import { BrowserEnvironmentService } from "../../platform/services/browser-environment.service"; import BrowserLocalStorageService from "../../platform/services/browser-local-storage.service"; import { BrowserScriptInjectorService } from "../../platform/services/browser-script-injector.service"; @@ -125,13 +130,12 @@ const OBSERVABLE_LARGE_OBJECT_MEMORY_STORAGE = new SafeInjectionToken< >("OBSERVABLE_LARGE_OBJECT_MEMORY_STORAGE"); const needsBackgroundInit = BrowserPopupUtils.backgroundInitializationRequired(); -const isPrivateMode = BrowserPopupUtils.inPrivateMode(); const mainBackground: MainBackground = needsBackgroundInit ? createLocalBgService() : BrowserApi.getBackgroundPage().bitwardenMain; function createLocalBgService() { - const localBgService = new MainBackground(isPrivateMode, true); + const localBgService = new MainBackground(true); // 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 localBgService.bootstrap(); @@ -220,12 +224,48 @@ const safeProviders: SafeProvider[] = [ }), safeProvider({ provide: CryptoService, - useFactory: (encryptService: EncryptService) => { - const cryptoService = getBgService<CryptoService>("cryptoService")(); + useFactory: ( + masterPasswordService: InternalMasterPasswordServiceAbstraction, + keyGenerationService: KeyGenerationService, + cryptoFunctionService: CryptoFunctionService, + encryptService: EncryptService, + platformUtilsService: PlatformUtilsService, + logService: LogService, + stateService: StateServiceAbstraction, + accountService: AccountServiceAbstraction, + stateProvider: StateProvider, + biometricStateService: BiometricStateService, + kdfConfigService: KdfConfigService, + ) => { + const cryptoService = new BrowserCryptoService( + masterPasswordService, + keyGenerationService, + cryptoFunctionService, + encryptService, + platformUtilsService, + logService, + stateService, + accountService, + stateProvider, + biometricStateService, + kdfConfigService, + ); new ContainerService(cryptoService, encryptService).attachToGlobal(self); return cryptoService; }, - deps: [EncryptService], + deps: [ + InternalMasterPasswordServiceAbstraction, + KeyGenerationService, + CryptoFunctionService, + EncryptService, + PlatformUtilsService, + LogService, + StateServiceAbstraction, + AccountServiceAbstraction, + StateProvider, + BiometricStateService, + KdfConfigService, + ], }), safeProvider({ provide: TotpServiceAbstraction, From 8ae71fabafb88e3e313847a4bca8c63432ef730c Mon Sep 17 00:00:00 2001 From: Jason Ng <jng@bitwarden.com> Date: Wed, 1 May 2024 10:39:22 -0400 Subject: [PATCH 330/351] [AC-1586] individual reports filter (#8598) * add filtering to individual reports --- .../exposed-passwords-report.component.ts | 12 +- .../inactive-two-factor-report.component.ts | 12 +- .../reused-passwords-report.component.ts | 5 +- .../unsecured-websites-report.component.ts | 5 +- .../tools/weak-passwords-report.component.ts | 4 + .../reports/pages/cipher-report.component.ts | 124 +++++++++++++++++- .../exposed-passwords-report.component.html | 25 +++- ...exposed-passwords-report.component.spec.ts | 6 +- .../exposed-passwords-report.component.ts | 14 +- .../inactive-two-factor-report.component.html | 25 +++- ...active-two-factor-report.component.spec.ts | 8 +- .../inactive-two-factor-report.component.ts | 13 +- .../reused-passwords-report.component.html | 27 +++- .../reused-passwords-report.component.spec.ts | 8 +- .../reused-passwords-report.component.ts | 12 +- .../unsecured-websites-report.component.html | 26 +++- ...nsecured-websites-report.component.spec.ts | 8 +- .../unsecured-websites-report.component.ts | 18 ++- .../weak-passwords-report.component.html | 25 +++- .../weak-passwords-report.component.spec.ts | 6 +- .../pages/weak-passwords-report.component.ts | 16 ++- apps/web/src/locales/en/messages.json | 40 ++++-- 22 files changed, 374 insertions(+), 65 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/tools/exposed-passwords-report.component.ts b/apps/web/src/app/admin-console/organizations/tools/exposed-passwords-report.component.ts index 54a4bb18b7..d354459ee9 100644 --- a/apps/web/src/app/admin-console/organizations/tools/exposed-passwords-report.component.ts +++ b/apps/web/src/app/admin-console/organizations/tools/exposed-passwords-report.component.ts @@ -4,6 +4,7 @@ import { ActivatedRoute } from "@angular/router"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -27,11 +28,20 @@ export class ExposedPasswordsReportComponent extends BaseExposedPasswordsReportC organizationService: OrganizationService, private route: ActivatedRoute, passwordRepromptService: PasswordRepromptService, + i18nService: I18nService, ) { - super(cipherService, auditService, organizationService, modalService, passwordRepromptService); + super( + cipherService, + auditService, + organizationService, + modalService, + passwordRepromptService, + i18nService, + ); } async ngOnInit() { + this.isAdminConsoleActive = true; // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe this.route.parent.parent.params.subscribe(async (params) => { this.organization = await this.organizationService.get(params.organizationId); diff --git a/apps/web/src/app/admin-console/organizations/tools/inactive-two-factor-report.component.ts b/apps/web/src/app/admin-console/organizations/tools/inactive-two-factor-report.component.ts index 906451397f..67d4e963b0 100644 --- a/apps/web/src/app/admin-console/organizations/tools/inactive-two-factor-report.component.ts +++ b/apps/web/src/app/admin-console/organizations/tools/inactive-two-factor-report.component.ts @@ -3,6 +3,7 @@ import { ActivatedRoute } from "@angular/router"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -24,11 +25,20 @@ export class InactiveTwoFactorReportComponent extends BaseInactiveTwoFactorRepor logService: LogService, passwordRepromptService: PasswordRepromptService, organizationService: OrganizationService, + i18nService: I18nService, ) { - super(cipherService, organizationService, modalService, logService, passwordRepromptService); + super( + cipherService, + organizationService, + modalService, + logService, + passwordRepromptService, + i18nService, + ); } async ngOnInit() { + this.isAdminConsoleActive = true; // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe this.route.parent.parent.params.subscribe(async (params) => { this.organization = await this.organizationService.get(params.organizationId); diff --git a/apps/web/src/app/admin-console/organizations/tools/reused-passwords-report.component.ts b/apps/web/src/app/admin-console/organizations/tools/reused-passwords-report.component.ts index 03418a7279..c8ceb023af 100644 --- a/apps/web/src/app/admin-console/organizations/tools/reused-passwords-report.component.ts +++ b/apps/web/src/app/admin-console/organizations/tools/reused-passwords-report.component.ts @@ -3,6 +3,7 @@ import { ActivatedRoute } from "@angular/router"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -25,11 +26,13 @@ export class ReusedPasswordsReportComponent extends BaseReusedPasswordsReportCom private route: ActivatedRoute, organizationService: OrganizationService, passwordRepromptService: PasswordRepromptService, + i18nService: I18nService, ) { - super(cipherService, organizationService, modalService, passwordRepromptService); + super(cipherService, organizationService, modalService, passwordRepromptService, i18nService); } async ngOnInit() { + this.isAdminConsoleActive = true; // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe this.route.parent.parent.params.subscribe(async (params) => { this.organization = await this.organizationService.get(params.organizationId); diff --git a/apps/web/src/app/admin-console/organizations/tools/unsecured-websites-report.component.ts b/apps/web/src/app/admin-console/organizations/tools/unsecured-websites-report.component.ts index 6e1f38e645..2a905b3665 100644 --- a/apps/web/src/app/admin-console/organizations/tools/unsecured-websites-report.component.ts +++ b/apps/web/src/app/admin-console/organizations/tools/unsecured-websites-report.component.ts @@ -3,6 +3,7 @@ import { ActivatedRoute } from "@angular/router"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { PasswordRepromptService } from "@bitwarden/vault"; @@ -22,11 +23,13 @@ export class UnsecuredWebsitesReportComponent extends BaseUnsecuredWebsitesRepor private route: ActivatedRoute, organizationService: OrganizationService, passwordRepromptService: PasswordRepromptService, + i18nService: I18nService, ) { - super(cipherService, organizationService, modalService, passwordRepromptService); + super(cipherService, organizationService, modalService, passwordRepromptService, i18nService); } async ngOnInit() { + this.isAdminConsoleActive = true; // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe this.route.parent.parent.params.subscribe(async (params) => { this.organization = await this.organizationService.get(params.organizationId); diff --git a/apps/web/src/app/admin-console/organizations/tools/weak-passwords-report.component.ts b/apps/web/src/app/admin-console/organizations/tools/weak-passwords-report.component.ts index a79691c01b..8820e596e3 100644 --- a/apps/web/src/app/admin-console/organizations/tools/weak-passwords-report.component.ts +++ b/apps/web/src/app/admin-console/organizations/tools/weak-passwords-report.component.ts @@ -3,6 +3,7 @@ import { ActivatedRoute } from "@angular/router"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; @@ -27,6 +28,7 @@ export class WeakPasswordsReportComponent extends BaseWeakPasswordsReportCompone private route: ActivatedRoute, organizationService: OrganizationService, passwordRepromptService: PasswordRepromptService, + i18nService: I18nService, ) { super( cipherService, @@ -34,10 +36,12 @@ export class WeakPasswordsReportComponent extends BaseWeakPasswordsReportCompone organizationService, modalService, passwordRepromptService, + i18nService, ); } async ngOnInit() { + this.isAdminConsoleActive = true; // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe this.route.parent.parent.params.subscribe(async (params) => { this.organization = await this.organizationService.get(params.organizationId); diff --git a/apps/web/src/app/tools/reports/pages/cipher-report.component.ts b/apps/web/src/app/tools/reports/pages/cipher-report.component.ts index 0d67b7a769..041307122b 100644 --- a/apps/web/src/app/tools/reports/pages/cipher-report.component.ts +++ b/apps/web/src/app/tools/reports/pages/cipher-report.component.ts @@ -1,9 +1,11 @@ -import { Directive, ViewChild, ViewContainerRef } from "@angular/core"; -import { Observable } from "rxjs"; +import { Directive, ViewChild, ViewContainerRef, OnDestroy } from "@angular/core"; +import { BehaviorSubject, Observable, Subject, takeUntil } from "rxjs"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { PasswordRepromptService } from "@bitwarden/vault"; @@ -12,27 +14,111 @@ import { AddEditComponent } from "../../../vault/individual-vault/add-edit.compo import { AddEditComponent as OrgAddEditComponent } from "../../../vault/org-vault/add-edit.component"; @Directive() -export class CipherReportComponent { +export class CipherReportComponent implements OnDestroy { @ViewChild("cipherAddEdit", { read: ViewContainerRef, static: true }) cipherAddEditModalRef: ViewContainerRef; + isAdminConsoleActive = false; loading = false; hasLoaded = false; ciphers: CipherView[] = []; + allCiphers: CipherView[] = []; organization: Organization; + organizations: Organization[]; organizations$: Observable<Organization[]>; + filterStatus: any = [0]; + showFilterToggle: boolean = false; + vaultMsg: string = "vault"; + currentFilterStatus: number | string; + protected filterOrgStatus$ = new BehaviorSubject<number | string>(0); + private destroyed$: Subject<void> = new Subject(); + constructor( + protected cipherService: CipherService, private modalService: ModalService, protected passwordRepromptService: PasswordRepromptService, protected organizationService: OrganizationService, + protected i18nService: I18nService, ) { this.organizations$ = this.organizationService.organizations$; + this.organizations$.pipe(takeUntil(this.destroyed$)).subscribe((orgs) => { + this.organizations = orgs; + }); + } + + ngOnDestroy(): void { + this.destroyed$.next(); + this.destroyed$.complete(); + } + + getName(filterId: string | number) { + let orgName: any; + + if (filterId === 0) { + orgName = this.i18nService.t("all"); + } else if (filterId === 1) { + orgName = this.i18nService.t("me"); + } else { + this.organizations.filter((org: Organization) => { + if (org.id === filterId) { + orgName = org.name; + return org; + } + }); + } + return orgName; + } + + getCount(filterId: string | number) { + let orgFilterStatus: any; + let cipherCount; + + if (filterId === 0) { + cipherCount = this.allCiphers.length; + } else if (filterId === 1) { + cipherCount = this.allCiphers.filter((c: any) => c.orgFilterStatus === null).length; + } else { + this.organizations.filter((org: Organization) => { + if (org.id === filterId) { + orgFilterStatus = org.id; + return org; + } + }); + cipherCount = this.allCiphers.filter( + (c: any) => c.orgFilterStatus === orgFilterStatus, + ).length; + } + return cipherCount; + } + + async filterOrgToggle(status: any) { + this.currentFilterStatus = status; + await this.setCiphers(); + if (status === 0) { + return; + } else if (status === 1) { + this.ciphers = this.ciphers.filter((c: any) => c.orgFilterStatus == null); + } else { + this.ciphers = this.ciphers.filter((c: any) => c.orgFilterStatus === status); + } } async load() { this.loading = true; - await this.setCiphers(); + // when a user fixes an item in a report we want to persist the filter they had + // if they fix the last item of that filter we will go back to the "All" filter + if (this.currentFilterStatus) { + if (this.ciphers.length > 2) { + this.filterOrgStatus$.next(this.currentFilterStatus); + await this.filterOrgToggle(this.currentFilterStatus); + } else { + this.filterOrgStatus$.next(0); + await this.filterOrgToggle(0); + } + } else { + await this.setCiphers(); + } this.loading = false; this.hasLoaded = true; } @@ -76,7 +162,7 @@ export class CipherReportComponent { } protected async setCiphers() { - this.ciphers = []; + this.allCiphers = []; } protected async repromptCipher(c: CipherView) { @@ -85,4 +171,32 @@ export class CipherReportComponent { (await this.passwordRepromptService.showPasswordPrompt()) ); } + + protected async getAllCiphers(): Promise<CipherView[]> { + return await this.cipherService.getAllDecrypted(); + } + + protected filterCiphersByOrg(ciphersList: CipherView[]) { + this.allCiphers = [...ciphersList]; + + this.ciphers = ciphersList.map((ciph: any) => { + ciph.orgFilterStatus = ciph.organizationId; + + if (this.filterStatus.indexOf(ciph.organizationId) === -1 && ciph.organizationId != null) { + this.filterStatus.push(ciph.organizationId); + } else if (this.filterStatus.indexOf(1) === -1 && ciph.organizationId == null) { + this.filterStatus.splice(1, 0, 1); + } + return ciph; + }); + + if (this.filterStatus.length > 2) { + this.showFilterToggle = true; + this.vaultMsg = "vaults"; + } else { + // If a user fixes an item and there is only one item left remove the filter toggle and change the vault message to singular + this.showFilterToggle = false; + this.vaultMsg = "vault"; + } + } } diff --git a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.html b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.html index af80e2e62b..30801a42fd 100644 --- a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.html +++ b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.html @@ -11,9 +11,32 @@ </app-callout> <ng-container *ngIf="ciphers.length"> <app-callout type="danger" title="{{ 'exposedPasswordsFound' | i18n }}" [useAlertRole]="true"> - {{ "exposedPasswordsFoundDesc" | i18n: (ciphers.length | number) }} + {{ "exposedPasswordsFoundReportDesc" | i18n: (ciphers.length | number) : vaultMsg }} </app-callout> + <bit-toggle-group + *ngIf="showFilterToggle && !isAdminConsoleActive" + [selected]="filterOrgStatus$ | async" + (selectedChange)="filterOrgToggle($event)" + [attr.aria-label]="'addAccessFilter' | i18n" + > + <ng-container *ngFor="let status of filterStatus"> + <bit-toggle [value]="status"> + {{ getName(status) }} + <span bitBadge variant="info"> {{ getCount(status) }} </span> + </bit-toggle> + </ng-container> + </bit-toggle-group> <table class="table table-hover table-list table-ciphers"> + <thead + class="tw-border-0 tw-border-b-2 tw-border-solid tw-border-secondary-300 tw-font-bold tw-text-muted" + *ngIf="!isAdminConsoleActive" + > + <tr> + <th></th> + <th>{{ "name" | i18n }}</th> + <th>{{ "owner" | i18n }}</th> + </tr> + </thead> <tbody> <tr *ngFor="let c of ciphers"> <td class="table-list-icon"> diff --git a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.spec.ts index d1cf89eb08..7b73ad8305 100644 --- a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.spec.ts +++ b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.spec.ts @@ -1,6 +1,7 @@ // eslint-disable-next-line no-restricted-imports import { ComponentFixture, TestBed } from "@angular/core/testing"; import { mock, MockProxy } from "jest-mock-extended"; +import { of } from "rxjs"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { ModalService } from "@bitwarden/angular/services/modal.service"; @@ -17,9 +18,12 @@ describe("ExposedPasswordsReportComponent", () => { let component: ExposedPasswordsReportComponent; let fixture: ComponentFixture<ExposedPasswordsReportComponent>; let auditService: MockProxy<AuditService>; + let organizationService: MockProxy<OrganizationService>; beforeEach(() => { auditService = mock<AuditService>(); + organizationService = mock<OrganizationService>(); + organizationService.organizations$ = of([]); // 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 TestBed.configureTestingModule({ @@ -35,7 +39,7 @@ describe("ExposedPasswordsReportComponent", () => { }, { provide: OrganizationService, - useValue: mock<OrganizationService>(), + useValue: organizationService, }, { provide: ModalService, diff --git a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.ts b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.ts index 631e9ef8a8..39414487d7 100644 --- a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.ts +++ b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.ts @@ -3,6 +3,7 @@ import { Component, OnInit } from "@angular/core"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -24,8 +25,9 @@ export class ExposedPasswordsReportComponent extends CipherReportComponent imple protected organizationService: OrganizationService, modalService: ModalService, passwordRepromptService: PasswordRepromptService, + i18nService: I18nService, ) { - super(modalService, passwordRepromptService, organizationService); + super(cipherService, modalService, passwordRepromptService, organizationService, i18nService); } async ngOnInit() { @@ -36,7 +38,9 @@ export class ExposedPasswordsReportComponent extends CipherReportComponent imple const allCiphers = await this.getAllCiphers(); const exposedPasswordCiphers: CipherView[] = []; const promises: Promise<void>[] = []; - allCiphers.forEach((ciph) => { + this.filterStatus = [0]; + + allCiphers.forEach((ciph: any) => { const { type, login, isDeleted, edit, viewPassword, id } = ciph; if ( type !== CipherType.Login || @@ -48,6 +52,7 @@ export class ExposedPasswordsReportComponent extends CipherReportComponent imple ) { return; } + const promise = this.auditService.passwordLeaked(login.password).then((exposedCount) => { if (exposedCount > 0) { exposedPasswordCiphers.push(ciph); @@ -57,11 +62,8 @@ export class ExposedPasswordsReportComponent extends CipherReportComponent imple promises.push(promise); }); await Promise.all(promises); - this.ciphers = [...exposedPasswordCiphers]; - } - protected getAllCiphers(): Promise<CipherView[]> { - return this.cipherService.getAllDecrypted(); + this.filterCiphersByOrg(exposedPasswordCiphers); } protected canManageCipher(c: CipherView): boolean { diff --git a/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.html b/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.html index d81fc2d413..ae03a3bcb8 100644 --- a/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.html +++ b/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.html @@ -16,9 +16,32 @@ </app-callout> <ng-container *ngIf="ciphers.length"> <app-callout type="danger" title="{{ 'inactive2faFound' | i18n }}"> - {{ "inactive2faFoundDesc" | i18n: (ciphers.length | number) }} + {{ "inactive2faFoundReportDesc" | i18n: (ciphers.length | number) : vaultMsg }} </app-callout> + <bit-toggle-group + *ngIf="showFilterToggle && !isAdminConsoleActive" + [selected]="filterOrgStatus$ | async" + (selectedChange)="filterOrgToggle($event)" + [attr.aria-label]="'addAccessFilter' | i18n" + > + <ng-container *ngFor="let status of filterStatus"> + <bit-toggle [value]="status"> + {{ getName(status) }} + <span bitBadge variant="info"> {{ getCount(status) }} </span> + </bit-toggle> + </ng-container> + </bit-toggle-group> <table class="table table-hover table-list table-ciphers"> + <thead + class="tw-border-0 tw-border-b-2 tw-border-solid tw-border-secondary-300 tw-font-bold tw-text-muted" + *ngIf="!isAdminConsoleActive" + > + <tr> + <th></th> + <th>{{ "name" | i18n }}</th> + <th>{{ "owner" | i18n }}</th> + </tr> + </thead> <tbody> <tr *ngFor="let c of ciphers"> <td class="table-list-icon"> diff --git a/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.spec.ts index 97321480fa..528f6306e0 100644 --- a/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.spec.ts +++ b/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.spec.ts @@ -1,6 +1,7 @@ // eslint-disable-next-line no-restricted-imports import { ComponentFixture, TestBed } from "@angular/core/testing"; -import { mock } from "jest-mock-extended"; +import { MockProxy, mock } from "jest-mock-extended"; +import { of } from "rxjs"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { ModalService } from "@bitwarden/angular/services/modal.service"; @@ -16,8 +17,11 @@ import { cipherData } from "./reports-ciphers.mock"; describe("InactiveTwoFactorReportComponent", () => { let component: InactiveTwoFactorReportComponent; let fixture: ComponentFixture<InactiveTwoFactorReportComponent>; + let organizationService: MockProxy<OrganizationService>; beforeEach(() => { + organizationService = mock<OrganizationService>(); + organizationService.organizations$ = of([]); // 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 TestBed.configureTestingModule({ @@ -29,7 +33,7 @@ describe("InactiveTwoFactorReportComponent", () => { }, { provide: OrganizationService, - useValue: mock<OrganizationService>(), + useValue: organizationService, }, { provide: ModalService, diff --git a/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.ts b/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.ts index 15b79981b6..956607c8fb 100644 --- a/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.ts +++ b/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.ts @@ -2,6 +2,7 @@ import { Component, OnInit } from "@angular/core"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -26,8 +27,9 @@ export class InactiveTwoFactorReportComponent extends CipherReportComponent impl modalService: ModalService, private logService: LogService, passwordRepromptService: PasswordRepromptService, + i18nService: I18nService, ) { - super(modalService, passwordRepromptService, organizationService); + super(cipherService, modalService, passwordRepromptService, organizationService, i18nService); } async ngOnInit() { @@ -45,6 +47,7 @@ export class InactiveTwoFactorReportComponent extends CipherReportComponent impl const allCiphers = await this.getAllCiphers(); const inactive2faCiphers: CipherView[] = []; const docs = new Map<string, string>(); + this.filterStatus = [0]; allCiphers.forEach((ciph) => { const { type, login, isDeleted, edit, id, viewPassword } = ciph; @@ -58,6 +61,7 @@ export class InactiveTwoFactorReportComponent extends CipherReportComponent impl ) { return; } + for (let i = 0; i < login.uris.length; i++) { const u = login.uris[i]; if (u.uri != null && u.uri !== "") { @@ -75,15 +79,12 @@ export class InactiveTwoFactorReportComponent extends CipherReportComponent impl } } }); - this.ciphers = [...inactive2faCiphers]; + + this.filterCiphersByOrg(inactive2faCiphers); this.cipherDocs = docs; } } - protected getAllCiphers(): Promise<CipherView[]> { - return this.cipherService.getAllDecrypted(); - } - private async load2fa() { if (this.services.size > 0) { return; diff --git a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.html b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.html index cde2e59ea8..549773ba8c 100644 --- a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.html +++ b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.html @@ -16,9 +16,34 @@ </app-callout> <ng-container *ngIf="ciphers.length"> <app-callout type="danger" title="{{ 'reusedPasswordsFound' | i18n }}"> - {{ "reusedPasswordsFoundDesc" | i18n: (ciphers.length | number) }} + {{ "reusedPasswordsFoundReportDesc" | i18n: (ciphers.length | number) : vaultMsg }} </app-callout> + + <bit-toggle-group + *ngIf="showFilterToggle && !isAdminConsoleActive" + [selected]="filterOrgStatus$ | async" + (selectedChange)="filterOrgToggle($event)" + [attr.aria-label]="'addAccessFilter' | i18n" + > + <ng-container *ngFor="let status of filterStatus"> + <bit-toggle [value]="status"> + {{ getName(status) }} + <span bitBadge variant="info"> {{ getCount(status) }} </span> + </bit-toggle> + </ng-container> + </bit-toggle-group> + <table class="table table-hover table-list table-ciphers"> + <thead + class="tw-border-0 tw-border-b-2 tw-border-solid tw-border-secondary-300 tw-font-bold tw-text-muted" + *ngIf="!isAdminConsoleActive" + > + <tr> + <th></th> + <th>{{ "name" | i18n }}</th> + <th>{{ "owner" | i18n }}</th> + </tr> + </thead> <tbody> <tr *ngFor="let c of ciphers"> <td class="table-list-icon"> diff --git a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.spec.ts index 450e42805a..29e20c11af 100644 --- a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.spec.ts +++ b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.spec.ts @@ -1,6 +1,7 @@ // eslint-disable-next-line no-restricted-imports import { ComponentFixture, TestBed } from "@angular/core/testing"; -import { mock } from "jest-mock-extended"; +import { MockProxy, mock } from "jest-mock-extended"; +import { of } from "rxjs"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { ModalService } from "@bitwarden/angular/services/modal.service"; @@ -15,8 +16,11 @@ import { ReusedPasswordsReportComponent } from "./reused-passwords-report.compon describe("ReusedPasswordsReportComponent", () => { let component: ReusedPasswordsReportComponent; let fixture: ComponentFixture<ReusedPasswordsReportComponent>; + let organizationService: MockProxy<OrganizationService>; beforeEach(() => { + organizationService = mock<OrganizationService>(); + organizationService.organizations$ = of([]); // 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 TestBed.configureTestingModule({ @@ -28,7 +32,7 @@ describe("ReusedPasswordsReportComponent", () => { }, { provide: OrganizationService, - useValue: mock<OrganizationService>(), + useValue: organizationService, }, { provide: ModalService, diff --git a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.ts b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.ts index f785186c15..cbc2ea11b5 100644 --- a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.ts +++ b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.ts @@ -2,6 +2,7 @@ import { Component, OnInit } from "@angular/core"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -22,8 +23,9 @@ export class ReusedPasswordsReportComponent extends CipherReportComponent implem protected organizationService: OrganizationService, modalService: ModalService, passwordRepromptService: PasswordRepromptService, + i18nService: I18nService, ) { - super(modalService, passwordRepromptService, organizationService); + super(cipherService, modalService, passwordRepromptService, organizationService, i18nService); } async ngOnInit() { @@ -34,6 +36,8 @@ export class ReusedPasswordsReportComponent extends CipherReportComponent implem const allCiphers = await this.getAllCiphers(); const ciphersWithPasswords: CipherView[] = []; this.passwordUseMap = new Map<string, number>(); + this.filterStatus = [0]; + allCiphers.forEach((ciph) => { const { type, login, isDeleted, edit, viewPassword } = ciph; if ( @@ -46,6 +50,7 @@ export class ReusedPasswordsReportComponent extends CipherReportComponent implem ) { return; } + ciphersWithPasswords.push(ciph); if (this.passwordUseMap.has(login.password)) { this.passwordUseMap.set(login.password, this.passwordUseMap.get(login.password) + 1); @@ -57,11 +62,8 @@ export class ReusedPasswordsReportComponent extends CipherReportComponent implem (c) => this.passwordUseMap.has(c.login.password) && this.passwordUseMap.get(c.login.password) > 1, ); - this.ciphers = reusedPasswordCiphers; - } - protected getAllCiphers(): Promise<CipherView[]> { - return this.cipherService.getAllDecrypted(); + this.filterCiphersByOrg(reusedPasswordCiphers); } protected canManageCipher(c: CipherView): boolean { diff --git a/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.html b/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.html index 616bdbba0b..ced0ff9731 100644 --- a/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.html +++ b/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.html @@ -16,9 +16,33 @@ </app-callout> <ng-container *ngIf="ciphers.length"> <app-callout type="danger" title="{{ 'unsecuredWebsitesFound' | i18n }}"> - {{ "unsecuredWebsitesFoundDesc" | i18n: (ciphers.length | number) }} + {{ "unsecuredWebsitesFoundReportDesc" | i18n: (ciphers.length | number) : vaultMsg }} </app-callout> + + <bit-toggle-group + *ngIf="showFilterToggle && !isAdminConsoleActive" + [selected]="filterOrgStatus$ | async" + (selectedChange)="filterOrgToggle($event)" + [attr.aria-label]="'addAccessFilter' | i18n" + > + <ng-container *ngFor="let status of filterStatus"> + <bit-toggle [value]="status"> + {{ getName(status) }} + <span bitBadge variant="info"> {{ getCount(status) }} </span> + </bit-toggle> + </ng-container> + </bit-toggle-group> <table class="table table-hover table-list table-ciphers"> + <thead + class="tw-border-0 tw-border-b-2 tw-border-solid tw-border-secondary-300 tw-font-bold tw-text-muted" + *ngIf="!isAdminConsoleActive" + > + <tr> + <th></th> + <th>{{ "name" | i18n }}</th> + <th>{{ "owner" | i18n }}</th> + </tr> + </thead> <tbody> <tr *ngFor="let c of ciphers"> <td class="table-list-icon"> diff --git a/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.spec.ts index 5cdf640c55..3b7c6d350f 100644 --- a/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.spec.ts +++ b/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.spec.ts @@ -1,6 +1,7 @@ // eslint-disable-next-line no-restricted-imports import { ComponentFixture, TestBed } from "@angular/core/testing"; -import { mock } from "jest-mock-extended"; +import { MockProxy, mock } from "jest-mock-extended"; +import { of } from "rxjs"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { ModalService } from "@bitwarden/angular/services/modal.service"; @@ -15,8 +16,11 @@ import { UnsecuredWebsitesReportComponent } from "./unsecured-websites-report.co describe("UnsecuredWebsitesReportComponent", () => { let component: UnsecuredWebsitesReportComponent; let fixture: ComponentFixture<UnsecuredWebsitesReportComponent>; + let organizationService: MockProxy<OrganizationService>; beforeEach(() => { + organizationService = mock<OrganizationService>(); + organizationService.organizations$ = of([]); // 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 TestBed.configureTestingModule({ @@ -28,7 +32,7 @@ describe("UnsecuredWebsitesReportComponent", () => { }, { provide: OrganizationService, - useValue: mock<OrganizationService>(), + useValue: organizationService, }, { provide: ModalService, diff --git a/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.ts b/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.ts index 2de70e928b..769eb058cd 100644 --- a/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.ts +++ b/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.ts @@ -2,9 +2,9 @@ import { Component, OnInit } from "@angular/core"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; -import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { PasswordRepromptService } from "@bitwarden/vault"; import { CipherReportComponent } from "./cipher-report.component"; @@ -21,8 +21,9 @@ export class UnsecuredWebsitesReportComponent extends CipherReportComponent impl protected organizationService: OrganizationService, modalService: ModalService, passwordRepromptService: PasswordRepromptService, + i18nService: I18nService, ) { - super(modalService, passwordRepromptService, organizationService); + super(cipherService, modalService, passwordRepromptService, organizationService, i18nService); } async ngOnInit() { @@ -31,18 +32,15 @@ export class UnsecuredWebsitesReportComponent extends CipherReportComponent impl async setCiphers() { const allCiphers = await this.getAllCiphers(); + this.filterStatus = [0]; const unsecuredCiphers = allCiphers.filter((c) => { if (c.type !== CipherType.Login || !c.login.hasUris || c.isDeleted) { return false; } - return c.login.uris.some((u) => u.uri != null && u.uri.indexOf("http://") === 0); - }); - this.ciphers = unsecuredCiphers.filter( - (c) => (!this.organization && c.edit) || (this.organization && !c.edit), - ); - } - protected getAllCiphers(): Promise<CipherView[]> { - return this.cipherService.getAllDecrypted(); + return c.login.uris.some((u: any) => u.uri != null && u.uri.indexOf("http://") === 0); + }); + + this.filterCiphersByOrg(unsecuredCiphers); } } diff --git a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.html b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.html index b4c77b2fa1..a943c8c29e 100644 --- a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.html +++ b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.html @@ -16,9 +16,32 @@ </app-callout> <ng-container *ngIf="ciphers.length"> <app-callout type="danger" title="{{ 'weakPasswordsFound' | i18n }}"> - {{ "weakPasswordsFoundDesc" | i18n: (ciphers.length | number) }} + {{ "weakPasswordsFoundReportDesc" | i18n: (ciphers.length | number) : vaultMsg }} </app-callout> + <bit-toggle-group + *ngIf="showFilterToggle && !isAdminConsoleActive" + [selected]="filterOrgStatus$ | async" + (selectedChange)="filterOrgToggle($event)" + [attr.aria-label]="'addAccessFilter' | i18n" + > + <ng-container *ngFor="let status of filterStatus"> + <bit-toggle [value]="status"> + {{ getName(status) }} + <span bitBadge variant="info"> {{ getCount(status) }} </span> + </bit-toggle> + </ng-container> + </bit-toggle-group> <table class="table table-hover table-list table-ciphers"> + <thead + class="tw-border-0 tw-border-b-2 tw-border-solid tw-border-secondary-300 tw-font-bold tw-text-muted" + *ngIf="!isAdminConsoleActive" + > + <tr> + <th></th> + <th>{{ "name" | i18n }}</th> + <th>{{ "owner" | i18n }}</th> + </tr> + </thead> <tbody> <tr *ngFor="let c of ciphers"> <td class="table-list-icon"> diff --git a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.spec.ts index f1446c4209..dbc367b108 100644 --- a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.spec.ts +++ b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.spec.ts @@ -1,6 +1,7 @@ // eslint-disable-next-line no-restricted-imports import { ComponentFixture, TestBed } from "@angular/core/testing"; import { mock, MockProxy } from "jest-mock-extended"; +import { of } from "rxjs"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { ModalService } from "@bitwarden/angular/services/modal.service"; @@ -17,9 +18,12 @@ describe("WeakPasswordsReportComponent", () => { let component: WeakPasswordsReportComponent; let fixture: ComponentFixture<WeakPasswordsReportComponent>; let passwordStrengthService: MockProxy<PasswordStrengthServiceAbstraction>; + let organizationService: MockProxy<OrganizationService>; beforeEach(() => { passwordStrengthService = mock<PasswordStrengthServiceAbstraction>(); + organizationService = mock<OrganizationService>(); + organizationService.organizations$ = of([]); // 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 TestBed.configureTestingModule({ @@ -35,7 +39,7 @@ describe("WeakPasswordsReportComponent", () => { }, { provide: OrganizationService, - useValue: mock<OrganizationService>(), + useValue: organizationService, }, { provide: ModalService, diff --git a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.ts b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.ts index a7ed119e19..4d179b58f3 100644 --- a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.ts +++ b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.ts @@ -2,6 +2,7 @@ import { Component, OnInit } from "@angular/core"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -29,8 +30,9 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen protected organizationService: OrganizationService, modalService: ModalService, passwordRepromptService: PasswordRepromptService, + i18nService: I18nService, ) { - super(modalService, passwordRepromptService, organizationService); + super(cipherService, modalService, passwordRepromptService, organizationService, i18nService); } async ngOnInit() { @@ -38,7 +40,10 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen } async setCiphers() { - const allCiphers = await this.getAllCiphers(); + const allCiphers: any = await this.getAllCiphers(); + this.passwordStrengthCache = new Map<string, number>(); + this.weakPasswordCiphers = []; + this.filterStatus = [0]; this.findWeakPasswords(allCiphers); } @@ -55,6 +60,7 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen ) { return; } + const hasUserName = this.isUserNameNotEmpty(ciph); const cacheKey = this.getCacheKey(ciph); if (!this.passwordStrengthCache.has(cacheKey)) { @@ -87,6 +93,7 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen this.passwordStrengthCache.set(cacheKey, result.score); } const score = this.passwordStrengthCache.get(cacheKey); + if (score != null && score <= 2) { this.passwordStrengthMap.set(id, this.scoreKey(score)); this.weakPasswordCiphers.push(ciph); @@ -98,11 +105,8 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen this.passwordStrengthCache.get(this.getCacheKey(b)) ); }); - this.ciphers = [...this.weakPasswordCiphers]; - } - protected getAllCiphers(): Promise<CipherView[]> { - return this.cipherService.getAllDecrypted(); + this.filterCiphersByOrg(this.weakPasswordCiphers); } protected canManageCipher(c: CipherView): boolean { diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 3a590622b8..e37ccaf536 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Unsecured websites found" }, - "unsecuredWebsitesFoundDesc": { - "message": "We found $COUNT$ items in your vault with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Logins without two-step login found" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Exposed passwords found" }, - "exposedPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault that have passwords that were exposed in known data breaches. You should change them to use a new password.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Weak passwords found" }, - "weakPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault with passwords that are not strong. You should update them to use stronger passwords.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Reused passwords found" }, - "reusedPasswordsFoundDesc": { - "message": "We found $COUNT$ passwords that are being reused in your vault. You should change them to a unique value.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, From 89df0e4fade94ee118723a02ebb3be6544aca131 Mon Sep 17 00:00:00 2001 From: Shane Melton <smelton@bitwarden.com> Date: Wed, 1 May 2024 08:40:12 -0700 Subject: [PATCH 331/351] [AC-1623] Introduce Clone option to individual vault for organization items (#8608) * [AC-1623] Remove cloneableOrganizationCiphers property and update canClone to reflect new clone permission logic * [AC-1623] Remove allowOwnershipAssignment override in orgVault as the same restrictions apply to both vaults * [AC-1623] Ensure ownershipOptions are restricted for non-admins when cloning an org cipher item --- .../vault-items/vault-items.component.ts | 26 +++++++++++++++---- .../individual-vault/vault.component.html | 1 - .../app/vault/org-vault/add-edit.component.ts | 16 ------------ .../app/vault/org-vault/vault.component.html | 1 - .../vault/components/add-edit.component.ts | 10 +++++++ 5 files changed, 31 insertions(+), 23 deletions(-) diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts index 8b6ead33be..dd18ca0879 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts @@ -32,7 +32,6 @@ export class VaultItemsComponent { @Input() showCollections: boolean; @Input() showGroups: boolean; @Input() useEvents: boolean; - @Input() cloneableOrganizationCiphers: boolean; @Input() showPremiumFeatures: boolean; @Input() showBulkMove: boolean; @Input() showBulkTrashOptions: boolean; @@ -160,10 +159,27 @@ export class VaultItemsComponent { } protected canClone(vaultItem: VaultItem) { - return ( - (vaultItem.cipher.organizationId && this.cloneableOrganizationCiphers) || - vaultItem.cipher.organizationId == null - ); + if (vaultItem.cipher.organizationId == null) { + return true; + } + + const org = this.allOrganizations.find((o) => o.id === vaultItem.cipher.organizationId); + + // Admins and custom users can always clone in the Org Vault + if (this.viewingOrgVault && (org.isAdmin || org.permissions.editAnyCollection)) { + return true; + } + + // Check if the cipher belongs to a collection with canManage permission + const orgCollections = this.allCollections.filter((c) => c.organizationId === org.id); + + for (const collection of orgCollections) { + if (vaultItem.cipher.collectionIds.includes(collection.id) && collection.manage) { + return true; + } + } + + return false; } private refreshItems() { diff --git a/apps/web/src/app/vault/individual-vault/vault.component.html b/apps/web/src/app/vault/individual-vault/vault.component.html index 003066dadd..3f95665f37 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.html +++ b/apps/web/src/app/vault/individual-vault/vault.component.html @@ -47,7 +47,6 @@ [showBulkMove]="showBulkMove" [showBulkTrashOptions]="filter.type === 'trash'" [useEvents]="false" - [cloneableOrganizationCiphers]="false" [showAdminActions]="false" (onEvent)="onVaultItemsEvent($event)" [flexibleCollectionsV1Enabled]="flexibleCollectionsV1Enabled$ | async" diff --git a/apps/web/src/app/vault/org-vault/add-edit.component.ts b/apps/web/src/app/vault/org-vault/add-edit.component.ts index c4213989c6..01e4dbaadf 100644 --- a/apps/web/src/app/vault/org-vault/add-edit.component.ts +++ b/apps/web/src/app/vault/org-vault/add-edit.component.ts @@ -81,22 +81,6 @@ export class AddEditComponent extends BaseAddEditComponent { ); } - protected allowOwnershipAssignment() { - if ( - this.ownershipOptions != null && - (this.ownershipOptions.length > 1 || !this.allowPersonal) - ) { - if (this.organization != null) { - return ( - this.cloneMode && this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled) - ); - } else { - return !this.editMode || this.cloneMode; - } - } - return false; - } - protected loadCollections() { if (!this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) { return super.loadCollections(); diff --git a/apps/web/src/app/vault/org-vault/vault.component.html b/apps/web/src/app/vault/org-vault/vault.component.html index bcbd56630c..f815fccb21 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.html +++ b/apps/web/src/app/vault/org-vault/vault.component.html @@ -48,7 +48,6 @@ [showBulkMove]="false" [showBulkTrashOptions]="filter.type === 'trash'" [useEvents]="organization?.useEvents" - [cloneableOrganizationCiphers]="true" [showAdminActions]="true" (onEvent)="onVaultItemsEvent($event)" [showBulkEditCollectionAccess]="organization?.flexibleCollections" diff --git a/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts index 177b4289f4..0397a7a663 100644 --- a/libs/angular/src/vault/components/add-edit.component.ts +++ b/libs/angular/src/vault/components/add-edit.component.ts @@ -289,6 +289,16 @@ export class AddEditComponent implements OnInit, OnDestroy { }); } } + // Only Admins can clone a cipher to different owner + if (this.cloneMode && this.cipher.organizationId != null) { + const cipherOrg = (await firstValueFrom(this.organizationService.memberOrganizations$)).find( + (o) => o.id === this.cipher.organizationId, + ); + + if (cipherOrg != null && !cipherOrg.isAdmin && !cipherOrg.permissions.editAnyCollection) { + this.ownershipOptions = [{ name: cipherOrg.name, value: cipherOrg.id }]; + } + } // We don't want to copy passkeys when we clone a cipher if (this.cloneMode && this.cipher?.login?.hasFido2Credentials) { From a4b3b83c465f73db00aefa3408751d35470c1712 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Wed, 1 May 2024 11:31:03 -0500 Subject: [PATCH 332/351] sort organizations by name within product switcher (#8980) --- .../product-switcher-content.component.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.ts b/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.ts index 0ce98948c9..398105a75f 100644 --- a/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.ts +++ b/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.ts @@ -1,12 +1,13 @@ import { Component, ViewChild } from "@angular/core"; -import { ActivatedRoute, Router } from "@angular/router"; -import { combineLatest, concatMap } from "rxjs"; +import { ActivatedRoute, ParamMap, Router } from "@angular/router"; +import { combineLatest, concatMap, map } from "rxjs"; import { canAccessOrgAdmin, OrganizationService, } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { MenuComponent } from "@bitwarden/components"; type ProductSwitcherItem = { @@ -48,6 +49,13 @@ export class ProductSwitcherContentComponent { this.organizationService.organizations$, this.route.paramMap, ]).pipe( + map(([orgs, paramMap]): [Organization[], ParamMap] => { + return [ + // Sort orgs by name to match the order within the sidebar + orgs.sort((a, b) => a.name.localeCompare(b.name)), + paramMap, + ]; + }), concatMap(async ([orgs, paramMap]) => { const routeOrg = orgs.find((o) => o.id === paramMap.get("organizationId")); // If the active route org doesn't have access to SM, find the first org that does. From af0a884ee8109dd5ee665d19cab25b682bb0503a Mon Sep 17 00:00:00 2001 From: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> Date: Wed, 1 May 2024 11:47:06 -0500 Subject: [PATCH 333/351] [SM-910] Migrate service account -> projects tab to new access policy selector (#8572) * Add view, requests and responses * access policy service update * Add read only support to access policy selector * Migrate service account -> projects tab --- .../models/view/access-policy.view.ts | 9 + .../service-account-projects.component.html | 44 +- .../service-account-projects.component.ts | 217 +++++++--- .../access-policy-selector.component.html | 17 +- .../access-policy-selector.component.ts | 45 +- .../access-policy-selector.service.spec.ts | 1 + .../models/ap-item-value.type.ts | 26 ++ .../models/ap-item-view.type.ts | 33 ++ .../access-policies/access-policy.service.ts | 383 +++++++++--------- ...ervice-account-granted-policies.request.ts | 5 + ...ed-policies-permission-details.response.ts | 15 + ...ject-policy-permission-details.response.ts | 14 + 12 files changed, 531 insertions(+), 278 deletions(-) create mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/requests/service-account-granted-policies.request.ts create mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/service-account-granted-policies-permission-details.response.ts create mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/service-account-project-policy-permission-details.response.ts diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policy.view.ts b/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policy.view.ts index 958fe0d48e..18b6994459 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policy.view.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policy.view.ts @@ -58,3 +58,12 @@ export class ServiceAccountPeopleAccessPoliciesView { userAccessPolicies: UserServiceAccountAccessPolicyView[]; groupAccessPolicies: GroupServiceAccountAccessPolicyView[]; } + +export class ServiceAccountProjectPolicyPermissionDetailsView { + accessPolicy: ServiceAccountProjectAccessPolicyView; + hasPermission: boolean; +} + +export class ServiceAccountGrantedPoliciesView { + grantedProjectPolicies: ServiceAccountProjectPolicyPermissionDetailsView[]; +} 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 b97c5ef114..623542bd33 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,17 +1,27 @@ -<div class="tw-mt-4 tw-w-2/5"> - <p class="tw-mt-6"> - {{ "machineAccountProjectsDescription" | i18n }} - </p> - <sm-access-selector - [rows]="rows$ | async" - granteeType="projects" - [label]="'projects' | i18n" - [hint]="'newSaSelectAccess' | i18n" - [columnTitle]="'projects' | i18n" - [emptyMessage]="'serviceAccountEmptyProjectAccesspolicies' | i18n" - (onCreateAccessPolicies)="handleCreateAccessPolicies($event)" - (onDeleteAccessPolicy)="handleDeleteAccessPolicy($event)" - (onUpdateAccessPolicy)="handleUpdateAccessPolicy($event)" - > - </sm-access-selector> -</div> +<form [formGroup]="formGroup" [bitSubmit]="submit" *ngIf="!loading; else spinner"> + <div class="tw-w-2/5"> + <p class="tw-mt-8" *ngIf="!loading"> + {{ "machineAccountProjectsDescription" | i18n }} + </p> + <sm-access-policy-selector + [loading]="loading" + formControlName="accessPolicies" + [addButtonMode]="true" + [items]="potentialGrantees" + [label]="'projects' | i18n" + [hint]="'newSaSelectAccess' | i18n" + [columnTitle]="'projects' | i18n" + [emptyMessage]="'serviceAccountEmptyProjectAccesspolicies' | i18n" + > + </sm-access-policy-selector> + <button bitButton buttonType="primary" bitFormButton type="submit" class="tw-mt-7"> + {{ "save" | i18n }} + </button> + </div> +</form> + +<ng-template #spinner> + <div class="tw-items-center tw-justify-center tw-pt-64 tw-text-center"> + <i class="bwi bwi-spinner bwi-spin bwi-3x"></i> + </div> +</ng-template> diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/projects/service-account-projects.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/projects/service-account-projects.component.ts index 2fcc10988d..a6f3d720b7 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/projects/service-account-projects.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/projects/service-account-projects.component.ts @@ -1,90 +1,68 @@ -import { Component, OnDestroy, OnInit } from "@angular/core"; +import { ChangeDetectorRef, Component, OnDestroy, OnInit } from "@angular/core"; +import { FormControl, FormGroup } from "@angular/forms"; import { ActivatedRoute } from "@angular/router"; -import { combineLatestWith, map, Observable, startWith, Subject, switchMap, takeUntil } from "rxjs"; +import { combineLatest, Subject, switchMap, takeUntil } from "rxjs"; +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 { SelectItemView } from "@bitwarden/components/src/multi-select/models/select-item-view"; -import { ServiceAccountProjectAccessPolicyView } from "../../models/view/access-policy.view"; -import { AccessPolicyService } from "../../shared/access-policies/access-policy.service"; +import { ServiceAccountGrantedPoliciesView } from "../../models/view/access-policy.view"; import { - AccessSelectorComponent, - AccessSelectorRowView, -} from "../../shared/access-policies/access-selector.component"; + ApItemValueType, + convertToServiceAccountGrantedPoliciesView, +} from "../../shared/access-policies/access-policy-selector/models/ap-item-value.type"; +import { + ApItemViewType, + convertPotentialGranteesToApItemViewType, + convertGrantedPoliciesToAccessPolicyItemViews, +} from "../../shared/access-policies/access-policy-selector/models/ap-item-view.type"; +import { AccessPolicyService } from "../../shared/access-policies/access-policy.service"; @Component({ selector: "sm-service-account-projects", templateUrl: "./service-account-projects.component.html", }) export class ServiceAccountProjectsComponent implements OnInit, OnDestroy { + private currentAccessPolicies: ApItemViewType[]; private destroy$ = new Subject<void>(); - private serviceAccountId: string; private organizationId: string; + private serviceAccountId: string; - protected rows$: Observable<AccessSelectorRowView[]> = - this.accessPolicyService.serviceAccountGrantedPolicyChanges$.pipe( - startWith(null), - combineLatestWith(this.route.params), - switchMap(([_, params]) => - this.accessPolicyService.getGrantedPolicies(params.serviceAccountId, params.organizationId), - ), - map((policies) => { - return policies.map((policy) => { - return { - type: "project", - name: policy.grantedProjectName, - id: policy.grantedProjectId, - accessPolicyId: policy.id, - read: policy.read, - write: policy.write, - icon: AccessSelectorComponent.projectIcon, - static: false, - } as AccessSelectorRowView; - }); - }), - ); + private currentAccessPolicies$ = combineLatest([this.route.params]).pipe( + switchMap(([params]) => + this.accessPolicyService + .getServiceAccountGrantedPolicies(params.organizationId, params.serviceAccountId) + .then((policies) => { + return convertGrantedPoliciesToAccessPolicyItemViews(policies); + }), + ), + ); - protected handleCreateAccessPolicies(selected: SelectItemView[]) { - const serviceAccountProjectAccessPolicyView = selected - .filter((selection) => AccessSelectorComponent.getAccessItemType(selection) === "project") - .map((filtered) => { - const view = new ServiceAccountProjectAccessPolicyView(); - view.serviceAccountId = this.serviceAccountId; - view.grantedProjectId = filtered.id; - view.read = true; - view.write = false; - return view; - }); + private potentialGrantees$ = combineLatest([this.route.params]).pipe( + switchMap(([params]) => + this.accessPolicyService + .getProjectsPotentialGrantees(params.organizationId) + .then((grantees) => { + return convertPotentialGranteesToApItemViewType(grantees); + }), + ), + ); - return this.accessPolicyService.createGrantedPolicies( - this.organizationId, - this.serviceAccountId, - serviceAccountProjectAccessPolicyView, - ); - } + protected formGroup = new FormGroup({ + accessPolicies: new FormControl([] as ApItemValueType[]), + }); - protected async handleUpdateAccessPolicy(policy: AccessSelectorRowView) { - try { - return await this.accessPolicyService.updateAccessPolicy( - AccessSelectorComponent.getBaseAccessPolicyView(policy), - ); - } catch (e) { - this.validationService.showError(e); - } - } - - protected async handleDeleteAccessPolicy(policy: AccessSelectorRowView) { - try { - await this.accessPolicyService.deleteAccessPolicy(policy.accessPolicyId); - } catch (e) { - this.validationService.showError(e); - } - } + protected loading = true; + protected potentialGrantees: ApItemViewType[]; constructor( private route: ActivatedRoute, + private changeDetectorRef: ChangeDetectorRef, private validationService: ValidationService, private accessPolicyService: AccessPolicyService, + private platformUtilsService: PlatformUtilsService, + private i18nService: I18nService, ) {} ngOnInit(): void { @@ -92,10 +70,119 @@ export class ServiceAccountProjectsComponent implements OnInit, OnDestroy { this.organizationId = params.organizationId; this.serviceAccountId = params.serviceAccountId; }); + + combineLatest([this.potentialGrantees$, this.currentAccessPolicies$]) + .pipe(takeUntil(this.destroy$)) + .subscribe(([potentialGrantees, currentAccessPolicies]) => { + this.potentialGrantees = this.getPotentialGrantees( + potentialGrantees, + currentAccessPolicies, + ); + this.setSelected(currentAccessPolicies); + }); } ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); } + + submit = async () => { + if (this.isFormInvalid()) { + return; + } + const formValues = this.getFormValues(); + this.formGroup.disable(); + + try { + const grantedViews = await this.updateServiceAccountGrantedPolicies( + this.organizationId, + this.serviceAccountId, + formValues, + ); + + this.currentAccessPolicies = convertGrantedPoliciesToAccessPolicyItemViews(grantedViews); + + this.platformUtilsService.showToast( + "success", + null, + this.i18nService.t("serviceAccountAccessUpdated"), + ); + } catch (e) { + this.validationService.showError(e); + this.setSelected(this.currentAccessPolicies); + } + this.formGroup.enable(); + }; + + private setSelected(policiesToSelect: ApItemViewType[]) { + this.loading = true; + this.currentAccessPolicies = policiesToSelect; + if (policiesToSelect != undefined) { + // Must detect changes so that AccessSelector @Inputs() are aware of the latest + // potentialGrantees, otherwise no selected values will be patched below + this.changeDetectorRef.detectChanges(); + this.formGroup.patchValue({ + accessPolicies: policiesToSelect.map((m) => ({ + type: m.type, + id: m.id, + permission: m.permission, + readOnly: m.readOnly, + })), + }); + } + this.loading = false; + } + + private isFormInvalid(): boolean { + this.formGroup.markAllAsTouched(); + return this.formGroup.invalid; + } + + private async updateServiceAccountGrantedPolicies( + organizationId: string, + serviceAccountId: string, + selectedPolicies: ApItemValueType[], + ): Promise<ServiceAccountGrantedPoliciesView> { + const grantedViews = convertToServiceAccountGrantedPoliciesView( + serviceAccountId, + selectedPolicies, + ); + return await this.accessPolicyService.putServiceAccountGrantedPolicies( + organizationId, + serviceAccountId, + grantedViews, + ); + } + + private getPotentialGrantees( + potentialGrantees: ApItemViewType[], + currentAccessPolicies: ApItemViewType[], + ) { + // If the user doesn't have access to the project, they won't be in the potentialGrantees list. + // Add them to the potentialGrantees list so they can be selected as read-only. + for (const policy of currentAccessPolicies) { + const exists = potentialGrantees.some((grantee) => grantee.id === policy.id); + if (!exists) { + potentialGrantees.push(policy); + } + } + return potentialGrantees; + } + + private getFormValues(): ApItemValueType[] { + // The read-only disabled form values are not included in the formGroup value. + // Manually add them to the returned result to ensure they are included in the form submission. + let formValues = this.formGroup.value.accessPolicies; + formValues = formValues.concat( + this.currentAccessPolicies + .filter((m) => m.readOnly) + .map((m) => ({ + id: m.id, + type: m.type, + permission: m.permission, + })), + ); + return formValues; + } } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.component.html index e1faf2a185..e926ba6a13 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.component.html @@ -29,14 +29,17 @@ bitRow *ngFor="let item of selectionList.selectedItems; let i = index" [formGroupName]="i" + [ngClass]="{ 'tw-text-muted': item.readOnly }" > <td bitCell class="tw-w-0 tw-pr-0"> - <i class="bwi {{ item.icon }} tw-text-muted" aria-hidden="true"></i> + <i class="bwi {{ item.icon }}" aria-hidden="true"></i> + </td> + <td bitCell class="tw-max-w-sm tw-truncate"> + {{ item.labelName }} </td> - <td bitCell class="tw-max-w-sm tw-truncate">{{ item.labelName }}</td> <td bitCell class="tw-mb-auto tw-inline-block tw-w-auto"> <select - *ngIf="!staticPermission; else static" + *ngIf="!staticPermission && !item.readOnly; else readOnly" bitInput formControlName="permission" (blur)="handleBlur()" @@ -45,12 +48,20 @@ {{ p.labelId | i18n }} </option> </select> + <ng-template #readOnly> + <ng-container *ngIf="item.readOnly; else static"> + <div class="tw-overflow-hidden tw-overflow-ellipsis tw-whitespace-nowrap"> + {{ item.permission | i18n }} + </div> + </ng-container> + </ng-template> <ng-template #static> <span>{{ staticPermission | i18n }}</span> </ng-template> </td> <td bitCell class="tw-w-0"> <button + *ngIf="!item.readOnly" type="button" bitIconButton="bwi-close" buttonType="main" diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.component.ts index 15449f0416..e8e17fd83b 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.component.ts @@ -35,6 +35,34 @@ export class AccessPolicySelectorComponent implements ControlValueAccessor, OnIn private notifyOnTouch: () => void; private pauseChangeNotification: boolean; + /** + * Updates the enabled/disabled state of provided row form group based on the item's readonly state. + * If a row is enabled, it also updates the enabled/disabled state of the permission control + * based on the item's accessAllItems state and the current value of `permissionMode`. + * @param controlRow - The form group for the row to update + * @param item - The access item that is represented by the row + */ + private updateRowControlDisableState = ( + controlRow: FormGroup<ControlsOf<ApItemValueType>>, + item: ApItemViewType, + ) => { + // Disable entire row form group if readOnly + if (item.readOnly || this.disabled) { + controlRow.disable(); + } else { + controlRow.enable(); + } + }; + + /** + * Updates the enabled/disabled state of ALL row form groups based on each item's readonly state. + */ + private updateAllRowControlDisableStates = () => { + this.selectionList.forEachControlItem((controlRow, item) => { + this.updateRowControlDisableState(controlRow as FormGroup<ControlsOf<ApItemValueType>>, item); + }); + }; + /** * The internal selection list that tracks the value of this form control / component. * It's responsible for keeping items sorted and synced with the rendered form controls @@ -59,6 +87,9 @@ export class AccessPolicySelectorComponent implements ControlValueAccessor, OnIn currentUserInGroup: new FormControl(currentUserInGroup), currentUser: new FormControl(currentUser), }); + + this.updateRowControlDisableState(fg, item); + return fg; }, this._itemComparator.bind(this)); @@ -100,7 +131,13 @@ export class AccessPolicySelectorComponent implements ControlValueAccessor, OnIn set items(val: ApItemViewType[]) { if (val != null) { - const selected = this.selectionList.formArray.getRawValue() ?? []; + let selected = this.selectionList.formArray.getRawValue() ?? []; + selected = selected.concat( + val + .filter((m) => m.readOnly) + .map((m) => ({ id: m.id, type: m.type, permission: m.permission })), + ); + this.selectionList.populateItems( val.map((m) => { m.icon = m.icon ?? ApItemEnumUtil.itemIcon(m.type); @@ -137,6 +174,9 @@ export class AccessPolicySelectorComponent implements ControlValueAccessor, OnIn } else { this.formGroup.enable(); this.multiSelectFormGroup.enable(); + // The enable() above automatically enables all the row controls, + // so we need to disable the readonly ones again + this.updateAllRowControlDisableStates(); } } @@ -149,6 +189,9 @@ export class AccessPolicySelectorComponent implements ControlValueAccessor, OnIn // Always clear the internal selection list on a new value this.selectionList.deselectAll(); + // We need to also select any read only items to appear in the table + this.selectionList.selectItems(this.items.filter((m) => m.readOnly).map((m) => m.id)); + // If the new value is null, then we're done if (selectedItems == null) { this.pauseChangeNotification = false; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.service.spec.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.service.spec.ts index 8c0f9c3731..324406e766 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.service.spec.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.service.spec.ts @@ -347,6 +347,7 @@ function createApItemViewType(options: Partial<ApItemViewType> = {}) { labelName: options?.labelName ?? "test", type: options?.type ?? ApItemEnum.User, permission: options?.permission ?? ApPermissionEnum.CanRead, + readOnly: options?.readOnly ?? false, }; } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/ap-item-value.type.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/ap-item-value.type.ts index 362f3c524a..37c9f5523a 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/ap-item-value.type.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/ap-item-value.type.ts @@ -5,6 +5,9 @@ import { ServiceAccountPeopleAccessPoliciesView, UserServiceAccountAccessPolicyView, GroupServiceAccountAccessPolicyView, + ServiceAccountGrantedPoliciesView, + ServiceAccountProjectPolicyPermissionDetailsView, + ServiceAccountProjectAccessPolicyView, } from "../../../../models/view/access-policy.view"; import { ApItemEnum } from "./enums/ap-item.enum"; @@ -76,3 +79,26 @@ export function convertToServiceAccountPeopleAccessPoliciesView( }); return view; } + +export function convertToServiceAccountGrantedPoliciesView( + serviceAccountId: string, + selectedPolicyValues: ApItemValueType[], +): ServiceAccountGrantedPoliciesView { + const view = new ServiceAccountGrantedPoliciesView(); + + view.grantedProjectPolicies = selectedPolicyValues + .filter((x) => x.type == ApItemEnum.Project) + .map((filtered) => { + const detailView = new ServiceAccountProjectPolicyPermissionDetailsView(); + const policyView = new ServiceAccountProjectAccessPolicyView(); + policyView.serviceAccountId = serviceAccountId; + policyView.grantedProjectId = filtered.id; + policyView.read = ApPermissionEnumUtil.toRead(filtered.permission); + policyView.write = ApPermissionEnumUtil.toWrite(filtered.permission); + + detailView.accessPolicy = policyView; + return detailView; + }); + + return view; +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/ap-item-view.type.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/ap-item-view.type.ts index 1f494b8fbf..996818001d 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/ap-item-view.type.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/ap-item-view.type.ts @@ -3,6 +3,7 @@ import { SelectItemView } from "@bitwarden/components"; import { ProjectPeopleAccessPoliciesView, + ServiceAccountGrantedPoliciesView, ServiceAccountPeopleAccessPoliciesView, } from "../../../../models/view/access-policy.view"; import { PotentialGranteeView } from "../../../../models/view/potential-grantee.view"; @@ -13,6 +14,12 @@ import { ApPermissionEnum, ApPermissionEnumUtil } from "./enums/ap-permission.en export type ApItemViewType = SelectItemView & { accessPolicyId?: string; permission?: ApPermissionEnum; + /** + * Flag that this item cannot be modified. + * This will disable the permission editor and will keep + * the item always selected. + */ + readOnly: boolean; } & ( | { type: ApItemEnum.User; @@ -47,6 +54,7 @@ export function convertToAccessPolicyItemViews( permission: ApPermissionEnumUtil.toApPermissionEnum(policy.read, policy.write), userId: policy.userId, currentUser: policy.currentUser, + readOnly: false, }); }); @@ -60,12 +68,36 @@ export function convertToAccessPolicyItemViews( listName: policy.groupName, permission: ApPermissionEnumUtil.toApPermissionEnum(policy.read, policy.write), currentUserInGroup: policy.currentUserInGroup, + readOnly: false, }); }); return accessPolicies; } +export function convertGrantedPoliciesToAccessPolicyItemViews( + value: ServiceAccountGrantedPoliciesView, +): ApItemViewType[] { + const accessPolicies: ApItemViewType[] = []; + + value.grantedProjectPolicies.forEach((detailView) => { + accessPolicies.push({ + type: ApItemEnum.Project, + icon: ApItemEnumUtil.itemIcon(ApItemEnum.Project), + id: detailView.accessPolicy.grantedProjectId, + accessPolicyId: detailView.accessPolicy.id, + labelName: detailView.accessPolicy.grantedProjectName, + listName: detailView.accessPolicy.grantedProjectName, + permission: ApPermissionEnumUtil.toApPermissionEnum( + detailView.accessPolicy.read, + detailView.accessPolicy.write, + ), + readOnly: !detailView.hasPermission, + }); + }); + return accessPolicies; +} + export function convertPotentialGranteesToApItemViewType( grantees: PotentialGranteeView[], ): ApItemViewType[] { @@ -108,6 +140,7 @@ export function convertPotentialGranteesToApItemViewType( listName: listName, currentUserInGroup: granteeView.currentUserInGroup, currentUser: granteeView.currentUser, + readOnly: false, }; }); } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy.service.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy.service.ts index 05b95e127d..967bbf7ed0 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy.service.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy.service.ts @@ -18,15 +18,17 @@ import { UserProjectAccessPolicyView, UserServiceAccountAccessPolicyView, ServiceAccountPeopleAccessPoliciesView, + ServiceAccountGrantedPoliciesView, + ServiceAccountProjectPolicyPermissionDetailsView, } from "../../models/view/access-policy.view"; import { PotentialGranteeView } from "../../models/view/potential-grantee.view"; import { AccessPoliciesCreateRequest } from "../../shared/access-policies/models/requests/access-policies-create.request"; import { PeopleAccessPoliciesRequest } from "../../shared/access-policies/models/requests/people-access-policies.request"; import { ProjectAccessPoliciesResponse } from "../../shared/access-policies/models/responses/project-access-policies.response"; +import { ServiceAccountGrantedPoliciesRequest } from "../access-policies/models/requests/service-account-granted-policies.request"; import { AccessPolicyUpdateRequest } from "./models/requests/access-policy-update.request"; import { AccessPolicyRequest } from "./models/requests/access-policy.request"; -import { GrantedPolicyRequest } from "./models/requests/granted-policy.request"; import { GroupServiceAccountAccessPolicyResponse, UserServiceAccountAccessPolicyResponse, @@ -36,28 +38,21 @@ import { } from "./models/responses/access-policy.response"; import { PotentialGranteeResponse } from "./models/responses/potential-grantee.response"; import { ProjectPeopleAccessPoliciesResponse } from "./models/responses/project-people-access-policies.response"; +import { ServiceAccountGrantedPoliciesPermissionDetailsResponse } from "./models/responses/service-account-granted-policies-permission-details.response"; import { ServiceAccountPeopleAccessPoliciesResponse } from "./models/responses/service-account-people-access-policies.response"; +import { ServiceAccountProjectPolicyPermissionDetailsResponse } from "./models/responses/service-account-project-policy-permission-details.response"; @Injectable({ providedIn: "root", }) export class AccessPolicyService { private _projectAccessPolicyChanges$ = new Subject<ProjectAccessPoliciesView>(); - private _serviceAccountGrantedPolicyChanges$ = new Subject< - ServiceAccountProjectAccessPolicyView[] - >(); /** * Emits when a project access policy is created or deleted. */ readonly projectAccessPolicyChanges$ = this._projectAccessPolicyChanges$.asObservable(); - /** - * Emits when a service account granted policy is created or deleted. - */ - readonly serviceAccountGrantedPolicyChanges$ = - this._serviceAccountGrantedPolicyChanges$.asObservable(); - constructor( private cryptoService: CryptoService, protected apiService: ApiService, @@ -68,44 +63,6 @@ export class AccessPolicyService { this._projectAccessPolicyChanges$.next(null); } - async getGrantedPolicies( - serviceAccountId: string, - organizationId: string, - ): Promise<ServiceAccountProjectAccessPolicyView[]> { - const r = await this.apiService.send( - "GET", - "/service-accounts/" + serviceAccountId + "/granted-policies", - null, - true, - true, - ); - - const results = new ListResponse(r, ServiceAccountProjectAccessPolicyResponse); - return await this.createServiceAccountProjectAccessPolicyViews(results.data, organizationId); - } - - async createGrantedPolicies( - organizationId: string, - serviceAccountId: string, - policies: ServiceAccountProjectAccessPolicyView[], - ): Promise<ServiceAccountProjectAccessPolicyView[]> { - const request = this.getGrantedPoliciesCreateRequest(policies); - const r = await this.apiService.send( - "POST", - "/service-accounts/" + serviceAccountId + "/granted-policies", - request, - true, - true, - ); - const results = new ListResponse(r, ServiceAccountProjectAccessPolicyResponse); - const views = await this.createServiceAccountProjectAccessPolicyViews( - results.data, - organizationId, - ); - this._serviceAccountGrantedPolicyChanges$.next(views); - return views; - } - async getProjectAccessPolicies( organizationId: string, projectId: string, @@ -184,6 +141,40 @@ export class AccessPolicyService { return this.createServiceAccountPeopleAccessPoliciesView(results); } + async getServiceAccountGrantedPolicies( + organizationId: string, + serviceAccountId: string, + ): Promise<ServiceAccountGrantedPoliciesView> { + const r = await this.apiService.send( + "GET", + "/service-accounts/" + serviceAccountId + "/granted-policies", + null, + true, + true, + ); + + const result = new ServiceAccountGrantedPoliciesPermissionDetailsResponse(r); + return await this.createServiceAccountGrantedPoliciesView(result, organizationId); + } + + async putServiceAccountGrantedPolicies( + organizationId: string, + serviceAccountId: string, + policies: ServiceAccountGrantedPoliciesView, + ): Promise<ServiceAccountGrantedPoliciesView> { + const request = this.getServiceAccountGrantedPoliciesRequest(policies); + const r = await this.apiService.send( + "PUT", + "/service-accounts/" + serviceAccountId + "/granted-policies", + request, + true, + true, + ); + + const result = new ServiceAccountGrantedPoliciesPermissionDetailsResponse(r); + return await this.createServiceAccountGrantedPoliciesView(result, organizationId); + } + async createProjectAccessPolicies( organizationId: string, projectId: string, @@ -206,7 +197,6 @@ export class AccessPolicyService { async deleteAccessPolicy(accessPolicyId: string): Promise<void> { await this.apiService.send("DELETE", "/access-policies/" + accessPolicyId, null, true, false); this._projectAccessPolicyChanges$.next(null); - this._serviceAccountGrantedPolicyChanges$.next(null); } async updateAccessPolicy(baseAccessPolicyView: BaseAccessPolicyView): Promise<void> { @@ -222,6 +212,158 @@ export class AccessPolicyService { ); } + async getPeoplePotentialGrantees(organizationId: string) { + const r = await this.apiService.send( + "GET", + "/organizations/" + organizationId + "/access-policies/people/potential-grantees", + null, + true, + true, + ); + const results = new ListResponse(r, PotentialGranteeResponse); + return await this.createPotentialGranteeViews(organizationId, results.data); + } + + async getServiceAccountsPotentialGrantees(organizationId: string) { + const r = await this.apiService.send( + "GET", + "/organizations/" + organizationId + "/access-policies/service-accounts/potential-grantees", + null, + true, + true, + ); + const results = new ListResponse(r, PotentialGranteeResponse); + return await this.createPotentialGranteeViews(organizationId, results.data); + } + + async getProjectsPotentialGrantees(organizationId: string) { + const r = await this.apiService.send( + "GET", + "/organizations/" + organizationId + "/access-policies/projects/potential-grantees", + null, + true, + true, + ); + const results = new ListResponse(r, PotentialGranteeResponse); + return await this.createPotentialGranteeViews(organizationId, results.data); + } + + protected async getOrganizationKey(organizationId: string): Promise<SymmetricCryptoKey> { + return await this.cryptoService.getOrgKey(organizationId); + } + + protected getAccessPolicyRequest( + granteeId: string, + view: + | UserProjectAccessPolicyView + | UserServiceAccountAccessPolicyView + | GroupProjectAccessPolicyView + | GroupServiceAccountAccessPolicyView + | ServiceAccountProjectAccessPolicyView, + ) { + const request = new AccessPolicyRequest(); + request.granteeId = granteeId; + request.read = view.read; + request.write = view.write; + return request; + } + + protected createBaseAccessPolicyView( + response: + | UserProjectAccessPolicyResponse + | UserServiceAccountAccessPolicyResponse + | GroupProjectAccessPolicyResponse + | GroupServiceAccountAccessPolicyResponse + | ServiceAccountProjectAccessPolicyResponse, + ) { + return { + id: response.id, + read: response.read, + write: response.write, + creationDate: response.creationDate, + revisionDate: response.revisionDate, + }; + } + + private async createPotentialGranteeViews( + organizationId: string, + results: PotentialGranteeResponse[], + ): Promise<PotentialGranteeView[]> { + const orgKey = await this.getOrganizationKey(organizationId); + return await Promise.all( + results.map(async (r) => { + const view = new PotentialGranteeView(); + view.id = r.id; + view.type = r.type; + view.email = r.email; + view.currentUser = r.currentUser; + view.currentUserInGroup = r.currentUserInGroup; + + if (r.type === "serviceAccount" || r.type === "project") { + view.name = r.name + ? await this.encryptService.decryptToUtf8(new EncString(r.name), orgKey) + : null; + } else { + view.name = r.name; + } + return view; + }), + ); + } + + private getServiceAccountGrantedPoliciesRequest( + policies: ServiceAccountGrantedPoliciesView, + ): ServiceAccountGrantedPoliciesRequest { + const request = new ServiceAccountGrantedPoliciesRequest(); + + request.projectGrantedPolicyRequests = policies.grantedProjectPolicies.map((detailView) => ({ + grantedId: detailView.accessPolicy.grantedProjectId, + read: detailView.accessPolicy.read, + write: detailView.accessPolicy.write, + })); + + return request; + } + + private async createServiceAccountGrantedPoliciesView( + response: ServiceAccountGrantedPoliciesPermissionDetailsResponse, + organizationId: string, + ): Promise<ServiceAccountGrantedPoliciesView> { + const orgKey = await this.getOrganizationKey(organizationId); + + const view = new ServiceAccountGrantedPoliciesView(); + view.grantedProjectPolicies = + await this.createServiceAccountProjectPolicyPermissionDetailsViews( + orgKey, + response.grantedProjectPolicies, + ); + return view; + } + + private async createServiceAccountProjectPolicyPermissionDetailsViews( + orgKey: SymmetricCryptoKey, + responses: ServiceAccountProjectPolicyPermissionDetailsResponse[], + ): Promise<ServiceAccountProjectPolicyPermissionDetailsView[]> { + return await Promise.all( + responses.map(async (response) => { + return await this.createServiceAccountProjectPolicyPermissionDetailsView(orgKey, response); + }), + ); + } + + private async createServiceAccountProjectPolicyPermissionDetailsView( + orgKey: SymmetricCryptoKey, + response: ServiceAccountProjectPolicyPermissionDetailsResponse, + ): Promise<ServiceAccountProjectPolicyPermissionDetailsView> { + const view = new ServiceAccountProjectPolicyPermissionDetailsView(); + view.hasPermission = response.hasPermission; + view.accessPolicy = await this.createServiceAccountProjectAccessPolicyView( + orgKey, + response.accessPolicy, + ); + return view; + } + private async createProjectAccessPoliciesView( organizationId: string, projectAccessPoliciesResponse: ProjectAccessPoliciesResponse, @@ -393,147 +535,4 @@ export class AccessPolicyService { currentUserInGroup: response.currentUserInGroup, }; } - - async getPeoplePotentialGrantees(organizationId: string) { - const r = await this.apiService.send( - "GET", - "/organizations/" + organizationId + "/access-policies/people/potential-grantees", - null, - true, - true, - ); - const results = new ListResponse(r, PotentialGranteeResponse); - return await this.createPotentialGranteeViews(organizationId, results.data); - } - - async getServiceAccountsPotentialGrantees(organizationId: string) { - const r = await this.apiService.send( - "GET", - "/organizations/" + organizationId + "/access-policies/service-accounts/potential-grantees", - null, - true, - true, - ); - const results = new ListResponse(r, PotentialGranteeResponse); - return await this.createPotentialGranteeViews(organizationId, results.data); - } - - async getProjectsPotentialGrantees(organizationId: string) { - const r = await this.apiService.send( - "GET", - "/organizations/" + organizationId + "/access-policies/projects/potential-grantees", - null, - true, - true, - ); - const results = new ListResponse(r, PotentialGranteeResponse); - return await this.createPotentialGranteeViews(organizationId, results.data); - } - - protected async getOrganizationKey(organizationId: string): Promise<SymmetricCryptoKey> { - return await this.cryptoService.getOrgKey(organizationId); - } - - protected getAccessPolicyRequest( - granteeId: string, - view: - | UserProjectAccessPolicyView - | UserServiceAccountAccessPolicyView - | GroupProjectAccessPolicyView - | GroupServiceAccountAccessPolicyView - | ServiceAccountProjectAccessPolicyView, - ) { - const request = new AccessPolicyRequest(); - request.granteeId = granteeId; - request.read = view.read; - request.write = view.write; - return request; - } - - protected createBaseAccessPolicyView( - response: - | UserProjectAccessPolicyResponse - | UserServiceAccountAccessPolicyResponse - | GroupProjectAccessPolicyResponse - | GroupServiceAccountAccessPolicyResponse - | ServiceAccountProjectAccessPolicyResponse, - ) { - return { - id: response.id, - read: response.read, - write: response.write, - creationDate: response.creationDate, - revisionDate: response.revisionDate, - }; - } - - private async createPotentialGranteeViews( - organizationId: string, - results: PotentialGranteeResponse[], - ): Promise<PotentialGranteeView[]> { - const orgKey = await this.getOrganizationKey(organizationId); - return await Promise.all( - results.map(async (r) => { - const view = new PotentialGranteeView(); - view.id = r.id; - view.type = r.type; - view.email = r.email; - view.currentUser = r.currentUser; - view.currentUserInGroup = r.currentUserInGroup; - - if (r.type === "serviceAccount" || r.type === "project") { - view.name = r.name - ? await this.encryptService.decryptToUtf8(new EncString(r.name), orgKey) - : null; - } else { - view.name = r.name; - } - return view; - }), - ); - } - - private getGrantedPoliciesCreateRequest( - policies: ServiceAccountProjectAccessPolicyView[], - ): GrantedPolicyRequest[] { - return policies.map((ap) => { - const request = new GrantedPolicyRequest(); - request.grantedId = ap.grantedProjectId; - request.read = ap.read; - request.write = ap.write; - return request; - }); - } - - private async createServiceAccountProjectAccessPolicyViews( - responses: ServiceAccountProjectAccessPolicyResponse[], - organizationId: string, - ): Promise<ServiceAccountProjectAccessPolicyView[]> { - const orgKey = await this.getOrganizationKey(organizationId); - return await Promise.all( - responses.map(async (response: ServiceAccountProjectAccessPolicyResponse) => { - const view = new ServiceAccountProjectAccessPolicyView(); - view.id = response.id; - view.read = response.read; - view.write = response.write; - view.creationDate = response.creationDate; - view.revisionDate = response.revisionDate; - view.serviceAccountId = response.serviceAccountId; - view.grantedProjectId = response.grantedProjectId; - view.serviceAccountName = response.serviceAccountName - ? await this.encryptService.decryptToUtf8( - new EncString(response.serviceAccountName), - orgKey, - ) - : null; - view.grantedProjectName = response.grantedProjectName - ? await this.encryptService.decryptToUtf8( - new EncString(response.grantedProjectName), - orgKey, - ) - : null; - return view; - }), - ); - } } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/requests/service-account-granted-policies.request.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/requests/service-account-granted-policies.request.ts new file mode 100644 index 0000000000..74ec52021f --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/requests/service-account-granted-policies.request.ts @@ -0,0 +1,5 @@ +import { GrantedPolicyRequest } from "./granted-policy.request"; + +export class ServiceAccountGrantedPoliciesRequest { + projectGrantedPolicyRequests?: GrantedPolicyRequest[]; +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/service-account-granted-policies-permission-details.response.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/service-account-granted-policies-permission-details.response.ts new file mode 100644 index 0000000000..858a59ff43 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/service-account-granted-policies-permission-details.response.ts @@ -0,0 +1,15 @@ +import { BaseResponse } from "@bitwarden/common/models/response/base.response"; + +import { ServiceAccountProjectPolicyPermissionDetailsResponse } from "./service-account-project-policy-permission-details.response"; + +export class ServiceAccountGrantedPoliciesPermissionDetailsResponse extends BaseResponse { + grantedProjectPolicies: ServiceAccountProjectPolicyPermissionDetailsResponse[]; + + constructor(response: any) { + super(response); + const grantedProjectPolicies = this.getResponseProperty("GrantedProjectPolicies"); + this.grantedProjectPolicies = grantedProjectPolicies.map( + (k: any) => new ServiceAccountProjectPolicyPermissionDetailsResponse(k), + ); + } +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/service-account-project-policy-permission-details.response.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/service-account-project-policy-permission-details.response.ts new file mode 100644 index 0000000000..dbc4fe0727 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/service-account-project-policy-permission-details.response.ts @@ -0,0 +1,14 @@ +import { BaseResponse } from "@bitwarden/common/models/response/base.response"; + +import { ServiceAccountProjectAccessPolicyResponse } from "./access-policy.response"; + +export class ServiceAccountProjectPolicyPermissionDetailsResponse extends BaseResponse { + accessPolicy: ServiceAccountProjectAccessPolicyResponse; + hasPermission: boolean; + + constructor(response: any) { + super(response); + this.accessPolicy = this.getResponseProperty("AccessPolicy"); + this.hasPermission = this.getResponseProperty("HasPermission"); + } +} From b45c309f838c13d6743c09913daaf3bf55b91c74 Mon Sep 17 00:00:00 2001 From: Matt Gibson <mgibson@bitwarden.com> Date: Wed, 1 May 2024 13:43:15 -0400 Subject: [PATCH 334/351] Add beta extension to allowed native messaging hosts (#8996) --- apps/desktop/src/main/native-messaging.main.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/desktop/src/main/native-messaging.main.ts b/apps/desktop/src/main/native-messaging.main.ts index d3dd25c644..8c8404578b 100644 --- a/apps/desktop/src/main/native-messaging.main.ts +++ b/apps/desktop/src/main/native-messaging.main.ts @@ -146,6 +146,8 @@ export class NativeMessagingMain { allowed_origins: [ // Chrome extension "chrome-extension://nngceckbapebfimnlniiiahkandclblb/", + // Chrome beta extension + "chrome-extension://hccnnhgbibccigepcmlgppchkpfdophk/", // Edge extension "chrome-extension://jbkfoedolllekgbhcbcoahefnbanhhlh/", // Opera extension From 4b42ff71713b3642732c087f5f9c03e087b78514 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Wed, 1 May 2024 15:50:40 -0400 Subject: [PATCH 335/351] [PM-3483] Remove `migrateKeyForNeverLockIfNeeded` Logic (#8953) * Remove `migrateKeyForNeverLockIfNeeded` Logic * Fix Test * Remove `migrateAutoKeyIfNeeded` --- .../browser/src/background/main.background.ts | 1 - .../vault-timeout-service.factory.ts | 6 ---- apps/cli/src/bw.ts | 1 - .../src/services/jslib-services.module.ts | 1 - .../platform/abstractions/crypto.service.ts | 4 --- .../platform/abstractions/state.service.ts | 4 --- .../src/platform/services/crypto.service.ts | 29 ------------------- .../src/platform/services/state.service.ts | 17 ----------- .../vault-timeout.service.spec.ts | 4 --- .../vault-timeout/vault-timeout.service.ts | 22 -------------- 10 files changed, 89 deletions(-) diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 16b8c8beea..74b21204be 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -737,7 +737,6 @@ export default class MainBackground { this.cipherService, this.folderService, this.collectionService, - this.cryptoService, this.platformUtilsService, this.messagingService, this.searchService, diff --git a/apps/browser/src/background/service-factories/vault-timeout-service.factory.ts b/apps/browser/src/background/service-factories/vault-timeout-service.factory.ts index 14f055114b..0b176c28f1 100644 --- a/apps/browser/src/background/service-factories/vault-timeout-service.factory.ts +++ b/apps/browser/src/background/service-factories/vault-timeout-service.factory.ts @@ -12,10 +12,6 @@ import { internalMasterPasswordServiceFactory, MasterPasswordServiceInitOptions, } from "../../auth/background/service-factories/master-password-service.factory"; -import { - CryptoServiceInitOptions, - cryptoServiceFactory, -} from "../../platform/background/service-factories/crypto-service.factory"; import { CachedServices, factory, @@ -70,7 +66,6 @@ export type VaultTimeoutServiceInitOptions = VaultTimeoutServiceFactoryOptions & CipherServiceInitOptions & FolderServiceInitOptions & CollectionServiceInitOptions & - CryptoServiceInitOptions & PlatformUtilsServiceInitOptions & MessagingServiceInitOptions & SearchServiceInitOptions & @@ -94,7 +89,6 @@ export function vaultTimeoutServiceFactory( await cipherServiceFactory(cache, opts), await folderServiceFactory(cache, opts), await collectionServiceFactory(cache, opts), - await cryptoServiceFactory(cache, opts), await platformUtilsServiceFactory(cache, opts), await messagingServiceFactory(cache, opts), await searchServiceFactory(cache, opts), diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index 665701639e..114765a789 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -611,7 +611,6 @@ export class Main { this.cipherService, this.folderService, this.collectionService, - this.cryptoService, this.platformUtilsService, this.messagingService, this.searchService, diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 46d739d0f5..80024bb0b6 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -656,7 +656,6 @@ const safeProviders: SafeProvider[] = [ CipherServiceAbstraction, FolderServiceAbstraction, CollectionServiceAbstraction, - CryptoServiceAbstraction, PlatformUtilsServiceAbstraction, MessagingServiceAbstraction, SearchServiceAbstraction, diff --git a/libs/common/src/platform/abstractions/crypto.service.ts b/libs/common/src/platform/abstractions/crypto.service.ts index f56714bfda..14baee0d12 100644 --- a/libs/common/src/platform/abstractions/crypto.service.ts +++ b/libs/common/src/platform/abstractions/crypto.service.ts @@ -296,10 +296,6 @@ export abstract class CryptoService { kdfConfig: KdfConfig, oldPinKey: EncString, ): Promise<UserKey>; - /** - * Replaces old master auto keys with new user auto keys - */ - abstract migrateAutoKeyIfNeeded(userId?: string): Promise<void>; /** * @param keyMaterial The key material to derive the send key from * @returns A new send key diff --git a/libs/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index 5ca604b526..e8edd5d132 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -82,10 +82,6 @@ export abstract class StateService<T extends Account = Account> { * @deprecated For migration purposes only, use getUserKeyMasterKey instead */ getEncryptedCryptoSymmetricKey: (options?: StorageOptions) => Promise<string>; - /** - * @deprecated For migration purposes only, use getUserKeyAuto instead - */ - getCryptoMasterKeyAuto: (options?: StorageOptions) => Promise<string>; /** * @deprecated For migration purposes only, use setUserKeyAuto instead */ diff --git a/libs/common/src/platform/services/crypto.service.ts b/libs/common/src/platform/services/crypto.service.ts index 713fe7d230..70f10a4b98 100644 --- a/libs/common/src/platform/services/crypto.service.ts +++ b/libs/common/src/platform/services/crypto.service.ts @@ -930,35 +930,6 @@ export class CryptoService implements CryptoServiceAbstraction { } } - async migrateAutoKeyIfNeeded(userId?: UserId) { - const oldAutoKey = await this.stateService.getCryptoMasterKeyAuto({ userId: userId }); - if (!oldAutoKey) { - return; - } - // Decrypt - const masterKey = new SymmetricCryptoKey(Utils.fromB64ToArray(oldAutoKey)) as MasterKey; - 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. - userId ??= await firstValueFrom(this.stateProvider.activeUserId$); - await this.masterPasswordService.setMasterKey(masterKey, userId); - return; - } - const encryptedUserKey = await this.stateService.getEncryptedCryptoSymmetricKey({ - userId: userId, - }); - const userKey = await this.decryptUserKeyWithMasterKey( - masterKey, - new EncString(encryptedUserKey), - userId, - ); - // Migrate - await this.stateService.setUserKeyAutoUnlock(userKey.keyB64, { userId: userId }); - await this.stateService.setCryptoMasterKeyAuto(null, { userId: userId }); - // Set encrypted user key in case user immediately locks without syncing - await this.setMasterKeyEncryptedUserKey(encryptedUserKey); - } - async decryptAndMigrateOldPinKey( masterPasswordOnRestart: boolean, pin: string, diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index 9479d64710..3324c4d2ab 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -268,23 +268,6 @@ export class StateService< ); } - /** - * @deprecated Use UserKeyAuto instead - */ - async getCryptoMasterKeyAuto(options?: StorageOptions): Promise<string> { - options = this.reconcileOptions( - this.reconcileOptions(options, { keySuffix: "auto" }), - await this.defaultSecureStorageOptions(), - ); - if (options?.userId == null) { - return null; - } - return await this.secureStorageService.get<string>( - `${options.userId}${partialKeys.autoKey}`, - options, - ); - } - /** * @deprecated Use UserKeyAuto instead */ 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 12c24dcdef..42ffb5192b 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 @@ -9,7 +9,6 @@ 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"; @@ -28,7 +27,6 @@ describe("VaultTimeoutService", () => { let cipherService: MockProxy<CipherService>; let folderService: MockProxy<FolderService>; let collectionService: MockProxy<CollectionService>; - let cryptoService: MockProxy<CryptoService>; let platformUtilsService: MockProxy<PlatformUtilsService>; let messagingService: MockProxy<MessagingService>; let searchService: MockProxy<SearchService>; @@ -52,7 +50,6 @@ describe("VaultTimeoutService", () => { cipherService = mock(); folderService = mock(); collectionService = mock(); - cryptoService = mock(); platformUtilsService = mock(); messagingService = mock(); searchService = mock(); @@ -76,7 +73,6 @@ describe("VaultTimeoutService", () => { cipherService, folderService, collectionService, - cryptoService, platformUtilsService, messagingService, searchService, 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 8e0978d07d..2f3a259562 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout.service.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout.service.ts @@ -7,9 +7,7 @@ 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"; -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"; @@ -28,7 +26,6 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { private cipherService: CipherService, private folderService: FolderService, private collectionService: CollectionService, - private cryptoService: CryptoService, protected platformUtilsService: PlatformUtilsService, private messagingService: MessagingService, private searchService: SearchService, @@ -44,8 +41,6 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { if (this.inited) { return; } - // TODO: Remove after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3483) - await this.migrateKeyForNeverLockIfNeeded(); this.inited = true; if (checkOnInterval) { @@ -175,21 +170,4 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { ? await this.logOut(userId) : await this.lock(userId); } - - private async migrateKeyForNeverLockIfNeeded(): Promise<void> { - // Web can't set vault timeout to never - if (this.platformUtilsService.getClientType() == ClientType.Web) { - return; - } - const accounts = await firstValueFrom(this.stateService.accounts$); - for (const userId in accounts) { - if (userId != null) { - await this.cryptoService.migrateAutoKeyIfNeeded(userId); - // Legacy users should be logged out since we're not on the web vault and can't migrate. - if (await this.cryptoService.isLegacyUser(null, userId)) { - await this.logOut(userId); - } - } - } - } } From 66d9ec19a329fbb3e155a4660cc663fa53300038 Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Wed, 1 May 2024 22:00:10 +0100 Subject: [PATCH 336/351] resolve the issue (#9000) --- .../providers/settings/account.component.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts index 70eb8af7ba..0dace2945e 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts @@ -1,5 +1,5 @@ import { Component } from "@angular/core"; -import { ActivatedRoute } from "@angular/router"; +import { ActivatedRoute, Router } from "@angular/router"; import { UserVerificationDialogComponent } from "@bitwarden/auth/angular"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -42,6 +42,7 @@ export class AccountComponent { private dialogService: DialogService, private configService: ConfigService, private providerApiService: ProviderApiServiceAbstraction, + private router: Router, ) {} async ngOnInit() { @@ -93,9 +94,8 @@ export class AccountComponent { return; } - this.formPromise = this.providerApiService.deleteProvider(this.providerId); try { - await this.formPromise; + await this.providerApiService.deleteProvider(this.providerId); this.platformUtilsService.showToast( "success", this.i18nService.t("providerDeleted"), @@ -104,7 +104,8 @@ export class AccountComponent { } catch (e) { this.logService.error(e); } - this.formPromise = null; + + await this.router.navigate(["/"]); } private async verifyUser(): Promise<boolean> { From 9dda5e8ee16ea537e6d892028cabad0c360619a1 Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Thu, 2 May 2024 09:54:18 +1000 Subject: [PATCH 337/351] [AC-2170] Group modal - limit admin access - collections tab (#8758) * Update Group modal -> Collections tab to respect collection management settings, e.g. only allow admins to assign access to collections they can manage * Update collectionAdminView getters for custom permissions --- .../manage/group-add-edit.component.html | 7 +- .../manage/group-add-edit.component.ts | 203 +++++++++++------- .../vault/core/views/collection-admin.view.ts | 9 +- apps/web/src/locales/en/messages.json | 3 + 4 files changed, 140 insertions(+), 82 deletions(-) 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 3afb816e14..5fcf7b0f42 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 @@ -50,7 +50,12 @@ </bit-tab> <bit-tab label="{{ 'collections' | i18n }}"> - <p>{{ "editGroupCollectionsDesc" | i18n }}</p> + <p> + {{ "editGroupCollectionsDesc" | i18n }} + <span *ngIf="!(allowAdminAccessToAllCollectionItems$ | async)"> + {{ "editGroupCollectionsRestrictionsDesc" | i18n }} + </span> + </p> <div *ngIf="!(flexibleCollectionsEnabled$ | async)" class="tw-my-3"> <input type="checkbox" formControlName="accessAll" id="accessAll" /> <label class="tw-mb-0 tw-text-lg" for="accessAll">{{ diff --git a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts index b18effac86..aabaac2f1c 100644 --- a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts @@ -11,13 +11,13 @@ import { of, shareReplay, Subject, - switchMap, takeUntil, } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; @@ -26,12 +26,10 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { UserId } from "@bitwarden/common/types/guid"; -import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; -import { CollectionData } from "@bitwarden/common/vault/models/data/collection.data"; -import { Collection } from "@bitwarden/common/vault/models/domain/collection"; -import { CollectionDetailsResponse } from "@bitwarden/common/vault/models/response/collection.response"; import { DialogService } from "@bitwarden/components"; +import { CollectionAdminService } from "../../../vault/core/collection-admin.service"; +import { CollectionAdminView } from "../../../vault/core/views/collection-admin.view"; import { InternalGroupService as GroupService, GroupView } from "../core"; import { AccessItemType, @@ -95,9 +93,15 @@ export const openGroupAddEditDialog = ( templateUrl: "group-add-edit.component.html", }) export class GroupAddEditComponent implements OnInit, OnDestroy { - protected flexibleCollectionsEnabled$ = this.organizationService + private organization$ = this.organizationService .get$(this.organizationId) - .pipe(map((o) => o?.flexibleCollections)); + .pipe(shareReplay({ refCount: true })); + protected flexibleCollectionsEnabled$ = this.organization$.pipe( + map((o) => o?.flexibleCollections), + ); + private flexibleCollectionsV1Enabled$ = this.configService.getFeatureFlag$( + FeatureFlag.FlexibleCollectionsV1, + ); protected PermissionMode = PermissionMode; protected ResultType = GroupAddEditDialogResultType; @@ -131,27 +135,9 @@ export class GroupAddEditComponent implements OnInit, OnDestroy { private destroy$ = new Subject<void>(); - private get orgCollections$() { - return from(this.apiService.getCollections(this.organizationId)).pipe( - switchMap((response) => { - return from( - this.collectionService.decryptMany( - response.data.map( - (r) => new Collection(new CollectionData(r as CollectionDetailsResponse)), - ), - ), - ); - }), - map((collections) => - collections.map<AccessItemView>((c) => ({ - id: c.id, - type: AccessItemType.Collection, - labelName: c.name, - listName: c.name, - })), - ), - ); - } + private orgCollections$ = from(this.collectionAdminService.getAll(this.organizationId)).pipe( + shareReplay({ refCount: false }), + ); private get orgMembers$(): Observable<Array<AccessItemView & { userId: UserId }>> { return from(this.organizationUserService.getAllUsers(this.organizationId)).pipe( @@ -197,23 +183,24 @@ export class GroupAddEditComponent implements OnInit, OnDestroy { shareReplay({ refCount: true, bufferSize: 1 }), ); - restrictGroupAccess$ = combineLatest([ - this.organizationService.get$(this.organizationId), - this.configService.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1), - this.groupDetails$, + allowAdminAccessToAllCollectionItems$ = combineLatest([ + this.organization$, + this.flexibleCollectionsV1Enabled$, ]).pipe( - map( - ([organization, flexibleCollectionsV1Enabled, group]) => - // Feature flag conditionals - flexibleCollectionsV1Enabled && - organization.flexibleCollections && - // Business logic conditionals - !organization.allowAdminAccessToAllCollectionItems && - group !== undefined, - ), - shareReplay({ refCount: true, bufferSize: 1 }), + map(([organization, flexibleCollectionsV1Enabled]) => { + if (!flexibleCollectionsV1Enabled || !organization.flexibleCollections) { + return true; + } + + return organization.allowAdminAccessToAllCollectionItems; + }), ); + restrictGroupAccess$ = combineLatest([ + this.allowAdminAccessToAllCollectionItems$, + this.groupDetails$, + ]).pipe(map(([allowAdminAccess, groupDetails]) => !allowAdminAccess && groupDetails != null)); + constructor( @Inject(DIALOG_DATA) private params: GroupAddEditDialogParams, private dialogRef: DialogRef<GroupAddEditDialogResultType>, @@ -221,7 +208,6 @@ export class GroupAddEditComponent implements OnInit, OnDestroy { private organizationUserService: OrganizationUserService, private groupService: GroupService, private i18nService: I18nService, - private collectionService: CollectionService, private platformUtilsService: PlatformUtilsService, private logService: LogService, private formBuilder: FormBuilder, @@ -230,6 +216,7 @@ export class GroupAddEditComponent implements OnInit, OnDestroy { private organizationService: OrganizationService, private configService: ConfigService, private accountService: AccountService, + private collectionAdminService: CollectionAdminService, ) { this.tabIndex = params.initialTab ?? GroupAddEditTabType.Info; } @@ -244,48 +231,61 @@ export class GroupAddEditComponent implements OnInit, OnDestroy { this.groupDetails$, this.restrictGroupAccess$, this.accountService.activeAccount$, + this.organization$, + this.flexibleCollectionsV1Enabled$, ]) .pipe(takeUntil(this.destroy$)) - .subscribe(([collections, members, group, restrictGroupAccess, activeAccount]) => { - this.collections = collections; - this.members = members; - this.group = group; - - if (this.group != undefined) { - // Must detect changes so that AccessSelector @Inputs() are aware of the latest - // collections/members set above, otherwise no selected values will be patched below - this.changeDetectorRef.detectChanges(); - - this.groupForm.patchValue({ - name: this.group.name, - externalId: this.group.externalId, - accessAll: this.group.accessAll, - members: this.group.members.map((m) => ({ - id: m, - type: AccessItemType.Member, - })), - collections: this.group.collections.map((gc) => ({ - id: gc.id, - type: AccessItemType.Collection, - permission: convertToPermission(gc), - })), - }); - } - - // 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, + .subscribe( + ([ + collections, + members, + group, + restrictGroupAccess, + activeAccount, + organization, + flexibleCollectionsV1Enabled, + ]) => { + this.members = members; + this.group = group; + this.collections = mapToAccessItemViews( + collections, + organization, + flexibleCollectionsV1Enabled, + group, ); - if (!isAlreadyInGroup) { - this.members = this.members.filter((m) => m.id !== organizationUserId); - } - } + if (this.group != undefined) { + // Must detect changes so that AccessSelector @Inputs() are aware of the latest + // collections/members set above, otherwise no selected values will be patched below + this.changeDetectorRef.detectChanges(); - this.loading = false; - }); + this.groupForm.patchValue({ + name: this.group.name, + externalId: this.group.externalId, + accessAll: this.group.accessAll, + members: this.group.members.map((m) => ({ + id: m, + type: AccessItemType.Member, + })), + collections: mapToAccessSelections(group, this.collections), + }); + } + + // 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; + }, + ); } ngOnDestroy() { @@ -355,3 +355,46 @@ export class GroupAddEditComponent implements OnInit, OnDestroy { this.dialogRef.close(GroupAddEditDialogResultType.Deleted); }; } + +/** + * Maps the group's current collection access to AccessItemValues to populate the access-selector's FormControl + */ +function mapToAccessSelections(group: GroupView, items: AccessItemView[]): AccessItemValue[] { + return ( + group.collections + // The FormControl value only represents editable collection access - exclude readonly access selections + .filter((selection) => !items.find((item) => item.id == selection.id).readonly) + .map((gc) => ({ + id: gc.id, + type: AccessItemType.Collection, + permission: convertToPermission(gc), + })) + ); +} + +/** + * Maps the organization's collections to AccessItemViews to populate the access-selector's multi-select + */ +function mapToAccessItemViews( + collections: CollectionAdminView[], + organization: Organization, + flexibleCollectionsV1Enabled: boolean, + group?: GroupView, +): AccessItemView[] { + return ( + collections + .map<AccessItemView>((c) => { + const accessSelection = group?.collections.find((access) => access.id == c.id) ?? undefined; + return { + id: c.id, + type: AccessItemType.Collection, + labelName: c.name, + listName: c.name, + readonly: !c.canEditGroupAccess(organization, flexibleCollectionsV1Enabled), + readonlyPermission: accessSelection ? convertToPermission(accessSelection) : undefined, + }; + }) + // Remove any collection views that are not already assigned and that we don't have permissions to assign access to + .filter((item) => !item.readonly || group?.collections.some((access) => access.id == item.id)) + ); +} diff --git a/apps/web/src/app/vault/core/views/collection-admin.view.ts b/apps/web/src/app/vault/core/views/collection-admin.view.ts index 369c78636c..2be84b0d24 100644 --- a/apps/web/src/app/vault/core/views/collection-admin.view.ts +++ b/apps/web/src/app/vault/core/views/collection-admin.view.ts @@ -51,6 +51,13 @@ export class CollectionAdminView extends CollectionView { * Whether the user can modify user access to this collection */ canEditUserAccess(org: Organization, flexibleCollectionsV1Enabled: boolean): boolean { - return this.canEdit(org, flexibleCollectionsV1Enabled) || org.canManageUsers; + return this.canEdit(org, flexibleCollectionsV1Enabled) || org.permissions.manageUsers; + } + + /** + * Whether the user can modify group access to this collection + */ + canEditGroupAccess(org: Organization, flexibleCollectionsV1Enabled: boolean): boolean { + return this.canEdit(org, flexibleCollectionsV1Enabled) || org.permissions.manageGroups; } } diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index e37ccaf536..f5fff9a81d 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -6480,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, From ee2f96d3c498714a89d99e6c8c92b609e2cbf149 Mon Sep 17 00:00:00 2001 From: Matt Gibson <mgibson@bitwarden.com> Date: Thu, 2 May 2024 03:10:06 -0400 Subject: [PATCH 338/351] Use a service to track when to open and close offscreen document (#8977) * Use a service to track when to open and close offscreen document There some strangeness around maintaining the offscreen document for more callbacks, that need not have the same reasons and justifications as the original. We'd need to test, but perhaps the intent is something closer to maintaining a work queue ourselves and creating the offscreen page for only a single reason as it comes in, then waiting for that page to close before opening another. * Prefer builtin promise flattening * Await anything and everything --------- Co-authored-by: Cesar Gonzalez <cesar.a.gonzalezcs@gmail.com> --- .../browser/src/background/main.background.ts | 6 ++ .../platform-utils-service.factory.ts | 1 + .../src/platform/browser/browser-api.spec.ts | 26 ----- .../src/platform/browser/browser-api.ts | 28 ----- .../abstractions/offscreen-document.ts | 18 ++-- .../offscreen-document.service.spec.ts | 101 ++++++++++++++++++ .../offscreen-document.service.ts | 41 +++++++ .../background-platform-utils.service.ts | 5 +- .../browser-platform-utils.service.spec.ts | 42 +++++--- .../browser-platform-utils.service.ts | 16 +-- .../foreground-platform-utils.service.ts | 5 +- .../src/popup/services/services.module.ts | 15 ++- 12 files changed, 218 insertions(+), 86 deletions(-) create mode 100644 apps/browser/src/platform/offscreen-document/offscreen-document.service.spec.ts create mode 100644 apps/browser/src/platform/offscreen-document/offscreen-document.service.ts diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 74b21204be..f86a44bc8f 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -212,6 +212,8 @@ import { UpdateBadge } from "../platform/listeners/update-badge"; /* eslint-disable no-restricted-imports */ import { ChromeMessageSender } from "../platform/messaging/chrome-message.sender"; /* eslint-enable no-restricted-imports */ +import { OffscreenDocumentService } from "../platform/offscreen-document/abstractions/offscreen-document"; +import { DefaultOffscreenDocumentService } from "../platform/offscreen-document/offscreen-document.service"; import { BrowserStateService as StateServiceAbstraction } from "../platform/services/abstractions/browser-state.service"; import { BrowserCryptoService } from "../platform/services/browser-crypto.service"; import { BrowserEnvironmentService } from "../platform/services/browser-environment.service"; @@ -336,6 +338,7 @@ export default class MainBackground { userAutoUnlockKeyService: UserAutoUnlockKeyService; scriptInjectorService: BrowserScriptInjectorService; kdfConfigService: kdfConfigServiceAbstraction; + offscreenDocumentService: OffscreenDocumentService; onUpdatedRan: boolean; onReplacedRan: boolean; @@ -393,11 +396,14 @@ export default class MainBackground { ), ); + this.offscreenDocumentService = new DefaultOffscreenDocumentService(); + this.platformUtilsService = new BackgroundPlatformUtilsService( this.messagingService, (clipboardValue, clearMs) => this.clearClipboard(clipboardValue, clearMs), async () => this.biometricUnlock(), self, + this.offscreenDocumentService, ); // Creates a session key for mv3 storage of large memory items diff --git a/apps/browser/src/platform/background/service-factories/platform-utils-service.factory.ts b/apps/browser/src/platform/background/service-factories/platform-utils-service.factory.ts index 6f46d87418..2cd34ba412 100644 --- a/apps/browser/src/platform/background/service-factories/platform-utils-service.factory.ts +++ b/apps/browser/src/platform/background/service-factories/platform-utils-service.factory.ts @@ -30,6 +30,7 @@ export function platformUtilsServiceFactory( opts.platformUtilsServiceOptions.clipboardWriteCallback, opts.platformUtilsServiceOptions.biometricCallback, opts.platformUtilsServiceOptions.win, + null, ), ); } diff --git a/apps/browser/src/platform/browser/browser-api.spec.ts b/apps/browser/src/platform/browser/browser-api.spec.ts index e452d6d8ee..7e0c61c9d1 100644 --- a/apps/browser/src/platform/browser/browser-api.spec.ts +++ b/apps/browser/src/platform/browser/browser-api.spec.ts @@ -525,32 +525,6 @@ describe("BrowserApi", () => { }); }); - describe("createOffscreenDocument", () => { - it("creates the offscreen document with the supplied reasons and justification", async () => { - const reasons = [chrome.offscreen.Reason.CLIPBOARD]; - const justification = "justification"; - - await BrowserApi.createOffscreenDocument(reasons, justification); - - expect(chrome.offscreen.createDocument).toHaveBeenCalledWith({ - url: "offscreen-document/index.html", - reasons, - justification, - }); - }); - }); - - describe("closeOffscreenDocument", () => { - it("closes the offscreen document", () => { - const callbackMock = jest.fn(); - - BrowserApi.closeOffscreenDocument(callbackMock); - - expect(chrome.offscreen.closeDocument).toHaveBeenCalled(); - expect(callbackMock).toHaveBeenCalled(); - }); - }); - describe("registerContentScriptsMv2", () => { const details: browser.contentScripts.RegisteredContentScriptOptions = { matches: ["<all_urls>"], diff --git a/apps/browser/src/platform/browser/browser-api.ts b/apps/browser/src/platform/browser/browser-api.ts index e804cf2b8d..d0695d53fd 100644 --- a/apps/browser/src/platform/browser/browser-api.ts +++ b/apps/browser/src/platform/browser/browser-api.ts @@ -558,34 +558,6 @@ export class BrowserApi { chrome.privacy.services.passwordSavingEnabled.set({ value }); } - /** - * Opens the offscreen document with the given reasons and justification. - * - * @param reasons - List of reasons for opening the offscreen document. - * @see https://developer.chrome.com/docs/extensions/reference/api/offscreen#type-Reason - * @param justification - Custom written justification for opening the offscreen document. - */ - static async createOffscreenDocument(reasons: chrome.offscreen.Reason[], justification: string) { - await chrome.offscreen.createDocument({ - url: "offscreen-document/index.html", - reasons, - justification, - }); - } - - /** - * Closes the offscreen document. - * - * @param callback - Optional callback to execute after the offscreen document is closed. - */ - static closeOffscreenDocument(callback?: () => void) { - chrome.offscreen.closeDocument(() => { - if (callback) { - callback(); - } - }); - } - /** * Handles registration of static content scripts within manifest v2. * diff --git a/apps/browser/src/platform/offscreen-document/abstractions/offscreen-document.ts b/apps/browser/src/platform/offscreen-document/abstractions/offscreen-document.ts index e5aa8c86f5..2d3c6a3e71 100644 --- a/apps/browser/src/platform/offscreen-document/abstractions/offscreen-document.ts +++ b/apps/browser/src/platform/offscreen-document/abstractions/offscreen-document.ts @@ -1,4 +1,4 @@ -type OffscreenDocumentExtensionMessage = { +export type OffscreenDocumentExtensionMessage = { [key: string]: any; command: string; text?: string; @@ -9,18 +9,20 @@ type OffscreenExtensionMessageEventParams = { sender: chrome.runtime.MessageSender; }; -type OffscreenDocumentExtensionMessageHandlers = { +export type OffscreenDocumentExtensionMessageHandlers = { [key: string]: ({ message, sender }: OffscreenExtensionMessageEventParams) => any; offscreenCopyToClipboard: ({ message }: OffscreenExtensionMessageEventParams) => any; offscreenReadFromClipboard: () => any; }; -interface OffscreenDocument { +export interface OffscreenDocument { init(): void; } -export { - OffscreenDocumentExtensionMessage, - OffscreenDocumentExtensionMessageHandlers, - OffscreenDocument, -}; +export abstract class OffscreenDocumentService { + abstract withDocument<T>( + reasons: chrome.offscreen.Reason[], + justification: string, + callback: () => Promise<T> | T, + ): Promise<T>; +} diff --git a/apps/browser/src/platform/offscreen-document/offscreen-document.service.spec.ts b/apps/browser/src/platform/offscreen-document/offscreen-document.service.spec.ts new file mode 100644 index 0000000000..d6be0a924e --- /dev/null +++ b/apps/browser/src/platform/offscreen-document/offscreen-document.service.spec.ts @@ -0,0 +1,101 @@ +import { DefaultOffscreenDocumentService } from "./offscreen-document.service"; + +class TestCase { + synchronicity: string; + private _callback: () => Promise<any> | any; + get callback() { + return jest.fn(this._callback); + } + + constructor(synchronicity: string, callback: () => Promise<any> | any) { + this.synchronicity = synchronicity; + this._callback = callback; + } + + toString() { + return this.synchronicity; + } +} + +describe.each([ + new TestCase("synchronous callback", () => 42), + new TestCase("asynchronous callback", () => Promise.resolve(42)), +])("DefaultOffscreenDocumentService %s", (testCase) => { + let sut: DefaultOffscreenDocumentService; + const reasons = [chrome.offscreen.Reason.TESTING]; + const justification = "justification is testing"; + const url = "offscreen-document/index.html"; + const api = { + createDocument: jest.fn(), + closeDocument: jest.fn(), + hasDocument: jest.fn().mockResolvedValue(false), + Reason: chrome.offscreen.Reason, + }; + let callback: jest.Mock<() => Promise<number> | number>; + + beforeEach(() => { + callback = testCase.callback; + chrome.offscreen = api; + + sut = new DefaultOffscreenDocumentService(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe("withDocument", () => { + it("creates a document when none exists", async () => { + await sut.withDocument(reasons, justification, () => {}); + + expect(chrome.offscreen.createDocument).toHaveBeenCalledWith({ + url, + reasons, + justification, + }); + }); + + it("does not create a document when one exists", async () => { + api.hasDocument.mockResolvedValue(true); + + await sut.withDocument(reasons, justification, callback); + + expect(chrome.offscreen.createDocument).not.toHaveBeenCalled(); + }); + + describe.each([true, false])("hasDocument returns %s", (hasDocument) => { + beforeEach(() => { + api.hasDocument.mockResolvedValue(hasDocument); + }); + + it("calls the callback", async () => { + await sut.withDocument(reasons, justification, callback); + + expect(callback).toHaveBeenCalled(); + }); + + it("returns the callback result", async () => { + const result = await sut.withDocument(reasons, justification, callback); + + expect(result).toBe(42); + }); + + it("closes the document when the callback completes and no other callbacks are running", async () => { + await sut.withDocument(reasons, justification, callback); + + expect(chrome.offscreen.closeDocument).toHaveBeenCalled(); + }); + + it("does not close the document when the callback completes and other callbacks are running", async () => { + await Promise.all([ + sut.withDocument(reasons, justification, callback), + sut.withDocument(reasons, justification, callback), + sut.withDocument(reasons, justification, callback), + sut.withDocument(reasons, justification, callback), + ]); + + expect(chrome.offscreen.closeDocument).toHaveBeenCalledTimes(1); + }); + }); + }); +}); diff --git a/apps/browser/src/platform/offscreen-document/offscreen-document.service.ts b/apps/browser/src/platform/offscreen-document/offscreen-document.service.ts new file mode 100644 index 0000000000..da0ca38269 --- /dev/null +++ b/apps/browser/src/platform/offscreen-document/offscreen-document.service.ts @@ -0,0 +1,41 @@ +export class DefaultOffscreenDocumentService implements DefaultOffscreenDocumentService { + private workerCount = 0; + + constructor() {} + + async withDocument<T>( + reasons: chrome.offscreen.Reason[], + justification: string, + callback: () => Promise<T> | T, + ): Promise<T> { + this.workerCount++; + try { + if (!(await this.documentExists())) { + await this.create(reasons, justification); + } + + return await callback(); + } finally { + this.workerCount--; + if (this.workerCount === 0) { + await this.close(); + } + } + } + + private async create(reasons: chrome.offscreen.Reason[], justification: string): Promise<void> { + await chrome.offscreen.createDocument({ + url: "offscreen-document/index.html", + reasons, + justification, + }); + } + + private async close(): Promise<void> { + await chrome.offscreen.closeDocument(); + } + + private async documentExists(): Promise<boolean> { + return await chrome.offscreen.hasDocument(); + } +} diff --git a/apps/browser/src/platform/services/platform-utils/background-platform-utils.service.ts b/apps/browser/src/platform/services/platform-utils/background-platform-utils.service.ts index 27ed3f016b..ec26d6aa29 100644 --- a/apps/browser/src/platform/services/platform-utils/background-platform-utils.service.ts +++ b/apps/browser/src/platform/services/platform-utils/background-platform-utils.service.ts @@ -1,5 +1,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { OffscreenDocumentService } from "../../offscreen-document/abstractions/offscreen-document"; + import { BrowserPlatformUtilsService } from "./browser-platform-utils.service"; export class BackgroundPlatformUtilsService extends BrowserPlatformUtilsService { @@ -8,8 +10,9 @@ export class BackgroundPlatformUtilsService extends BrowserPlatformUtilsService clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void, biometricCallback: () => Promise<boolean>, win: Window & typeof globalThis, + offscreenDocumentService: OffscreenDocumentService, ) { - super(clipboardWriteCallback, biometricCallback, win); + super(clipboardWriteCallback, biometricCallback, win, offscreenDocumentService); } override showToast( diff --git a/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.spec.ts b/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.spec.ts index 0df8f26344..02c10b62cc 100644 --- a/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.spec.ts +++ b/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.spec.ts @@ -1,15 +1,22 @@ +import { MockProxy, mock } from "jest-mock-extended"; + import { DeviceType } from "@bitwarden/common/enums"; import { flushPromises } from "../../../autofill/spec/testing-utils"; import { SafariApp } from "../../../browser/safariApp"; import { BrowserApi } from "../../browser/browser-api"; +import { OffscreenDocumentService } from "../../offscreen-document/abstractions/offscreen-document"; import BrowserClipboardService from "../browser-clipboard.service"; import { BrowserPlatformUtilsService } from "./browser-platform-utils.service"; class TestBrowserPlatformUtilsService extends BrowserPlatformUtilsService { - constructor(clipboardSpy: jest.Mock, win: Window & typeof globalThis) { - super(clipboardSpy, null, win); + constructor( + clipboardSpy: jest.Mock, + win: Window & typeof globalThis, + offscreenDocumentService: OffscreenDocumentService, + ) { + super(clipboardSpy, null, win, offscreenDocumentService); } showToast( @@ -24,13 +31,16 @@ class TestBrowserPlatformUtilsService extends BrowserPlatformUtilsService { describe("Browser Utils Service", () => { let browserPlatformUtilsService: BrowserPlatformUtilsService; + let offscreenDocumentService: MockProxy<OffscreenDocumentService>; const clipboardWriteCallbackSpy = jest.fn(); beforeEach(() => { + offscreenDocumentService = mock(); (window as any).matchMedia = jest.fn().mockReturnValueOnce({}); browserPlatformUtilsService = new TestBrowserPlatformUtilsService( clipboardWriteCallbackSpy, window, + offscreenDocumentService, ); }); @@ -223,23 +233,23 @@ describe("Browser Utils Service", () => { .spyOn(browserPlatformUtilsService, "getDevice") .mockReturnValue(DeviceType.ChromeExtension); getManifestVersionSpy.mockReturnValue(3); - jest.spyOn(BrowserApi, "createOffscreenDocument"); - jest.spyOn(BrowserApi, "sendMessageWithResponse").mockResolvedValue(undefined); - jest.spyOn(BrowserApi, "closeOffscreenDocument"); browserPlatformUtilsService.copyToClipboard(text); await flushPromises(); expect(triggerOffscreenCopyToClipboardSpy).toHaveBeenCalledWith(text); expect(clipboardServiceCopySpy).not.toHaveBeenCalled(); - expect(BrowserApi.createOffscreenDocument).toHaveBeenCalledWith( + expect(offscreenDocumentService.withDocument).toHaveBeenCalledWith( [chrome.offscreen.Reason.CLIPBOARD], "Write text to the clipboard.", + expect.any(Function), ); + + const callback = offscreenDocumentService.withDocument.mock.calls[0][2]; + await callback(); expect(BrowserApi.sendMessageWithResponse).toHaveBeenCalledWith("offscreenCopyToClipboard", { text, }); - expect(BrowserApi.closeOffscreenDocument).toHaveBeenCalled(); }); it("skips the clipboardWriteCallback if the clipboard is clearing", async () => { @@ -298,18 +308,21 @@ describe("Browser Utils Service", () => { .spyOn(browserPlatformUtilsService, "getDevice") .mockReturnValue(DeviceType.ChromeExtension); getManifestVersionSpy.mockReturnValue(3); - jest.spyOn(BrowserApi, "createOffscreenDocument"); - jest.spyOn(BrowserApi, "sendMessageWithResponse").mockResolvedValue("test"); - jest.spyOn(BrowserApi, "closeOffscreenDocument"); + offscreenDocumentService.withDocument.mockImplementationOnce((_, __, callback) => + Promise.resolve("test"), + ); await browserPlatformUtilsService.readFromClipboard(); - expect(BrowserApi.createOffscreenDocument).toHaveBeenCalledWith( + expect(offscreenDocumentService.withDocument).toHaveBeenCalledWith( [chrome.offscreen.Reason.CLIPBOARD], "Read text from the clipboard.", + expect.any(Function), ); + + const callback = offscreenDocumentService.withDocument.mock.calls[0][2]; + await callback(); expect(BrowserApi.sendMessageWithResponse).toHaveBeenCalledWith("offscreenReadFromClipboard"); - expect(BrowserApi.closeOffscreenDocument).toHaveBeenCalled(); }); it("returns an empty string from the offscreen document if the response is not of type string", async () => { @@ -317,9 +330,10 @@ describe("Browser Utils Service", () => { .spyOn(browserPlatformUtilsService, "getDevice") .mockReturnValue(DeviceType.ChromeExtension); getManifestVersionSpy.mockReturnValue(3); - jest.spyOn(BrowserApi, "createOffscreenDocument"); jest.spyOn(BrowserApi, "sendMessageWithResponse").mockResolvedValue(1); - jest.spyOn(BrowserApi, "closeOffscreenDocument"); + offscreenDocumentService.withDocument.mockImplementationOnce((_, __, callback) => + Promise.resolve(1), + ); const result = await browserPlatformUtilsService.readFromClipboard(); diff --git a/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.ts b/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.ts index 6e3b3aa403..855492521b 100644 --- a/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.ts +++ b/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.ts @@ -6,6 +6,7 @@ import { import { SafariApp } from "../../../browser/safariApp"; import { BrowserApi } from "../../browser/browser-api"; +import { OffscreenDocumentService } from "../../offscreen-document/abstractions/offscreen-document"; import BrowserClipboardService from "../browser-clipboard.service"; export abstract class BrowserPlatformUtilsService implements PlatformUtilsService { @@ -15,6 +16,7 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic private clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void, private biometricCallback: () => Promise<boolean>, private globalContext: Window | ServiceWorkerGlobalScope, + private offscreenDocumentService: OffscreenDocumentService, ) {} static getDevice(globalContext: Window | ServiceWorkerGlobalScope): DeviceType { @@ -316,24 +318,26 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic * Triggers the offscreen document API to copy the text to the clipboard. */ private async triggerOffscreenCopyToClipboard(text: string) { - await BrowserApi.createOffscreenDocument( + await this.offscreenDocumentService.withDocument( [chrome.offscreen.Reason.CLIPBOARD], "Write text to the clipboard.", + async () => { + await BrowserApi.sendMessageWithResponse("offscreenCopyToClipboard", { text }); + }, ); - await BrowserApi.sendMessageWithResponse("offscreenCopyToClipboard", { text }); - BrowserApi.closeOffscreenDocument(); } /** * Triggers the offscreen document API to read the text from the clipboard. */ private async triggerOffscreenReadFromClipboard() { - await BrowserApi.createOffscreenDocument( + const response = await this.offscreenDocumentService.withDocument( [chrome.offscreen.Reason.CLIPBOARD], "Read text from the clipboard.", + async () => { + return await BrowserApi.sendMessageWithResponse("offscreenReadFromClipboard"); + }, ); - const response = await BrowserApi.sendMessageWithResponse("offscreenReadFromClipboard"); - BrowserApi.closeOffscreenDocument(); if (typeof response === "string") { return response; } diff --git a/apps/browser/src/platform/services/platform-utils/foreground-platform-utils.service.ts b/apps/browser/src/platform/services/platform-utils/foreground-platform-utils.service.ts index 24aa45d5c3..f775f049e7 100644 --- a/apps/browser/src/platform/services/platform-utils/foreground-platform-utils.service.ts +++ b/apps/browser/src/platform/services/platform-utils/foreground-platform-utils.service.ts @@ -1,5 +1,7 @@ import { ToastService } from "@bitwarden/components"; +import { OffscreenDocumentService } from "../../offscreen-document/abstractions/offscreen-document"; + import { BrowserPlatformUtilsService } from "./browser-platform-utils.service"; export class ForegroundPlatformUtilsService extends BrowserPlatformUtilsService { @@ -8,8 +10,9 @@ export class ForegroundPlatformUtilsService extends BrowserPlatformUtilsService clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void, biometricCallback: () => Promise<boolean>, win: Window & typeof globalThis, + offscreenDocumentService: OffscreenDocumentService, ) { - super(clipboardWriteCallback, biometricCallback, win); + super(clipboardWriteCallback, biometricCallback, win, offscreenDocumentService); } override showToast( diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 163b2f1edb..6e7d3c2230 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -100,6 +100,8 @@ import { runInsideAngular } from "../../platform/browser/run-inside-angular.oper /* eslint-disable no-restricted-imports */ import { ChromeMessageSender } from "../../platform/messaging/chrome-message.sender"; /* eslint-enable no-restricted-imports */ +import { OffscreenDocumentService } from "../../platform/offscreen-document/abstractions/offscreen-document"; +import { DefaultOffscreenDocumentService } from "../../platform/offscreen-document/offscreen-document.service"; import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; import { BrowserFileDownloadService } from "../../platform/popup/services/browser-file-download.service"; import { BrowserStateService as StateServiceAbstraction } from "../../platform/services/abstractions/browser-state.service"; @@ -287,9 +289,17 @@ const safeProviders: SafeProvider[] = [ useFactory: getBgService<DevicesServiceAbstraction>("devicesService"), deps: [], }), + safeProvider({ + provide: OffscreenDocumentService, + useClass: DefaultOffscreenDocumentService, + deps: [], + }), safeProvider({ provide: PlatformUtilsService, - useFactory: (toastService: ToastService) => { + useFactory: ( + toastService: ToastService, + offscreenDocumentService: OffscreenDocumentService, + ) => { return new ForegroundPlatformUtilsService( toastService, (clipboardValue: string, clearMs: number) => { @@ -306,9 +316,10 @@ const safeProviders: SafeProvider[] = [ return response.result; }, window, + offscreenDocumentService, ); }, - deps: [ToastService], + deps: [ToastService, OffscreenDocumentService], }), safeProvider({ provide: PasswordGenerationServiceAbstraction, From d8bdad9f226fa70a47792f2a36089ed04a673269 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Thu, 2 May 2024 08:45:55 -0500 Subject: [PATCH 339/351] [AC-2140] Swap Admin Console icon (#8973) * swap admin console icon to `bwi-user-monitor` * use max-width to force wrapping of product switcher text * remove duplicate style --- .../product-switcher-content.component.html | 7 ++++--- .../product-switcher/product-switcher-content.component.ts | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) 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 f038fafecc..62d8b6a075 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 @@ -21,9 +21,10 @@ ariaCurrentWhenActive="page" > <i class="bwi {{ product.icon }} tw-text-4xl !tw-m-0 !tw-mb-1"></i> - <span class="tw-text-center tw-text-sm tw-leading-snug group-hover:tw-underline">{{ - product.name - }}</span> + <span + class="tw-max-w-24 tw-text-center tw-text-sm tw-leading-snug group-hover:tw-underline" + >{{ product.name }}</span + > </a> </section> diff --git a/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.ts b/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.ts index 398105a75f..9495894432 100644 --- a/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.ts +++ b/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.ts @@ -97,7 +97,7 @@ export class ProductSwitcherContentComponent { }, ac: { name: "Admin Console", - icon: "bwi-business", + icon: "bwi-user-monitor", appRoute: ["/organizations", acOrg?.id], marketingRoute: "https://bitwarden.com/products/business/", isActive: this.router.url.includes("/organizations/"), From 8b28eee3a75e5c890f28f21497387da57daf163c Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Thu, 2 May 2024 15:46:32 +0200 Subject: [PATCH 340/351] [PM-7701] Clean up services module (#8907) * Remove usage of getBgService for CipherService With CipherService using StateProviders: https://github.com/bitwarden/clients/pull/8314 - we should no longer need CipherService * Remove usage of getBgService for CollectionService With CollectionService using StateProviders: https://github.com/bitwarden/clients/pull/7732 - we should no longer need CollectionService --------- Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com> --- apps/browser/src/popup/services/services.module.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 6e7d3c2230..268ee9fa81 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -187,21 +187,11 @@ const safeProviders: SafeProvider[] = [ useClass: PopupSearchService, deps: [LogService, I18nServiceAbstraction, StateProvider], }), - safeProvider({ - provide: CipherService, - useFactory: getBgService<CipherService>("cipherService"), - deps: [], - }), safeProvider({ provide: CryptoFunctionService, useFactory: () => new WebCryptoFunctionService(window), deps: [], }), - safeProvider({ - provide: CollectionService, - useFactory: getBgService<CollectionService>("collectionService"), - deps: [], - }), safeProvider({ provide: LogService, useFactory: (platformUtilsService: PlatformUtilsService) => From 26988730b1b166d9a6e83e6d14b527453dd26003 Mon Sep 17 00:00:00 2001 From: Jonathan Prusik <jprusik@users.noreply.github.com> Date: Thu, 2 May 2024 11:19:00 -0400 Subject: [PATCH 341/351] [PM-7627] [MV3] Do not run fido2 content scripts on browser settings or extension pages (#8863) * do no run fido2 content scripts on browser settings or extension background pages * remove unneeded overlay visibility setting state guard * only filter content script and page script and update test * handle content script host permission errors * add activeTab to mv3 permissions * allow other browser inject errors to throw --- .../services/autofill.service.spec.ts | 4 +- .../src/autofill/services/autofill.service.ts | 15 +--- .../browser/src/background/main.background.ts | 5 +- apps/browser/src/manifest.v3.json | 4 +- ...browser-script-injector-service.factory.ts | 18 ++++- .../browser-script-injector.service.spec.ts | 9 ++- .../browser-script-injector.service.ts | 33 +++++++- .../src/popup/services/services.module.ts | 2 +- .../fileless-importer.background.spec.ts | 6 +- .../fido2/background/fido2.background.ts | 8 +- .../fido2/content/content-script.spec.ts | 76 ++++++++++++++----- .../src/vault/fido2/content/content-script.ts | 6 +- .../src/vault/fido2/content/page-script.ts | 7 +- .../page-script.webauthn-supported.spec.ts | 69 +++++++++++++++-- .../page-script.webauthn-unsupported.spec.ts | 31 ++++++-- 15 files changed, 232 insertions(+), 61 deletions(-) diff --git a/apps/browser/src/autofill/services/autofill.service.spec.ts b/apps/browser/src/autofill/services/autofill.service.spec.ts index d1fbf79bfa..158fde6a56 100644 --- a/apps/browser/src/autofill/services/autofill.service.spec.ts +++ b/apps/browser/src/autofill/services/autofill.service.spec.ts @@ -12,6 +12,7 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs import { EventType } from "@bitwarden/common/enums"; import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service"; import { @@ -74,9 +75,10 @@ describe("AutofillService", () => { const logService = mock<LogService>(); const userVerificationService = mock<UserVerificationService>(); const billingAccountProfileStateService = mock<BillingAccountProfileStateService>(); + const platformUtilsService = mock<PlatformUtilsService>(); beforeEach(() => { - scriptInjectorService = new BrowserScriptInjectorService(); + scriptInjectorService = new BrowserScriptInjectorService(platformUtilsService, logService); autofillService = new AutofillService( cipherService, autofillSettingsService, diff --git a/apps/browser/src/autofill/services/autofill.service.ts b/apps/browser/src/autofill/services/autofill.service.ts index 10e2d84361..dd87505441 100644 --- a/apps/browser/src/autofill/services/autofill.service.ts +++ b/apps/browser/src/autofill/services/autofill.service.ts @@ -3,7 +3,6 @@ import { firstValueFrom } from "rxjs"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; -import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types"; @@ -107,17 +106,13 @@ export default class AutofillService implements AutofillServiceInterface { frameId = 0, triggeringOnPageLoad = true, ): Promise<void> { - // Autofill settings loaded from state can await the active account state indefinitely if - // not guarded by an active account check (e.g. the user is logged in) + // Autofill user settings loaded from state can await the active account state indefinitely + // if not guarded by an active account check (e.g. the user is logged in) const activeAccount = await firstValueFrom(this.accountService.activeAccount$); - // These settings are not available until the user logs in - let overlayVisibility: InlineMenuVisibilitySetting = AutofillOverlayVisibility.Off; let autoFillOnPageLoadIsEnabled = false; + const overlayVisibility = await this.getOverlayVisibility(); - if (activeAccount) { - overlayVisibility = await this.getOverlayVisibility(); - } const mainAutofillScript = overlayVisibility ? "bootstrap-autofill-overlay.js" : "bootstrap-autofill.js"; @@ -2087,9 +2082,7 @@ export default class AutofillService implements AutofillServiceInterface { for (let index = 0; index < tabs.length; index++) { const tab = tabs[index]; if (tab.url?.startsWith("http")) { - // 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.injectAutofillScripts(tab, 0, false); + void this.injectAutofillScripts(tab, 0, false); } } } diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index f86a44bc8f..067fae1de8 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -813,7 +813,10 @@ export default class MainBackground { ); this.totpService = new TotpService(this.cryptoFunctionService, this.logService); - this.scriptInjectorService = new BrowserScriptInjectorService(); + this.scriptInjectorService = new BrowserScriptInjectorService( + this.platformUtilsService, + this.logService, + ); this.autofillService = new AutofillService( this.cipherService, this.autofillSettingsService, diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index b7aaba2e0e..93d798490f 100644 --- a/apps/browser/src/manifest.v3.json +++ b/apps/browser/src/manifest.v3.json @@ -51,7 +51,7 @@ "default_popup": "popup/index.html" }, "permissions": [ - "<all_urls>", + "activeTab", "tabs", "contextMenus", "storage", @@ -65,7 +65,7 @@ "webRequestAuthProvider" ], "optional_permissions": ["nativeMessaging", "privacy"], - "host_permissions": ["<all_urls>"], + "host_permissions": ["https://*/*", "http://*/*"], "content_security_policy": { "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'", "sandbox": "sandbox allow-scripts; script-src 'self'" diff --git a/apps/browser/src/platform/background/service-factories/browser-script-injector-service.factory.ts b/apps/browser/src/platform/background/service-factories/browser-script-injector-service.factory.ts index e3bc687f28..e9a8ee379a 100644 --- a/apps/browser/src/platform/background/service-factories/browser-script-injector-service.factory.ts +++ b/apps/browser/src/platform/background/service-factories/browser-script-injector-service.factory.ts @@ -1,10 +1,20 @@ +import { + LogServiceInitOptions, + logServiceFactory, +} from "../../background/service-factories/log-service.factory"; import { BrowserScriptInjectorService } from "../../services/browser-script-injector.service"; import { CachedServices, FactoryOptions, factory } from "./factory-options"; +import { + PlatformUtilsServiceInitOptions, + platformUtilsServiceFactory, +} from "./platform-utils-service.factory"; type BrowserScriptInjectorServiceOptions = FactoryOptions; -export type BrowserScriptInjectorServiceInitOptions = BrowserScriptInjectorServiceOptions; +export type BrowserScriptInjectorServiceInitOptions = BrowserScriptInjectorServiceOptions & + PlatformUtilsServiceInitOptions & + LogServiceInitOptions; export function browserScriptInjectorServiceFactory( cache: { browserScriptInjectorService?: BrowserScriptInjectorService } & CachedServices, @@ -14,6 +24,10 @@ export function browserScriptInjectorServiceFactory( cache, "browserScriptInjectorService", opts, - async () => new BrowserScriptInjectorService(), + async () => + new BrowserScriptInjectorService( + await platformUtilsServiceFactory(cache, opts), + await logServiceFactory(cache, opts), + ), ); } diff --git a/apps/browser/src/platform/services/browser-script-injector.service.spec.ts b/apps/browser/src/platform/services/browser-script-injector.service.spec.ts index 6ae84c6464..d6ec3dfde9 100644 --- a/apps/browser/src/platform/services/browser-script-injector.service.spec.ts +++ b/apps/browser/src/platform/services/browser-script-injector.service.spec.ts @@ -1,3 +1,8 @@ +import { mock } from "jest-mock-extended"; + +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; + import { BrowserApi } from "../browser/browser-api"; import { @@ -20,9 +25,11 @@ describe("ScriptInjectorService", () => { let scriptInjectorService: BrowserScriptInjectorService; jest.spyOn(BrowserApi, "executeScriptInTab").mockImplementation(); jest.spyOn(BrowserApi, "isManifestVersion"); + const platformUtilsService = mock<PlatformUtilsService>(); + const logService = mock<LogService>(); beforeEach(() => { - scriptInjectorService = new BrowserScriptInjectorService(); + scriptInjectorService = new BrowserScriptInjectorService(platformUtilsService, logService); }); describe("inject", () => { diff --git a/apps/browser/src/platform/services/browser-script-injector.service.ts b/apps/browser/src/platform/services/browser-script-injector.service.ts index 54513188d5..5b3a10ef2b 100644 --- a/apps/browser/src/platform/services/browser-script-injector.service.ts +++ b/apps/browser/src/platform/services/browser-script-injector.service.ts @@ -1,3 +1,6 @@ +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; + import { BrowserApi } from "../browser/browser-api"; import { @@ -7,6 +10,13 @@ import { } from "./abstractions/script-injector.service"; export class BrowserScriptInjectorService extends ScriptInjectorService { + constructor( + private readonly platformUtilsService: PlatformUtilsService, + private readonly logService: LogService, + ) { + super(); + } + /** * Facilitates the injection of a script into a tab context. Will adjust * behavior between manifest v2 and v3 based on the passed configuration. @@ -23,9 +33,26 @@ export class BrowserScriptInjectorService extends ScriptInjectorService { const injectionDetails = this.buildInjectionDetails(injectDetails, file); if (BrowserApi.isManifestVersion(3)) { - await BrowserApi.executeScriptInTab(tabId, injectionDetails, { - world: mv3Details?.world ?? "ISOLATED", - }); + try { + await BrowserApi.executeScriptInTab(tabId, injectionDetails, { + world: mv3Details?.world ?? "ISOLATED", + }); + } catch (error) { + // Swallow errors for host permissions, since this is believed to be a Manifest V3 Chrome bug + // @TODO remove when the bugged behaviour is resolved + if ( + error.message !== + "Cannot access contents of the page. Extension manifest must request permission to access the respective host." + ) { + throw error; + } + + if (this.platformUtilsService.isDev()) { + this.logService.warning( + `BrowserApi.executeScriptInTab exception for ${injectDetails.file} in tab ${tabId}: ${error.message}`, + ); + } + } return; } diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 268ee9fa81..36cbdf6292 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -353,7 +353,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: ScriptInjectorService, useClass: BrowserScriptInjectorService, - deps: [], + deps: [PlatformUtilsService, LogService], }), safeProvider({ provide: KeyConnectorService, diff --git a/apps/browser/src/tools/background/fileless-importer.background.spec.ts b/apps/browser/src/tools/background/fileless-importer.background.spec.ts index 7d226fcd9d..7b356b18fd 100644 --- a/apps/browser/src/tools/background/fileless-importer.background.spec.ts +++ b/apps/browser/src/tools/background/fileless-importer.background.spec.ts @@ -5,6 +5,8 @@ import { PolicyService } from "@bitwarden/common/admin-console/services/policy/p import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AuthService } from "@bitwarden/common/auth/services/auth.service"; 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 { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { Importer, ImportResult, ImportServiceAbstraction } from "@bitwarden/importer/core"; @@ -38,10 +40,12 @@ describe("FilelessImporterBackground ", () => { const notificationBackground = mock<NotificationBackground>(); const importService = mock<ImportServiceAbstraction>(); const syncService = mock<SyncService>(); + const platformUtilsService = mock<PlatformUtilsService>(); + const logService = mock<LogService>(); let scriptInjectorService: BrowserScriptInjectorService; beforeEach(() => { - scriptInjectorService = new BrowserScriptInjectorService(); + scriptInjectorService = new BrowserScriptInjectorService(platformUtilsService, logService); filelessImporterBackground = new FilelessImporterBackground( configService, authService, diff --git a/apps/browser/src/vault/fido2/background/fido2.background.ts b/apps/browser/src/vault/fido2/background/fido2.background.ts index 856874cee3..5e51e05d77 100644 --- a/apps/browser/src/vault/fido2/background/fido2.background.ts +++ b/apps/browser/src/vault/fido2/background/fido2.background.ts @@ -70,13 +70,13 @@ export class Fido2Background implements Fido2BackgroundInterface { */ async injectFido2ContentScriptsInAllTabs() { const tabs = await BrowserApi.tabsQuery({}); + for (let index = 0; index < tabs.length; index++) { const tab = tabs[index]; - if (!tab.url?.startsWith("https")) { - continue; - } - void this.injectFido2ContentScripts(tab); + if (tab.url?.startsWith("https")) { + void this.injectFido2ContentScripts(tab); + } } } diff --git a/apps/browser/src/vault/fido2/content/content-script.spec.ts b/apps/browser/src/vault/fido2/content/content-script.spec.ts index 29d3e9c257..0c2a52ed10 100644 --- a/apps/browser/src/vault/fido2/content/content-script.spec.ts +++ b/apps/browser/src/vault/fido2/content/content-script.spec.ts @@ -15,17 +15,43 @@ jest.mock("../../../autofill/utils", () => ({ }), })); +const originalGlobalThis = globalThis; +const mockGlobalThisDocument = { + ...originalGlobalThis.document, + contentType: "text/html", + location: { + ...originalGlobalThis.document.location, + href: "https://localhost", + origin: "https://localhost", + protocol: "https:", + }, +}; + describe("Fido2 Content Script", () => { + beforeAll(() => { + (jest.spyOn(globalThis, "document", "get") as jest.Mock).mockImplementation( + () => mockGlobalThisDocument, + ); + }); + + afterEach(() => { + jest.resetModules(); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + let messenger: Messenger; const messengerForDOMCommunicationSpy = jest .spyOn(Messenger, "forDOMCommunication") - .mockImplementation((window) => { - const windowOrigin = window.location.origin; + .mockImplementation((context) => { + const windowOrigin = context.location.origin; messenger = new Messenger({ - postMessage: (message, port) => window.postMessage(message, windowOrigin, [port]), - addEventListener: (listener) => window.addEventListener("message", listener), - removeEventListener: (listener) => window.removeEventListener("message", listener), + postMessage: (message, port) => context.postMessage(message, windowOrigin, [port]), + addEventListener: (listener) => context.addEventListener("message", listener), + removeEventListener: (listener) => context.removeEventListener("message", listener), }); messenger.destroy = jest.fn(); return messenger; @@ -33,16 +59,6 @@ describe("Fido2 Content Script", () => { const portSpy: MockProxy<chrome.runtime.Port> = createPortSpyMock(Fido2PortName.InjectedScript); chrome.runtime.connect = jest.fn(() => portSpy); - afterEach(() => { - Object.defineProperty(document, "contentType", { - value: "text/html", - writable: true, - }); - - jest.clearAllMocks(); - jest.resetModules(); - }); - it("destroys the messenger when the port is disconnected", () => { require("./content-script"); @@ -151,11 +167,31 @@ describe("Fido2 Content Script", () => { await expect(result).rejects.toEqual(errorMessage); }); - it("skips initializing the content script if the document content type is not 'text/html'", () => { - Object.defineProperty(document, "contentType", { - value: "application/json", - writable: true, - }); + it("skips initializing if the document content type is not 'text/html'", () => { + jest.clearAllMocks(); + + (jest.spyOn(globalThis, "document", "get") as jest.Mock).mockImplementation(() => ({ + ...mockGlobalThisDocument, + contentType: "application/json", + })); + + require("./content-script"); + + expect(messengerForDOMCommunicationSpy).not.toHaveBeenCalled(); + }); + + it("skips initializing if the document location protocol is not 'https'", () => { + jest.clearAllMocks(); + + (jest.spyOn(globalThis, "document", "get") as jest.Mock).mockImplementation(() => ({ + ...mockGlobalThisDocument, + location: { + ...mockGlobalThisDocument.location, + href: "http://localhost", + origin: "http://localhost", + protocol: "http:", + }, + })); require("./content-script"); diff --git a/apps/browser/src/vault/fido2/content/content-script.ts b/apps/browser/src/vault/fido2/content/content-script.ts index 809db11553..fe3aafe9fb 100644 --- a/apps/browser/src/vault/fido2/content/content-script.ts +++ b/apps/browser/src/vault/fido2/content/content-script.ts @@ -15,7 +15,11 @@ import { import { MessageWithMetadata, Messenger } from "./messaging/messenger"; (function (globalContext) { - if (globalContext.document.contentType !== "text/html") { + const shouldExecuteContentScript = + globalContext.document.contentType === "text/html" && + globalContext.document.location.protocol === "https:"; + + if (!shouldExecuteContentScript) { return; } diff --git a/apps/browser/src/vault/fido2/content/page-script.ts b/apps/browser/src/vault/fido2/content/page-script.ts index 1de0f3258a..5b04f7c1dd 100644 --- a/apps/browser/src/vault/fido2/content/page-script.ts +++ b/apps/browser/src/vault/fido2/content/page-script.ts @@ -6,9 +6,14 @@ import { MessageType } from "./messaging/message"; import { Messenger } from "./messaging/messenger"; (function (globalContext) { - if (globalContext.document.contentType !== "text/html") { + const shouldExecuteContentScript = + globalContext.document.contentType === "text/html" && + globalContext.document.location.protocol === "https:"; + + if (!shouldExecuteContentScript) { return; } + const BrowserPublicKeyCredential = globalContext.PublicKeyCredential; const BrowserNavigatorCredentials = navigator.credentials; const BrowserAuthenticatorAttestationResponse = globalContext.AuthenticatorAttestationResponse; diff --git a/apps/browser/src/vault/fido2/content/page-script.webauthn-supported.spec.ts b/apps/browser/src/vault/fido2/content/page-script.webauthn-supported.spec.ts index 211959c466..c235d53cb0 100644 --- a/apps/browser/src/vault/fido2/content/page-script.webauthn-supported.spec.ts +++ b/apps/browser/src/vault/fido2/content/page-script.webauthn-supported.spec.ts @@ -10,17 +10,29 @@ import { WebauthnUtils } from "../webauthn-utils"; import { MessageType } from "./messaging/message"; import { Messenger } from "./messaging/messenger"; +const originalGlobalThis = globalThis; +const mockGlobalThisDocument = { + ...originalGlobalThis.document, + contentType: "text/html", + location: { + ...originalGlobalThis.document.location, + href: "https://localhost", + origin: "https://localhost", + protocol: "https:", + }, +}; + let messenger: Messenger; jest.mock("./messaging/messenger", () => { return { Messenger: class extends jest.requireActual("./messaging/messenger").Messenger { - static forDOMCommunication: any = jest.fn((window) => { - const windowOrigin = window.location.origin; + static forDOMCommunication: any = jest.fn((context) => { + const windowOrigin = context.location.origin; messenger = new Messenger({ - postMessage: (message, port) => window.postMessage(message, windowOrigin, [port]), - addEventListener: (listener) => window.addEventListener("message", listener), - removeEventListener: (listener) => window.removeEventListener("message", listener), + postMessage: (message, port) => context.postMessage(message, windowOrigin, [port]), + addEventListener: (listener) => context.addEventListener("message", listener), + removeEventListener: (listener) => context.removeEventListener("message", listener), }); messenger.destroy = jest.fn(); return messenger; @@ -31,6 +43,10 @@ jest.mock("./messaging/messenger", () => { jest.mock("../webauthn-utils"); describe("Fido2 page script with native WebAuthn support", () => { + (jest.spyOn(globalThis, "document", "get") as jest.Mock).mockImplementation( + () => mockGlobalThisDocument, + ); + const mockCredentialCreationOptions = createCredentialCreationOptionsMock(); const mockCreateCredentialsResult = createCreateCredentialResultMock(); const mockCredentialRequestOptions = createCredentialRequestOptionsMock(); @@ -39,9 +55,12 @@ describe("Fido2 page script with native WebAuthn support", () => { require("./page-script"); + afterEach(() => { + jest.resetModules(); + }); + afterAll(() => { jest.clearAllMocks(); - jest.resetModules(); }); describe("creating WebAuthn credentials", () => { @@ -118,4 +137,42 @@ describe("Fido2 page script with native WebAuthn support", () => { expect(messenger.destroy).toHaveBeenCalled(); }); }); + + describe("content script execution", () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.resetModules(); + }); + + it("skips initializing if the document content type is not 'text/html'", () => { + jest.spyOn(Messenger, "forDOMCommunication"); + + (jest.spyOn(globalThis, "document", "get") as jest.Mock).mockImplementation(() => ({ + ...mockGlobalThisDocument, + contentType: "json/application", + })); + + require("./content-script"); + + expect(Messenger.forDOMCommunication).not.toHaveBeenCalled(); + }); + + it("skips initializing if the document location protocol is not 'https'", () => { + jest.spyOn(Messenger, "forDOMCommunication"); + + (jest.spyOn(globalThis, "document", "get") as jest.Mock).mockImplementation(() => ({ + ...mockGlobalThisDocument, + location: { + ...mockGlobalThisDocument.location, + href: "http://localhost", + origin: "http://localhost", + protocol: "http:", + }, + })); + + require("./content-script"); + + expect(Messenger.forDOMCommunication).not.toHaveBeenCalled(); + }); + }); }); diff --git a/apps/browser/src/vault/fido2/content/page-script.webauthn-unsupported.spec.ts b/apps/browser/src/vault/fido2/content/page-script.webauthn-unsupported.spec.ts index f3aee685e1..4b1f839a1d 100644 --- a/apps/browser/src/vault/fido2/content/page-script.webauthn-unsupported.spec.ts +++ b/apps/browser/src/vault/fido2/content/page-script.webauthn-unsupported.spec.ts @@ -9,17 +9,29 @@ import { WebauthnUtils } from "../webauthn-utils"; import { MessageType } from "./messaging/message"; import { Messenger } from "./messaging/messenger"; +const originalGlobalThis = globalThis; +const mockGlobalThisDocument = { + ...originalGlobalThis.document, + contentType: "text/html", + location: { + ...originalGlobalThis.document.location, + href: "https://localhost", + origin: "https://localhost", + protocol: "https:", + }, +}; + let messenger: Messenger; jest.mock("./messaging/messenger", () => { return { Messenger: class extends jest.requireActual("./messaging/messenger").Messenger { - static forDOMCommunication: any = jest.fn((window) => { - const windowOrigin = window.location.origin; + static forDOMCommunication: any = jest.fn((context) => { + const windowOrigin = context.location.origin; messenger = new Messenger({ - postMessage: (message, port) => window.postMessage(message, windowOrigin, [port]), - addEventListener: (listener) => window.addEventListener("message", listener), - removeEventListener: (listener) => window.removeEventListener("message", listener), + postMessage: (message, port) => context.postMessage(message, windowOrigin, [port]), + addEventListener: (listener) => context.addEventListener("message", listener), + removeEventListener: (listener) => context.removeEventListener("message", listener), }); messenger.destroy = jest.fn(); return messenger; @@ -30,15 +42,22 @@ jest.mock("./messaging/messenger", () => { jest.mock("../webauthn-utils"); describe("Fido2 page script without native WebAuthn support", () => { + (jest.spyOn(globalThis, "document", "get") as jest.Mock).mockImplementation( + () => mockGlobalThisDocument, + ); + const mockCredentialCreationOptions = createCredentialCreationOptionsMock(); const mockCreateCredentialsResult = createCreateCredentialResultMock(); const mockCredentialRequestOptions = createCredentialRequestOptionsMock(); const mockCredentialAssertResult = createAssertCredentialResultMock(); require("./page-script"); + afterEach(() => { + jest.resetModules(); + }); + afterAll(() => { jest.clearAllMocks(); - jest.resetModules(); }); describe("creating WebAuthn credentials", () => { From bdbb16ab4c150460f11663f5b47b209d3543be10 Mon Sep 17 00:00:00 2001 From: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> Date: Thu, 2 May 2024 11:05:10 -0500 Subject: [PATCH 342/351] [SM-923] Migrate Project -> Service Accounts access policy selector (#8789) * Add request and response models * Add view * Add support in ap item types * Add new endpoints to the access policy service * Migrate to access policy selector --------- Co-authored-by: cd-bitwarden <106776772+cd-bitwarden@users.noreply.github.com> --- .../models/view/access-policy.view.ts | 4 + .../project-service-accounts.component.html | 44 ++-- .../project-service-accounts.component.ts | 195 ++++++++++++------ .../models/ap-item-value.type.ts | 21 ++ .../models/ap-item-view.type.ts | 24 +++ .../access-policies/access-policy.service.ts | 64 ++++++ ...ervice-accounts-access-policies.request.ts | 5 + ...rvice-accounts-access-policies.response.ts | 15 ++ 8 files changed, 289 insertions(+), 83 deletions(-) create mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/requests/project-service-accounts-access-policies.request.ts create mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/project-service-accounts-access-policies.response.ts diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policy.view.ts b/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policy.view.ts index 18b6994459..6c005a1225 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policy.view.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policy.view.ts @@ -67,3 +67,7 @@ export class ServiceAccountProjectPolicyPermissionDetailsView { export class ServiceAccountGrantedPoliciesView { grantedProjectPolicies: ServiceAccountProjectPolicyPermissionDetailsView[]; } + +export class ProjectServiceAccountsAccessPoliciesView { + serviceAccountAccessPolicies: ServiceAccountProjectAccessPolicyView[]; +} 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 443711fd36..5d22358277 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,17 +1,27 @@ -<div class="tw-w-2/5"> - <p class="tw-mt-8"> - {{ "projectMachineAccountsDescription" | i18n }} - </p> - <sm-access-selector - [rows]="rows$ | async" - granteeType="serviceAccounts" - [label]="'machineAccounts' | i18n" - [hint]="'projectMachineAccountsSelectHint' | i18n" - [columnTitle]="'machineAccounts' | i18n" - [emptyMessage]="'projectEmptyMachineAccountAccessPolicies' | i18n" - (onCreateAccessPolicies)="handleCreateAccessPolicies($event)" - (onDeleteAccessPolicy)="handleDeleteAccessPolicy($event)" - (onUpdateAccessPolicy)="handleUpdateAccessPolicy($event)" - > - </sm-access-selector> -</div> +<form [formGroup]="formGroup" [bitSubmit]="submit" *ngIf="!loading; else spinner"> + <div class="tw-w-2/5"> + <p class="tw-mt-8" *ngIf="!loading"> + {{ "projectMachineAccountsDescription" | i18n }} + </p> + <sm-access-policy-selector + [loading]="loading" + formControlName="accessPolicies" + [addButtonMode]="true" + [items]="items" + [label]="'machineAccounts' | i18n" + [hint]="'projectMachineAccountsSelectHint' | i18n" + [columnTitle]="'machineAccounts' | i18n" + [emptyMessage]="'projectEmptyMachineAccountAccessPolicies' | i18n" + > + </sm-access-policy-selector> + <button bitButton buttonType="primary" bitFormButton type="submit" class="tw-mt-7"> + {{ "save" | i18n }} + </button> + </div> +</form> + +<ng-template #spinner> + <div class="tw-items-center tw-justify-center tw-pt-64 tw-text-center"> + <i class="bwi bwi-spinner bwi-spin bwi-3x"></i> + </div> +</ng-template> diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-service-accounts.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-service-accounts.component.ts index 1521bb742d..668bdbae43 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-service-accounts.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-service-accounts.component.ts @@ -1,93 +1,69 @@ -import { Component, OnDestroy, OnInit } from "@angular/core"; +import { ChangeDetectorRef, Component, OnDestroy, OnInit } from "@angular/core"; +import { FormControl, FormGroup } from "@angular/forms"; import { ActivatedRoute } from "@angular/router"; -import { map, Observable, startWith, Subject, switchMap, takeUntil } from "rxjs"; +import { combineLatest, Subject, switchMap, takeUntil } from "rxjs"; +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 { SelectItemView } from "@bitwarden/components"; +import { ProjectServiceAccountsAccessPoliciesView } from "../../models/view/access-policy.view"; import { - ProjectAccessPoliciesView, - ServiceAccountProjectAccessPolicyView, -} from "../../models/view/access-policy.view"; + ApItemValueType, + convertToProjectServiceAccountsAccessPoliciesView, +} from "../../shared/access-policies/access-policy-selector/models/ap-item-value.type"; +import { + ApItemViewType, + convertPotentialGranteesToApItemViewType, + convertProjectServiceAccountsViewToApItemViews, +} from "../../shared/access-policies/access-policy-selector/models/ap-item-view.type"; import { AccessPolicyService } from "../../shared/access-policies/access-policy.service"; -import { - AccessSelectorComponent, - AccessSelectorRowView, -} from "../../shared/access-policies/access-selector.component"; @Component({ selector: "sm-project-service-accounts", templateUrl: "./project-service-accounts.component.html", }) export class ProjectServiceAccountsComponent implements OnInit, OnDestroy { + private currentAccessPolicies: ApItemViewType[]; private destroy$ = new Subject<void>(); private organizationId: string; private projectId: string; - protected rows$: Observable<AccessSelectorRowView[]> = - this.accessPolicyService.projectAccessPolicyChanges$.pipe( - startWith(null), - switchMap(() => - this.accessPolicyService.getProjectAccessPolicies(this.organizationId, this.projectId), - ), - map((policies) => - policies.serviceAccountAccessPolicies.map((policy) => ({ - type: "serviceAccount", - name: policy.serviceAccountName, - id: policy.serviceAccountId, - accessPolicyId: policy.id, - read: policy.read, - write: policy.write, - icon: AccessSelectorComponent.serviceAccountIcon, - static: false, - })), - ), - ); + private currentAccessPolicies$ = combineLatest([this.route.params]).pipe( + switchMap(([params]) => + this.accessPolicyService + .getProjectServiceAccountsAccessPolicies(params.organizationId, params.projectId) + .then((policies) => { + return convertProjectServiceAccountsViewToApItemViews(policies); + }), + ), + ); - protected async handleUpdateAccessPolicy(policy: AccessSelectorRowView) { - try { - return await this.accessPolicyService.updateAccessPolicy( - AccessSelectorComponent.getBaseAccessPolicyView(policy), - ); - } catch (e) { - this.validationService.showError(e); - } - } + private potentialGrantees$ = combineLatest([this.route.params]).pipe( + switchMap(([params]) => + this.accessPolicyService + .getServiceAccountsPotentialGrantees(params.organizationId) + .then((grantees) => { + return convertPotentialGranteesToApItemViewType(grantees); + }), + ), + ); - protected handleCreateAccessPolicies(selected: SelectItemView[]) { - const projectAccessPoliciesView = new ProjectAccessPoliciesView(); - projectAccessPoliciesView.serviceAccountAccessPolicies = selected - .filter( - (selection) => AccessSelectorComponent.getAccessItemType(selection) === "serviceAccount", - ) - .map((filtered) => { - const view = new ServiceAccountProjectAccessPolicyView(); - view.grantedProjectId = this.projectId; - view.serviceAccountId = filtered.id; - view.read = true; - view.write = false; - return view; - }); + protected formGroup = new FormGroup({ + accessPolicies: new FormControl([] as ApItemValueType[]), + }); - return this.accessPolicyService.createProjectAccessPolicies( - this.organizationId, - this.projectId, - projectAccessPoliciesView, - ); - } - - protected async handleDeleteAccessPolicy(policy: AccessSelectorRowView) { - try { - await this.accessPolicyService.deleteAccessPolicy(policy.accessPolicyId); - } catch (e) { - this.validationService.showError(e); - } - } + protected loading = true; + protected potentialGrantees: ApItemViewType[]; + protected items: ApItemViewType[]; constructor( private route: ActivatedRoute, + private changeDetectorRef: ChangeDetectorRef, private validationService: ValidationService, private accessPolicyService: AccessPolicyService, + private platformUtilsService: PlatformUtilsService, + private i18nService: I18nService, ) {} ngOnInit(): void { @@ -95,10 +71,97 @@ export class ProjectServiceAccountsComponent implements OnInit, OnDestroy { this.organizationId = params.organizationId; this.projectId = params.projectId; }); + + combineLatest([this.potentialGrantees$, this.currentAccessPolicies$]) + .pipe(takeUntil(this.destroy$)) + .subscribe(([potentialGrantees, currentAccessPolicies]) => { + this.potentialGrantees = potentialGrantees; + this.items = this.getItems(potentialGrantees, currentAccessPolicies); + this.setSelected(currentAccessPolicies); + }); } ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); } + + submit = async () => { + if (this.isFormInvalid()) { + return; + } + const formValues = this.formGroup.value.accessPolicies; + this.formGroup.disable(); + + try { + const accessPoliciesView = await this.updateProjectServiceAccountsAccessPolicies( + this.organizationId, + this.projectId, + formValues, + ); + + const updatedView = convertProjectServiceAccountsViewToApItemViews(accessPoliciesView); + this.items = this.getItems(this.potentialGrantees, updatedView); + this.setSelected(updatedView); + + this.platformUtilsService.showToast( + "success", + null, + this.i18nService.t("projectAccessUpdated"), + ); + } catch (e) { + this.validationService.showError(e); + this.setSelected(this.currentAccessPolicies); + } + this.formGroup.enable(); + }; + + private setSelected(policiesToSelect: ApItemViewType[]) { + this.loading = true; + this.currentAccessPolicies = policiesToSelect; + if (policiesToSelect != undefined) { + // Must detect changes so that AccessSelector @Inputs() are aware of the latest + // potentialGrantees, otherwise no selected values will be patched below + this.changeDetectorRef.detectChanges(); + this.formGroup.patchValue({ + accessPolicies: policiesToSelect.map((m) => ({ + type: m.type, + id: m.id, + permission: m.permission, + })), + }); + } + this.loading = false; + } + + private isFormInvalid(): boolean { + this.formGroup.markAllAsTouched(); + return this.formGroup.invalid; + } + + private async updateProjectServiceAccountsAccessPolicies( + organizationId: string, + projectId: string, + selectedPolicies: ApItemValueType[], + ): Promise<ProjectServiceAccountsAccessPoliciesView> { + const view = convertToProjectServiceAccountsAccessPoliciesView(projectId, selectedPolicies); + return await this.accessPolicyService.putProjectServiceAccountsAccessPolicies( + organizationId, + projectId, + view, + ); + } + + private getItems(potentialGrantees: ApItemViewType[], currentAccessPolicies: ApItemViewType[]) { + // If the user doesn't have access to the service account, they won't be in the potentialGrantees list. + // Add them to the potentialGrantees list if they are selected. + const items = [...potentialGrantees]; + for (const policy of currentAccessPolicies) { + const exists = potentialGrantees.some((grantee) => grantee.id === policy.id); + if (!exists) { + items.push(policy); + } + } + return items; + } } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/ap-item-value.type.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/ap-item-value.type.ts index 37c9f5523a..237fa2f323 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/ap-item-value.type.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/ap-item-value.type.ts @@ -8,6 +8,7 @@ import { ServiceAccountGrantedPoliciesView, ServiceAccountProjectPolicyPermissionDetailsView, ServiceAccountProjectAccessPolicyView, + ProjectServiceAccountsAccessPoliciesView, } from "../../../../models/view/access-policy.view"; import { ApItemEnum } from "./enums/ap-item.enum"; @@ -102,3 +103,23 @@ export function convertToServiceAccountGrantedPoliciesView( return view; } + +export function convertToProjectServiceAccountsAccessPoliciesView( + projectId: string, + selectedPolicyValues: ApItemValueType[], +): ProjectServiceAccountsAccessPoliciesView { + const view = new ProjectServiceAccountsAccessPoliciesView(); + + view.serviceAccountAccessPolicies = selectedPolicyValues + .filter((x) => x.type == ApItemEnum.ServiceAccount) + .map((filtered) => { + const policyView = new ServiceAccountProjectAccessPolicyView(); + policyView.serviceAccountId = filtered.id; + policyView.grantedProjectId = projectId; + policyView.read = ApPermissionEnumUtil.toRead(filtered.permission); + policyView.write = ApPermissionEnumUtil.toWrite(filtered.permission); + return policyView; + }); + + return view; +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/ap-item-view.type.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/ap-item-view.type.ts index 996818001d..07e08afcf9 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/ap-item-view.type.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/ap-item-view.type.ts @@ -4,6 +4,7 @@ import { SelectItemView } from "@bitwarden/components"; import { ProjectPeopleAccessPoliciesView, ServiceAccountGrantedPoliciesView, + ProjectServiceAccountsAccessPoliciesView, ServiceAccountPeopleAccessPoliciesView, } from "../../../../models/view/access-policy.view"; import { PotentialGranteeView } from "../../../../models/view/potential-grantee.view"; @@ -98,6 +99,29 @@ export function convertGrantedPoliciesToAccessPolicyItemViews( return accessPolicies; } +export function convertProjectServiceAccountsViewToApItemViews( + value: ProjectServiceAccountsAccessPoliciesView, +): ApItemViewType[] { + const accessPolicies: ApItemViewType[] = []; + + value.serviceAccountAccessPolicies.forEach((accessPolicyView) => { + accessPolicies.push({ + type: ApItemEnum.ServiceAccount, + icon: ApItemEnumUtil.itemIcon(ApItemEnum.ServiceAccount), + id: accessPolicyView.serviceAccountId, + accessPolicyId: accessPolicyView.id, + labelName: accessPolicyView.serviceAccountName, + listName: accessPolicyView.serviceAccountName, + permission: ApPermissionEnumUtil.toApPermissionEnum( + accessPolicyView.read, + accessPolicyView.write, + ), + readOnly: false, + }); + }); + return accessPolicies; +} + export function convertPotentialGranteesToApItemViewType( grantees: PotentialGranteeView[], ): ApItemViewType[] { diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy.service.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy.service.ts index 967bbf7ed0..98684e3a60 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy.service.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy.service.ts @@ -19,6 +19,7 @@ import { UserServiceAccountAccessPolicyView, ServiceAccountPeopleAccessPoliciesView, ServiceAccountGrantedPoliciesView, + ProjectServiceAccountsAccessPoliciesView, ServiceAccountProjectPolicyPermissionDetailsView, } from "../../models/view/access-policy.view"; import { PotentialGranteeView } from "../../models/view/potential-grantee.view"; @@ -29,6 +30,7 @@ import { ServiceAccountGrantedPoliciesRequest } from "../access-policies/models/ import { AccessPolicyUpdateRequest } from "./models/requests/access-policy-update.request"; import { AccessPolicyRequest } from "./models/requests/access-policy.request"; +import { ProjectServiceAccountsAccessPoliciesRequest } from "./models/requests/project-service-accounts-access-policies.request"; import { GroupServiceAccountAccessPolicyResponse, UserServiceAccountAccessPolicyResponse, @@ -38,6 +40,7 @@ import { } from "./models/responses/access-policy.response"; import { PotentialGranteeResponse } from "./models/responses/potential-grantee.response"; import { ProjectPeopleAccessPoliciesResponse } from "./models/responses/project-people-access-policies.response"; +import { ProjectServiceAccountsAccessPoliciesResponse } from "./models/responses/project-service-accounts-access-policies.response"; import { ServiceAccountGrantedPoliciesPermissionDetailsResponse } from "./models/responses/service-account-granted-policies-permission-details.response"; import { ServiceAccountPeopleAccessPoliciesResponse } from "./models/responses/service-account-people-access-policies.response"; import { ServiceAccountProjectPolicyPermissionDetailsResponse } from "./models/responses/service-account-project-policy-permission-details.response"; @@ -175,6 +178,40 @@ export class AccessPolicyService { return await this.createServiceAccountGrantedPoliciesView(result, organizationId); } + async getProjectServiceAccountsAccessPolicies( + organizationId: string, + projectId: string, + ): Promise<ProjectServiceAccountsAccessPoliciesView> { + const r = await this.apiService.send( + "GET", + "/projects/" + projectId + "/access-policies/service-accounts", + null, + true, + true, + ); + + const result = new ProjectServiceAccountsAccessPoliciesResponse(r); + return await this.createProjectServiceAccountsAccessPoliciesView(result, organizationId); + } + + async putProjectServiceAccountsAccessPolicies( + organizationId: string, + projectId: string, + policies: ProjectServiceAccountsAccessPoliciesView, + ): Promise<ProjectServiceAccountsAccessPoliciesView> { + const request = this.getProjectServiceAccountsAccessPoliciesRequest(policies); + const r = await this.apiService.send( + "PUT", + "/projects/" + projectId + "/access-policies/service-accounts", + request, + true, + true, + ); + + const result = new ProjectServiceAccountsAccessPoliciesResponse(r); + return await this.createProjectServiceAccountsAccessPoliciesView(result, organizationId); + } + async createProjectAccessPolicies( organizationId: string, projectId: string, @@ -325,6 +362,18 @@ export class AccessPolicyService { return request; } + private getProjectServiceAccountsAccessPoliciesRequest( + policies: ProjectServiceAccountsAccessPoliciesView, + ): ProjectServiceAccountsAccessPoliciesRequest { + const request = new ProjectServiceAccountsAccessPoliciesRequest(); + + request.serviceAccountAccessPolicyRequests = policies.serviceAccountAccessPolicies.map((ap) => { + return this.getAccessPolicyRequest(ap.serviceAccountId, ap); + }); + + return request; + } + private async createServiceAccountGrantedPoliciesView( response: ServiceAccountGrantedPoliciesPermissionDetailsResponse, organizationId: string, @@ -535,4 +584,19 @@ export class AccessPolicyService { currentUserInGroup: response.currentUserInGroup, }; } + + private async createProjectServiceAccountsAccessPoliciesView( + response: ProjectServiceAccountsAccessPoliciesResponse, + organizationId: string, + ): Promise<ProjectServiceAccountsAccessPoliciesView> { + const orgKey = await this.getOrganizationKey(organizationId); + + const view = new ProjectServiceAccountsAccessPoliciesView(); + view.serviceAccountAccessPolicies = await Promise.all( + response.serviceAccountAccessPolicies.map(async (ap) => { + return await this.createServiceAccountProjectAccessPolicyView(orgKey, ap); + }), + ); + return view; + } } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/requests/project-service-accounts-access-policies.request.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/requests/project-service-accounts-access-policies.request.ts new file mode 100644 index 0000000000..e287775cd3 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/requests/project-service-accounts-access-policies.request.ts @@ -0,0 +1,5 @@ +import { AccessPolicyRequest } from "./access-policy.request"; + +export class ProjectServiceAccountsAccessPoliciesRequest { + serviceAccountAccessPolicyRequests?: AccessPolicyRequest[]; +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/project-service-accounts-access-policies.response.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/project-service-accounts-access-policies.response.ts new file mode 100644 index 0000000000..f26a9996dd --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/project-service-accounts-access-policies.response.ts @@ -0,0 +1,15 @@ +import { BaseResponse } from "@bitwarden/common/models/response/base.response"; + +import { ServiceAccountProjectAccessPolicyResponse } from "./access-policy.response"; + +export class ProjectServiceAccountsAccessPoliciesResponse extends BaseResponse { + serviceAccountAccessPolicies: ServiceAccountProjectAccessPolicyResponse[]; + + constructor(response: any) { + super(response); + const serviceAccountAccessPolicies = this.getResponseProperty("ServiceAccountAccessPolicies"); + this.serviceAccountAccessPolicies = serviceAccountAccessPolicies.map( + (k: any) => new ServiceAccountProjectAccessPolicyResponse(k), + ); + } +} From f51042f8131c0d6ba375520b5800683d5787dccb Mon Sep 17 00:00:00 2001 From: Cesar Gonzalez <cesar.a.gonzalezcs@gmail.com> Date: Thu, 2 May 2024 11:12:41 -0500 Subject: [PATCH 343/351] [PM-7810] Handle Multithread Decryption Through Offscreen API (#8978) * [PM-7810] Handle Multithread Decryption through Offscreen API * [PM-7810] Handle Multithread Decryption through Offscreen API * Use a service to track when to open and close offscreen document There some strangeness around maintaining the offscreen document for more callbacks, that need not have the same reasons and justifications as the original. We'd need to test, but perhaps the intent is something closer to maintaining a work queue ourselves and creating the offscreen page for only a single reason as it comes in, then waiting for that page to close before opening another. * [PM-7810] Handle Multithread Decryption through Offscreen API * [PM-7810] Handle Multithread Decryption through Offscreen API * [PM-7810] Handle Multithread Decryption through Offscreen API * [PM-7810] Handle Multithread Decryption through Offscreen API * [PM-7810] Implementing jest tests for OffscreenDocument and BrowserMultithreadEncryptServiceImplementation * [PM-7810] Separating out the process by which we get decrypted items from the web worker to ensure we do not do duplicate effort * [PM-7810] Separating out the process by which we get decrypted items from the web worker to ensure we do not do duplicate effort * Prefer builtin promise flattening * [PM-7810] Introducing a fallback to the MultithreadEncryptServiceImplementation to ensure we can fallback to single thread decryption if necessary * [PM-7810] Updating documentation * [PM-7810] Fixing implementation to leverage the new OffscreenDocumentService --------- Co-authored-by: Matt Gibson <mgibson@bitwarden.com> --- .../browser/src/background/main.background.ts | 18 ++-- .../abstractions/offscreen-document.ts | 2 + .../offscreen-document.spec.ts | 50 ++++++++++ .../offscreen-document/offscreen-document.ts | 35 ++++++- ...ead-encrypt.service.implementation.spec.ts | 97 +++++++++++++++++++ ...tithread-encrypt.service.implementation.ts | 91 +++++++++++++++++ ...tithread-encrypt.service.implementation.ts | 40 ++++++-- 7 files changed, 312 insertions(+), 21 deletions(-) create mode 100644 apps/browser/src/platform/services/browser-multithread-encrypt.service.implementation.spec.ts create mode 100644 apps/browser/src/platform/services/browser-multithread-encrypt.service.implementation.ts diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 067fae1de8..2bdb93bd07 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -106,7 +106,6 @@ import { DefaultConfigService } from "@bitwarden/common/platform/services/config import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation"; -import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation"; import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service"; import { KeyGenerationService } from "@bitwarden/common/platform/services/key-generation.service"; import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service"; @@ -219,6 +218,7 @@ import { BrowserCryptoService } from "../platform/services/browser-crypto.servic import { BrowserEnvironmentService } from "../platform/services/browser-environment.service"; import BrowserLocalStorageService from "../platform/services/browser-local-storage.service"; import BrowserMemoryStorageService from "../platform/services/browser-memory-storage.service"; +import { BrowserMultithreadEncryptServiceImplementation } from "../platform/services/browser-multithread-encrypt.service.implementation"; import { BrowserScriptInjectorService } from "../platform/services/browser-script-injector.service"; import { DefaultBrowserStateService } from "../platform/services/default-browser-state.service"; import I18nService from "../platform/services/i18n.service"; @@ -475,14 +475,14 @@ export default class MainBackground { storageServiceProvider, ); - this.encryptService = - flagEnabled("multithreadDecryption") && BrowserApi.isManifestVersion(2) - ? new MultithreadEncryptServiceImplementation( - this.cryptoFunctionService, - this.logService, - true, - ) - : new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, true); + this.encryptService = flagEnabled("multithreadDecryption") + ? new BrowserMultithreadEncryptServiceImplementation( + this.cryptoFunctionService, + this.logService, + true, + this.offscreenDocumentService, + ) + : new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, true); this.singleUserStateProvider = new DefaultSingleUserStateProvider( storageServiceProvider, diff --git a/apps/browser/src/platform/offscreen-document/abstractions/offscreen-document.ts b/apps/browser/src/platform/offscreen-document/abstractions/offscreen-document.ts index 2d3c6a3e71..2a67d55c96 100644 --- a/apps/browser/src/platform/offscreen-document/abstractions/offscreen-document.ts +++ b/apps/browser/src/platform/offscreen-document/abstractions/offscreen-document.ts @@ -2,6 +2,7 @@ export type OffscreenDocumentExtensionMessage = { [key: string]: any; command: string; text?: string; + decryptRequest?: string; }; type OffscreenExtensionMessageEventParams = { @@ -13,6 +14,7 @@ export type OffscreenDocumentExtensionMessageHandlers = { [key: string]: ({ message, sender }: OffscreenExtensionMessageEventParams) => any; offscreenCopyToClipboard: ({ message }: OffscreenExtensionMessageEventParams) => any; offscreenReadFromClipboard: () => any; + offscreenDecryptItems: ({ message }: OffscreenExtensionMessageEventParams) => Promise<string>; }; export interface OffscreenDocument { diff --git a/apps/browser/src/platform/offscreen-document/offscreen-document.spec.ts b/apps/browser/src/platform/offscreen-document/offscreen-document.spec.ts index 933cd08c2e..9d3cadbba8 100644 --- a/apps/browser/src/platform/offscreen-document/offscreen-document.spec.ts +++ b/apps/browser/src/platform/offscreen-document/offscreen-document.spec.ts @@ -1,7 +1,25 @@ +import { mock } from "jest-mock-extended"; + +import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface"; +import { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.interface"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; + import { flushPromises, sendExtensionRuntimeMessage } from "../../autofill/spec/testing-utils"; import { BrowserApi } from "../browser/browser-api"; import BrowserClipboardService from "../services/browser-clipboard.service"; +jest.mock( + "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation", + () => ({ + MultithreadEncryptServiceImplementation: class MultithreadEncryptServiceImplementation { + getDecryptedItemsFromWorker = async <T extends InitializerMetadata>( + items: Decryptable<T>[], + _key: SymmetricCryptoKey, + ): Promise<string> => JSON.stringify(items); + }, + }), +); + describe("OffscreenDocument", () => { const browserApiMessageListenerSpy = jest.spyOn(BrowserApi, "messageListener"); const browserClipboardServiceCopySpy = jest.spyOn(BrowserClipboardService, "copy"); @@ -60,5 +78,37 @@ describe("OffscreenDocument", () => { expect(browserClipboardServiceReadSpy).toHaveBeenCalledWith(window); }); }); + + describe("handleOffscreenDecryptItems", () => { + it("returns an empty array as a string if the decrypt request is not present in the message", async () => { + let response: string | undefined; + sendExtensionRuntimeMessage( + { command: "offscreenDecryptItems" }, + mock<chrome.runtime.MessageSender>(), + (res: string) => (response = res), + ); + await flushPromises(); + + expect(response).toBe("[]"); + }); + + it("decrypts the items and sends back the response as a string", async () => { + const items = [{ id: "test" }]; + const key = { id: "test" }; + const decryptRequest = JSON.stringify({ items, key }); + let response: string | undefined; + + sendExtensionRuntimeMessage( + { command: "offscreenDecryptItems", decryptRequest }, + mock<chrome.runtime.MessageSender>(), + (res: string) => { + response = res; + }, + ); + await flushPromises(); + + expect(response).toBe(JSON.stringify(items)); + }); + }); }); }); diff --git a/apps/browser/src/platform/offscreen-document/offscreen-document.ts b/apps/browser/src/platform/offscreen-document/offscreen-document.ts index 4994a6e9ba..509193d5ee 100644 --- a/apps/browser/src/platform/offscreen-document/offscreen-document.ts +++ b/apps/browser/src/platform/offscreen-document/offscreen-document.ts @@ -1,21 +1,35 @@ import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; +import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation"; +import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service"; import { BrowserApi } from "../browser/browser-api"; import BrowserClipboardService from "../services/browser-clipboard.service"; import { + OffscreenDocument as OffscreenDocumentInterface, OffscreenDocumentExtensionMessage, OffscreenDocumentExtensionMessageHandlers, - OffscreenDocument as OffscreenDocumentInterface, } from "./abstractions/offscreen-document"; class OffscreenDocument implements OffscreenDocumentInterface { - private consoleLogService: ConsoleLogService = new ConsoleLogService(false); + private readonly consoleLogService: ConsoleLogService; + private encryptService: MultithreadEncryptServiceImplementation; private readonly extensionMessageHandlers: OffscreenDocumentExtensionMessageHandlers = { offscreenCopyToClipboard: ({ message }) => this.handleOffscreenCopyToClipboard(message), offscreenReadFromClipboard: () => this.handleOffscreenReadFromClipboard(), + offscreenDecryptItems: ({ message }) => this.handleOffscreenDecryptItems(message), }; + constructor() { + const cryptoFunctionService = new WebCryptoFunctionService(self); + this.consoleLogService = new ConsoleLogService(false); + this.encryptService = new MultithreadEncryptServiceImplementation( + cryptoFunctionService, + this.consoleLogService, + true, + ); + } + /** * Initializes the offscreen document extension. */ @@ -39,6 +53,23 @@ class OffscreenDocument implements OffscreenDocumentInterface { return await BrowserClipboardService.read(self); } + /** + * Decrypts the items in the message using the encrypt service. + * + * @param message - The extension message containing the items to decrypt + */ + private async handleOffscreenDecryptItems( + message: OffscreenDocumentExtensionMessage, + ): Promise<string> { + const { decryptRequest } = message; + if (!decryptRequest) { + return "[]"; + } + + const request = JSON.parse(decryptRequest); + return await this.encryptService.getDecryptedItemsFromWorker(request.items, request.key); + } + /** * Sets up the listener for extension messages. */ diff --git a/apps/browser/src/platform/services/browser-multithread-encrypt.service.implementation.spec.ts b/apps/browser/src/platform/services/browser-multithread-encrypt.service.implementation.spec.ts new file mode 100644 index 0000000000..db5b3df7a3 --- /dev/null +++ b/apps/browser/src/platform/services/browser-multithread-encrypt.service.implementation.spec.ts @@ -0,0 +1,97 @@ +import { mock, MockProxy } from "jest-mock-extended"; + +import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { EncryptionType } from "@bitwarden/common/platform/enums"; +import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface"; +import { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.interface"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { InitializerKey } from "@bitwarden/common/platform/services/cryptography/initializer-key"; +import { makeStaticByteArray } from "@bitwarden/common/spec"; + +import { BrowserApi } from "../browser/browser-api"; +import { OffscreenDocumentService } from "../offscreen-document/abstractions/offscreen-document"; + +import { BrowserMultithreadEncryptServiceImplementation } from "./browser-multithread-encrypt.service.implementation"; + +describe("BrowserMultithreadEncryptServiceImplementation", () => { + let cryptoFunctionServiceMock: MockProxy<CryptoFunctionService>; + let logServiceMock: MockProxy<LogService>; + let offscreenDocumentServiceMock: MockProxy<OffscreenDocumentService>; + let encryptService: BrowserMultithreadEncryptServiceImplementation; + const manifestVersionSpy = jest.spyOn(BrowserApi, "manifestVersion", "get"); + const sendMessageWithResponseSpy = jest.spyOn(BrowserApi, "sendMessageWithResponse"); + const encType = EncryptionType.AesCbc256_HmacSha256_B64; + const key = new SymmetricCryptoKey(makeStaticByteArray(64, 100), encType); + const items: Decryptable<InitializerMetadata>[] = [ + { + decrypt: jest.fn(), + initializerKey: InitializerKey.Cipher, + }, + ]; + + beforeEach(() => { + cryptoFunctionServiceMock = mock<CryptoFunctionService>(); + logServiceMock = mock<LogService>(); + offscreenDocumentServiceMock = mock<OffscreenDocumentService>({ + withDocument: jest.fn((_, __, callback) => callback() as any), + }); + encryptService = new BrowserMultithreadEncryptServiceImplementation( + cryptoFunctionServiceMock, + logServiceMock, + false, + offscreenDocumentServiceMock, + ); + manifestVersionSpy.mockReturnValue(3); + sendMessageWithResponseSpy.mockResolvedValue(JSON.stringify([])); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it("decrypts items using web workers if the chrome.offscreen API is not supported", async () => { + manifestVersionSpy.mockReturnValue(2); + + await encryptService.decryptItems([], key); + + expect(offscreenDocumentServiceMock.withDocument).not.toHaveBeenCalled(); + }); + + it("decrypts items using the chrome.offscreen API if it is supported", async () => { + sendMessageWithResponseSpy.mockResolvedValue(JSON.stringify(items)); + + await encryptService.decryptItems(items, key); + + expect(offscreenDocumentServiceMock.withDocument).toHaveBeenCalledWith( + [chrome.offscreen.Reason.WORKERS], + "Use web worker to decrypt items.", + expect.any(Function), + ); + expect(BrowserApi.sendMessageWithResponse).toHaveBeenCalledWith("offscreenDecryptItems", { + decryptRequest: expect.any(String), + }); + }); + + it("returns an empty array if the passed items are not defined", async () => { + const result = await encryptService.decryptItems(null, key); + + expect(result).toEqual([]); + }); + + it("returns an empty array if the offscreen document message returns an empty value", async () => { + sendMessageWithResponseSpy.mockResolvedValue(""); + + const result = await encryptService.decryptItems(items, key); + + expect(result).toEqual([]); + }); + + it("returns an empty array if the offscreen document message returns an empty array", async () => { + sendMessageWithResponseSpy.mockResolvedValue("[]"); + + const result = await encryptService.decryptItems(items, key); + + expect(result).toEqual([]); + }); +}); diff --git a/apps/browser/src/platform/services/browser-multithread-encrypt.service.implementation.ts b/apps/browser/src/platform/services/browser-multithread-encrypt.service.implementation.ts new file mode 100644 index 0000000000..ace5015c8e --- /dev/null +++ b/apps/browser/src/platform/services/browser-multithread-encrypt.service.implementation.ts @@ -0,0 +1,91 @@ +import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface"; +import { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.interface"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation"; + +import { BrowserApi } from "../browser/browser-api"; +import { OffscreenDocumentService } from "../offscreen-document/abstractions/offscreen-document"; + +export class BrowserMultithreadEncryptServiceImplementation extends MultithreadEncryptServiceImplementation { + constructor( + cryptoFunctionService: CryptoFunctionService, + logService: LogService, + logMacFailures: boolean, + private offscreenDocumentService: OffscreenDocumentService, + ) { + super(cryptoFunctionService, logService, logMacFailures); + } + + /** + * Handles decryption of items, will use the offscreen document if supported. + * + * @param items - The items to decrypt. + * @param key - The key to use for decryption. + */ + async decryptItems<T extends InitializerMetadata>( + items: Decryptable<T>[], + key: SymmetricCryptoKey, + ): Promise<T[]> { + if (!this.isOffscreenDocumentSupported()) { + return await super.decryptItems(items, key); + } + + return await this.decryptItemsInOffscreenDocument(items, key); + } + + /** + * Decrypts items using the offscreen document api. + * + * @param items - The items to decrypt. + * @param key - The key to use for decryption. + */ + private async decryptItemsInOffscreenDocument<T extends InitializerMetadata>( + items: Decryptable<T>[], + key: SymmetricCryptoKey, + ): Promise<T[]> { + if (items == null || items.length < 1) { + return []; + } + + const request = { + id: Utils.newGuid(), + items: items, + key: key, + }; + + const response = await this.offscreenDocumentService.withDocument( + [chrome.offscreen.Reason.WORKERS], + "Use web worker to decrypt items.", + async () => { + return (await BrowserApi.sendMessageWithResponse("offscreenDecryptItems", { + decryptRequest: JSON.stringify(request), + })) as string; + }, + ); + + if (!response) { + return []; + } + + const responseItems = JSON.parse(response); + if (responseItems?.length < 1) { + return []; + } + + return this.initializeItems(responseItems); + } + + /** + * Checks if the offscreen document api is supported. + */ + private isOffscreenDocumentSupported() { + return ( + BrowserApi.isManifestVersion(3) && + typeof chrome !== "undefined" && + typeof chrome.offscreen !== "undefined" + ); + } +} diff --git a/libs/common/src/platform/services/cryptography/multithread-encrypt.service.implementation.ts b/libs/common/src/platform/services/cryptography/multithread-encrypt.service.implementation.ts index 6ac343bcb6..75a571fef2 100644 --- a/libs/common/src/platform/services/cryptography/multithread-encrypt.service.implementation.ts +++ b/libs/common/src/platform/services/cryptography/multithread-encrypt.service.implementation.ts @@ -19,17 +19,36 @@ export class MultithreadEncryptServiceImplementation extends EncryptServiceImple private clear$ = new Subject<void>(); /** - * Sends items to a web worker to decrypt them. - * This utilises multithreading to decrypt items faster without interrupting other operations (e.g. updating UI). + * Decrypts items using a web worker if the environment supports it. + * Will fall back to the main thread if the window object is not available. */ async decryptItems<T extends InitializerMetadata>( items: Decryptable<T>[], key: SymmetricCryptoKey, ): Promise<T[]> { + if (typeof window === "undefined") { + return super.decryptItems(items, key); + } + if (items == null || items.length < 1) { return []; } + const decryptedItems = await this.getDecryptedItemsFromWorker(items, key); + const parsedItems = JSON.parse(decryptedItems); + + return this.initializeItems(parsedItems); + } + + /** + * Sends items to a web worker to decrypt them. This utilizes multithreading to decrypt items + * faster without interrupting other operations (e.g. updating UI). This method returns values + * prior to deserialization to support forwarding results to another party + */ + async getDecryptedItemsFromWorker<T extends InitializerMetadata>( + items: Decryptable<T>[], + key: SymmetricCryptoKey, + ): Promise<string> { this.logService.info("Starting decryption using multithreading"); this.worker ??= new Worker( @@ -53,19 +72,20 @@ export class MultithreadEncryptServiceImplementation extends EncryptServiceImple return await firstValueFrom( fromEvent(this.worker, "message").pipe( filter((response: MessageEvent) => response.data?.id === request.id), - map((response) => JSON.parse(response.data.items)), - map((items) => - items.map((jsonItem: Jsonify<T>) => { - const initializer = getClassInitializer<T>(jsonItem.initializerKey); - return initializer(jsonItem); - }), - ), + map((response) => response.data.items), takeUntil(this.clear$), - defaultIfEmpty([]), + defaultIfEmpty("[]"), ), ); } + protected initializeItems<T extends InitializerMetadata>(items: Jsonify<T>[]): T[] { + return items.map((jsonItem: Jsonify<T>) => { + const initializer = getClassInitializer<T>(jsonItem.initializerKey); + return initializer(jsonItem); + }); + } + private clear() { this.clear$.next(); this.worker?.terminate(); From 6b286e9d9ea0b1453ad721e282a8780806be70d5 Mon Sep 17 00:00:00 2001 From: Cesar Gonzalez <cesar.a.gonzalezcs@gmail.com> Date: Thu, 2 May 2024 11:39:48 -0500 Subject: [PATCH 344/351] [PM-7874] Fix lost state when adding a vault item on the current tab view (#9020) --- .../popup/services/popup-search.service.ts | 22 ------------------- .../src/popup/services/popup-utils.service.ts | 0 .../src/popup/services/services.module.ts | 7 ------ .../components/vault/current-tab.component.ts | 2 -- .../src/vault/abstractions/cipher.service.ts | 1 - .../src/vault/services/cipher.service.ts | 5 +---- 6 files changed, 1 insertion(+), 36 deletions(-) delete mode 100644 apps/browser/src/popup/services/popup-search.service.ts delete mode 100644 apps/browser/src/popup/services/popup-utils.service.ts diff --git a/apps/browser/src/popup/services/popup-search.service.ts b/apps/browser/src/popup/services/popup-search.service.ts deleted file mode 100644 index 40e6fd2d96..0000000000 --- a/apps/browser/src/popup/services/popup-search.service.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { StateProvider } from "@bitwarden/common/platform/state"; -import { SearchService } from "@bitwarden/common/services/search.service"; - -export class PopupSearchService extends SearchService { - constructor(logService: LogService, i18nService: I18nService, stateProvider: StateProvider) { - super(logService, i18nService, stateProvider); - } - - clearIndex(): Promise<void> { - throw new Error("Not available."); - } - - indexCiphers(): Promise<void> { - throw new Error("Not available."); - } - - async getIndexForSearch() { - return await super.getIndexForSearch(); - } -} diff --git a/apps/browser/src/popup/services/popup-utils.service.ts b/apps/browser/src/popup/services/popup-utils.service.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 36cbdf6292..ee08ed84b7 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -19,7 +19,6 @@ import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services. import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common"; import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service"; import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service"; -import { SearchService as SearchServiceAbstraction } from "@bitwarden/common/abstractions/search.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 { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; @@ -125,7 +124,6 @@ import { VaultFilterService } from "../../vault/services/vault-filter.service"; import { DebounceNavigationService } from "./debounce-navigation.service"; import { InitService } from "./init.service"; import { PopupCloseWarningService } from "./popup-close-warning.service"; -import { PopupSearchService } from "./popup-search.service"; const OBSERVABLE_LARGE_OBJECT_MEMORY_STORAGE = new SafeInjectionToken< AbstractStorageService & ObservableStorageService @@ -182,11 +180,6 @@ const safeProviders: SafeProvider[] = [ useFactory: getBgService<SsoLoginServiceAbstraction>("ssoLoginService"), deps: [], }), - safeProvider({ - provide: SearchServiceAbstraction, - useClass: PopupSearchService, - deps: [LogService, I18nServiceAbstraction, StateProvider], - }), safeProvider({ provide: CryptoFunctionService, useFactory: () => new WebCryptoFunctionService(window), 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 d882dfd525..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 @@ -292,8 +292,6 @@ export class CurrentTabComponent implements OnInit, OnDestroy { const ciphers = await this.cipherService.getAllDecryptedForUrl( this.url, otherTypes.length > 0 ? otherTypes : null, - null, - false, ); this.loginCiphers = []; diff --git a/libs/common/src/vault/abstractions/cipher.service.ts b/libs/common/src/vault/abstractions/cipher.service.ts index 4337043cdf..22e2c54a59 100644 --- a/libs/common/src/vault/abstractions/cipher.service.ts +++ b/libs/common/src/vault/abstractions/cipher.service.ts @@ -33,7 +33,6 @@ export abstract class CipherService { url: string, includeOtherTypes?: CipherType[], defaultMatch?: UriMatchStrategySetting, - reindexCiphers?: boolean, ) => Promise<CipherView[]>; getAllFromApiForOrganization: (organizationId: string) => Promise<CipherView[]>; /** diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index fd484ee902..174da701bd 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -441,7 +441,6 @@ export class CipherService implements CipherServiceAbstraction { url: string, includeOtherTypes?: CipherType[], defaultMatch: UriMatchStrategySetting = null, - reindexCiphers = true, ): Promise<CipherView[]> { if (url == null && includeOtherTypes == null) { return Promise.resolve([]); @@ -450,9 +449,7 @@ export class CipherService implements CipherServiceAbstraction { const equivalentDomains = await firstValueFrom( this.domainSettingsService.getUrlEquivalentDomains(url), ); - const ciphers = reindexCiphers - ? await this.getAllDecrypted() - : await this.getDecryptedCiphers(); + const ciphers = await this.getAllDecrypted(); defaultMatch ??= await firstValueFrom(this.domainSettingsService.defaultUriMatchStrategy$); return ciphers.filter((cipher) => { From e774089d0ef090396e2887ab7d6ac21caad573fe Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Thu, 2 May 2024 21:04:38 +0200 Subject: [PATCH 345/351] Make premium.component to be owned by team-billing-dev (#8935) Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com> --- .github/CODEOWNERS | 1 + .../src/{ => billing}/popup/settings/premium.component.html | 0 .../src/{ => billing}/popup/settings/premium.component.ts | 0 apps/browser/src/popup/app-routing.module.ts | 2 +- apps/browser/src/popup/app.module.ts | 2 +- 5 files changed, 3 insertions(+), 2 deletions(-) rename apps/browser/src/{ => billing}/popup/settings/premium.component.html (100%) rename apps/browser/src/{ => billing}/popup/settings/premium.component.ts (100%) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e9c1f229a5..aba7d4e0e0 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -57,6 +57,7 @@ libs/common/src/admin-console @bitwarden/team-admin-console-dev libs/admin-console @bitwarden/team-admin-console-dev ## Billing team files ## +apps/browser/src/billing @bitwarden/team-billing-dev 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 diff --git a/apps/browser/src/popup/settings/premium.component.html b/apps/browser/src/billing/popup/settings/premium.component.html similarity index 100% rename from apps/browser/src/popup/settings/premium.component.html rename to apps/browser/src/billing/popup/settings/premium.component.html diff --git a/apps/browser/src/popup/settings/premium.component.ts b/apps/browser/src/billing/popup/settings/premium.component.ts similarity index 100% rename from apps/browser/src/popup/settings/premium.component.ts rename to apps/browser/src/billing/popup/settings/premium.component.ts diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index ac402e9583..14659cb4df 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -26,6 +26,7 @@ import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.comp import { TwoFactorComponent } from "../auth/popup/two-factor.component"; import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component"; import { AutofillComponent } from "../autofill/popup/settings/autofill.component"; +import { PremiumComponent } from "../billing/popup/settings/premium.component"; import BrowserPopupUtils from "../platform/popup/browser-popup-utils"; import { GeneratorComponent } from "../tools/popup/generator/generator.component"; import { PasswordGeneratorHistoryComponent } from "../tools/popup/generator/password-generator-history.component"; @@ -51,7 +52,6 @@ import { ExcludedDomainsComponent } from "./settings/excluded-domains.component" import { FoldersComponent } from "./settings/folders.component"; import { HelpAndFeedbackComponent } from "./settings/help-and-feedback.component"; import { OptionsComponent } from "./settings/options.component"; -import { PremiumComponent } from "./settings/premium.component"; import { SettingsComponent } from "./settings/settings.component"; import { SyncComponent } from "./settings/sync.component"; import { TabsComponent } from "./tabs.component"; diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index 0862c2da52..a6e953ad1d 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -35,6 +35,7 @@ import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.comp import { TwoFactorComponent } from "../auth/popup/two-factor.component"; import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component"; import { AutofillComponent } from "../autofill/popup/settings/autofill.component"; +import { PremiumComponent } from "../billing/popup/settings/premium.component"; import { HeaderComponent } from "../platform/popup/header.component"; import { PopupFooterComponent } from "../platform/popup/layout/popup-footer.component"; import { PopupHeaderComponent } from "../platform/popup/layout/popup-header.component"; @@ -76,7 +77,6 @@ import { ExcludedDomainsComponent } from "./settings/excluded-domains.component" import { FoldersComponent } from "./settings/folders.component"; import { HelpAndFeedbackComponent } from "./settings/help-and-feedback.component"; import { OptionsComponent } from "./settings/options.component"; -import { PremiumComponent } from "./settings/premium.component"; import { SettingsComponent } from "./settings/settings.component"; import { SyncComponent } from "./settings/sync.component"; import { VaultTimeoutInputComponent } from "./settings/vault-timeout-input.component"; From 6db90dc141b3215485f82b1791510d7596a8e1f1 Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Fri, 3 May 2024 05:48:57 +1000 Subject: [PATCH 346/351] Use refCount: true for shareReplay (#9005) --- .../organizations/manage/group-add-edit.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts index aabaac2f1c..52cba4f2ae 100644 --- a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts @@ -136,7 +136,7 @@ export class GroupAddEditComponent implements OnInit, OnDestroy { private destroy$ = new Subject<void>(); private orgCollections$ = from(this.collectionAdminService.getAll(this.organizationId)).pipe( - shareReplay({ refCount: false }), + shareReplay({ refCount: true, bufferSize: 1 }), ); private get orgMembers$(): Observable<Array<AccessItemView & { userId: UserId }>> { From 7b96979c009835d84d9ffebb5da2de2480ca7ccb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 3 May 2024 06:51:44 +0000 Subject: [PATCH 347/351] Autosync the updated translations (#9027) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/desktop/src/locales/ar/messages.json | 28 +++++----- apps/desktop/src/locales/bg/messages.json | 2 +- apps/desktop/src/locales/ca/messages.json | 14 ++--- apps/desktop/src/locales/fr/messages.json | 2 +- apps/desktop/src/locales/pt_PT/messages.json | 10 ++-- apps/desktop/src/locales/tr/messages.json | 56 ++++++++++---------- 6 files changed, 56 insertions(+), 56 deletions(-) diff --git a/apps/desktop/src/locales/ar/messages.json b/apps/desktop/src/locales/ar/messages.json index 2d25269fff..74c3d63e33 100644 --- a/apps/desktop/src/locales/ar/messages.json +++ b/apps/desktop/src/locales/ar/messages.json @@ -801,10 +801,10 @@ "message": "تغيير كلمة المرور الرئيسية" }, "continueToWebApp": { - "message": "Continue to web app?" + "message": "متابعة إلى تطبيق الويب؟" }, "changeMasterPasswordOnWebConfirmation": { - "message": "You can change your master password on the Bitwarden web app." + "message": "يمكنك تغيير كلمة المرور الرئيسية الخاصة بك على تطبيق ويب Bitwarden." }, "fingerprintPhrase": { "message": "عبارة بصمة الإصبع", @@ -1633,10 +1633,10 @@ "message": "تكامل المتصفح غير مدعوم" }, "browserIntegrationErrorTitle": { - "message": "Error enabling browser integration" + "message": "خطأ في تمكين تكامل المتصفح" }, "browserIntegrationErrorDesc": { - "message": "An error has occurred while enabling browser integration." + "message": "حدث خطأ أثناء تمكين دمج المتصفح." }, "browserIntegrationMasOnlyDesc": { "message": "للأسف، لا يتم دعم تكامل المتصفح إلا في إصدار متجر تطبيقات ماك في الوقت الحالي." @@ -1654,10 +1654,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": "الموافقة" @@ -2698,27 +2698,27 @@ "description": "Label indicating the most common import formats" }, "success": { - "message": "Success" + "message": "نجاح" }, "troubleshooting": { - "message": "Troubleshooting" + "message": "استكشاف الأخطاء وإصلاحها" }, "disableHardwareAccelerationRestart": { - "message": "Disable hardware acceleration and restart" + "message": "تعطيل تسارع العتاد وإعادة التشغيل" }, "enableHardwareAccelerationRestart": { - "message": "Enable hardware acceleration and restart" + "message": "تمكين تسارع العتاد وإعادة التشغيل" }, "removePasskey": { - "message": "Remove passkey" + "message": "إزالة مفتاح المرور" }, "passkeyRemoved": { - "message": "Passkey removed" + "message": "تمت إزالة كلمة المرور" }, "errorAssigningTargetCollection": { - "message": "Error assigning target collection." + "message": "خطأ في تعيين مجموعة الأهداف." }, "errorAssigningTargetFolder": { - "message": "Error assigning target folder." + "message": "خطأ في تعيين مجلد الهدف." } } diff --git a/apps/desktop/src/locales/bg/messages.json b/apps/desktop/src/locales/bg/messages.json index d53034d61c..0728178b7b 100644 --- a/apps/desktop/src/locales/bg/messages.json +++ b/apps/desktop/src/locales/bg/messages.json @@ -15,7 +15,7 @@ "message": "Видове" }, "typeLogin": { - "message": "Запис" + "message": "Вход" }, "typeCard": { "message": "Карта" diff --git a/apps/desktop/src/locales/ca/messages.json b/apps/desktop/src/locales/ca/messages.json index d8c0f32948..d7100af764 100644 --- a/apps/desktop/src/locales/ca/messages.json +++ b/apps/desktop/src/locales/ca/messages.json @@ -801,10 +801,10 @@ "message": "Canvia la contrasenya mestra" }, "continueToWebApp": { - "message": "Continue to web app?" + "message": "Continua cap a l'aplicació web?" }, "changeMasterPasswordOnWebConfirmation": { - "message": "You can change your master password on the Bitwarden web app." + "message": "Podeu canviar la vostra contrasenya mestra a l'aplicació web de Bitwarden." }, "fingerprintPhrase": { "message": "Frase d'empremta digital", @@ -1633,10 +1633,10 @@ "message": "La integració en el navegador no és compatible" }, "browserIntegrationErrorTitle": { - "message": "Error enabling browser integration" + "message": "S'ha produït un error en habilitar la integració del navegador" }, "browserIntegrationErrorDesc": { - "message": "An error has occurred while enabling browser integration." + "message": "S'ha produït un error en activar la integració del navegador." }, "browserIntegrationMasOnlyDesc": { "message": "Malauradament, la integració del navegador només és compatible amb la versió de Mac App Store." @@ -2698,7 +2698,7 @@ "description": "Label indicating the most common import formats" }, "success": { - "message": "Success" + "message": "Èxit" }, "troubleshooting": { "message": "Resolució de problemes" @@ -2716,9 +2716,9 @@ "message": "Clau de pas suprimida" }, "errorAssigningTargetCollection": { - "message": "Error assigning target collection." + "message": "S'ha produït un error en assignar la col·lecció de destinació." }, "errorAssigningTargetFolder": { - "message": "Error assigning target folder." + "message": "S'ha produït un error en assignar la carpeta de destinació." } } diff --git a/apps/desktop/src/locales/fr/messages.json b/apps/desktop/src/locales/fr/messages.json index e82420a1f5..8298544d2e 100644 --- a/apps/desktop/src/locales/fr/messages.json +++ b/apps/desktop/src/locales/fr/messages.json @@ -1633,7 +1633,7 @@ "message": "Intégration dans le navigateur non supportée" }, "browserIntegrationErrorTitle": { - "message": "Error enabling browser integration" + "message": "Erreur lors de l'intégration avec le navigateur" }, "browserIntegrationErrorDesc": { "message": "Une erreur s'est produite lors de l'action de l'intégration du navigateur." diff --git a/apps/desktop/src/locales/pt_PT/messages.json b/apps/desktop/src/locales/pt_PT/messages.json index 14f0ec5d2f..2535f5d860 100644 --- a/apps/desktop/src/locales/pt_PT/messages.json +++ b/apps/desktop/src/locales/pt_PT/messages.json @@ -163,7 +163,7 @@ "message": "Número do passaporte" }, "licenseNumber": { - "message": "Número da licença" + "message": "Número da carta de condução" }, "email": { "message": "E-mail" @@ -287,7 +287,7 @@ "message": "Cidade / Localidade" }, "stateProvince": { - "message": "Estado / Província" + "message": "Estado / Região" }, "zipPostalCode": { "message": "Código postal" @@ -435,10 +435,10 @@ "message": "Fechar" }, "minNumbers": { - "message": "Números mínimos" + "message": "Mínimo de números" }, "minSpecial": { - "message": "Caracteres especiais minímos", + "message": "Mínimo de caracteres especiais", "description": "Minimum Special Characters" }, "ambiguous": { @@ -1292,7 +1292,7 @@ "description": "ex. Date this item was updated" }, "dateCreated": { - "message": "Criado a", + "message": "Criado", "description": "ex. Date this item was created" }, "datePasswordUpdated": { diff --git a/apps/desktop/src/locales/tr/messages.json b/apps/desktop/src/locales/tr/messages.json index 3e7229c41b..adeb293ccf 100644 --- a/apps/desktop/src/locales/tr/messages.json +++ b/apps/desktop/src/locales/tr/messages.json @@ -801,10 +801,10 @@ "message": "Ana parolayı değiştir" }, "continueToWebApp": { - "message": "Continue to web app?" + "message": "Web uygulamasına devam edilsin mi?" }, "changeMasterPasswordOnWebConfirmation": { - "message": "You can change your master password on the Bitwarden web app." + "message": "Ana parolanızı Bitwarden web uygulamasında değiştirebilirsiniz." }, "fingerprintPhrase": { "message": "Parmak izi ifadesi", @@ -1557,7 +1557,7 @@ "description": "Used as a card title description on the set password page to explain why the user is there" }, "verificationRequired": { - "message": "Verification required", + "message": "Doğrulama gerekli", "description": "Default title for the user verification dialog." }, "currentMasterPass": { @@ -1633,7 +1633,7 @@ "message": "Tarayıcı entegrasyonu desteklenmiyor" }, "browserIntegrationErrorTitle": { - "message": "Error enabling browser integration" + "message": "Tarayıcı entegrasyonunu etkinleştirme hatası" }, "browserIntegrationErrorDesc": { "message": "An error has occurred while enabling browser integration." @@ -1654,10 +1654,10 @@ "message": "Masaüstü uygulamanızla tarayıcınız arasında bağlantı kurulurken parmak izi ifadesi doğrulamasını zorunlu kılarak ek bir güvenlik önlemi alabilirsiniz. Bu ayarı açarsanız her bağlantı kurulduğunda tekrar doğrulama yapmanız gerekir." }, "enableHardwareAcceleration": { - "message": "Donanım hızlandırması kullan" + "message": "Donanım hızlandırmayı kullan" }, "enableHardwareAccelerationDesc": { - "message": "Varsayılan olarak bu ayar AÇIK'tır. Yalnızca grafiksel sorunlarla karşılaşırsanız KAPATIN. Yeniden başlatma gerekli." + "message": "Varsayılan olarak bu ayar AÇIKTIR. Yalnızca grafik sorunlarıyla karşılaşırsanız KAPATIN. Yeniden başlatma gerekir." }, "approve": { "message": "Onayla" @@ -1898,40 +1898,40 @@ "message": "Ana parolanız kuruluş ilkelerinizi karşılamıyor. Kasanıza erişmek için ana parolanızı güncellemelisiniz. Devam ettiğinizde oturumunuz kapanacak ve yeniden oturum açmanız gerekecektir. Diğer cihazlardaki aktif oturumlar bir saate kadar aktif kalabilir." }, "tryAgain": { - "message": "Try again" + "message": "Yeniden dene" }, "verificationRequiredForActionSetPinToContinue": { - "message": "Verification required for this action. Set a PIN to continue." + "message": "Bu işlem için doğrulama gerekiyor. Devam etmek için bir PIN belirleyin." }, "setPin": { - "message": "Set PIN" + "message": "PIN belirle" }, "verifyWithBiometrics": { - "message": "Verify with biometrics" + "message": "Biyometri ile doğrula" }, "awaitingConfirmation": { - "message": "Awaiting confirmation" + "message": "Onay bekleniyor" }, "couldNotCompleteBiometrics": { - "message": "Could not complete biometrics." + "message": "Biyometri işlemi tamamlanamadı." }, "needADifferentMethod": { - "message": "Need a different method?" + "message": "Farklı bir yönteme mi ihtiyacınız var?" }, "useMasterPassword": { - "message": "Use master password" + "message": "Ana parolayı kullan" }, "usePin": { - "message": "Use PIN" + "message": "PIN kullan" }, "useBiometrics": { - "message": "Use biometrics" + "message": "Biyometri kullan" }, "enterVerificationCodeSentToEmail": { - "message": "Enter the verification code that was sent to your email." + "message": "E-posta adresinize gönderilen doğrulama kodunu girin." }, "resendCode": { - "message": "Resend code" + "message": "Kodu yeniden gönder" }, "hours": { "message": "Saat" @@ -2547,7 +2547,7 @@ "message": "Hesabınız için Duo iki adımlı giriş gereklidir." }, "launchDuo": { - "message": "Duo'yu Tarayıcıda Başlat" + "message": "Duo'yu tarayıcıda başlat" }, "importFormatError": { "message": "Veriler doğru biçimlendirilmemiş. Lütfen içe aktarma dosyanızı kontrol edin ve tekrar deneyin." @@ -2630,13 +2630,13 @@ "message": "Kullanıcı adı veya parola yanlış" }, "incorrectPassword": { - "message": "Incorrect password" + "message": "Yanlış parola" }, "incorrectCode": { - "message": "Incorrect code" + "message": "Yanlış kod" }, "incorrectPin": { - "message": "Incorrect PIN" + "message": "Yanlış PIN" }, "multifactorAuthenticationFailed": { "message": "Çok faktörlü kimlik doğrulama başarısız oldu" @@ -2694,26 +2694,26 @@ "message": "LastPass hesabınızla ilişkili YubiKey'i bilgisayarınızın USB portuna takıp düğmesine dokunun." }, "commonImportFormats": { - "message": "Common formats", + "message": "Sık kullanılan biçimler", "description": "Label indicating the most common import formats" }, "success": { - "message": "Success" + "message": "Başarılı" }, "troubleshooting": { "message": "Sorun giderme" }, "disableHardwareAccelerationRestart": { - "message": "Donanım hızlandırmayı devre dışı bırakın ve yeniden başlatın" + "message": "Donanım hızlandırmayı kapatıp yeniden başlat" }, "enableHardwareAccelerationRestart": { - "message": "Donanım hızlandırmayı etkinleştirin ve yeniden başlatın" + "message": "Donanım hızlandırmayı etkinleştirip yeniden başlat" }, "removePasskey": { - "message": "Remove passkey" + "message": "Geçiş anahtarını kaldır" }, "passkeyRemoved": { - "message": "Passkey removed" + "message": "Geçiş anahtarı kaldırıldı" }, "errorAssigningTargetCollection": { "message": "Error assigning target collection." From f5012e39ef2a2fa4b877715edc37811bff2edd42 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 3 May 2024 07:14:45 +0000 Subject: [PATCH 348/351] Autosync the updated translations (#9026) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/browser/src/_locales/ar/messages.json | 3 - apps/browser/src/_locales/az/messages.json | 3 - apps/browser/src/_locales/be/messages.json | 3 - apps/browser/src/_locales/bg/messages.json | 3 - apps/browser/src/_locales/bn/messages.json | 3 - apps/browser/src/_locales/bs/messages.json | 3 - apps/browser/src/_locales/ca/messages.json | 27 ++++---- apps/browser/src/_locales/cs/messages.json | 7 +- apps/browser/src/_locales/cy/messages.json | 3 - apps/browser/src/_locales/da/messages.json | 3 - apps/browser/src/_locales/de/messages.json | 3 - apps/browser/src/_locales/el/messages.json | 3 - apps/browser/src/_locales/en_GB/messages.json | 3 - apps/browser/src/_locales/en_IN/messages.json | 3 - apps/browser/src/_locales/es/messages.json | 3 - apps/browser/src/_locales/et/messages.json | 3 - apps/browser/src/_locales/eu/messages.json | 3 - apps/browser/src/_locales/fa/messages.json | 3 - apps/browser/src/_locales/fi/messages.json | 9 +-- apps/browser/src/_locales/fil/messages.json | 3 - apps/browser/src/_locales/fr/messages.json | 15 ++-- apps/browser/src/_locales/gl/messages.json | 3 - apps/browser/src/_locales/he/messages.json | 3 - apps/browser/src/_locales/hi/messages.json | 3 - apps/browser/src/_locales/hr/messages.json | 3 - apps/browser/src/_locales/hu/messages.json | 3 - apps/browser/src/_locales/id/messages.json | 3 - apps/browser/src/_locales/it/messages.json | 5 +- apps/browser/src/_locales/ja/messages.json | 3 - apps/browser/src/_locales/ka/messages.json | 3 - apps/browser/src/_locales/km/messages.json | 3 - apps/browser/src/_locales/kn/messages.json | 3 - apps/browser/src/_locales/ko/messages.json | 41 +++++------ apps/browser/src/_locales/lt/messages.json | 3 - apps/browser/src/_locales/lv/messages.json | 3 - apps/browser/src/_locales/ml/messages.json | 3 - apps/browser/src/_locales/mr/messages.json | 3 - apps/browser/src/_locales/my/messages.json | 3 - apps/browser/src/_locales/nb/messages.json | 3 - apps/browser/src/_locales/ne/messages.json | 3 - apps/browser/src/_locales/nl/messages.json | 3 - apps/browser/src/_locales/nn/messages.json | 3 - apps/browser/src/_locales/or/messages.json | 3 - apps/browser/src/_locales/pl/messages.json | 3 - apps/browser/src/_locales/pt_BR/messages.json | 3 - apps/browser/src/_locales/pt_PT/messages.json | 19 +++-- apps/browser/src/_locales/ro/messages.json | 3 - apps/browser/src/_locales/ru/messages.json | 3 - apps/browser/src/_locales/si/messages.json | 3 - apps/browser/src/_locales/sk/messages.json | 3 - apps/browser/src/_locales/sl/messages.json | 3 - apps/browser/src/_locales/sr/messages.json | 3 - apps/browser/src/_locales/sv/messages.json | 3 - apps/browser/src/_locales/te/messages.json | 3 - apps/browser/src/_locales/th/messages.json | 3 - apps/browser/src/_locales/tr/messages.json | 69 +++++++++---------- apps/browser/src/_locales/uk/messages.json | 3 - apps/browser/src/_locales/vi/messages.json | 3 - apps/browser/src/_locales/zh_CN/messages.json | 7 +- apps/browser/src/_locales/zh_TW/messages.json | 3 - apps/browser/store/locales/ca/copy.resx | 60 ++++++++-------- apps/browser/store/locales/cs/copy.resx | 60 ++++++++-------- apps/browser/store/locales/fi/copy.resx | 6 +- apps/browser/store/locales/fr/copy.resx | 6 +- apps/browser/store/locales/it/copy.resx | 56 +++++++-------- apps/browser/store/locales/pt_PT/copy.resx | 58 ++++++++-------- apps/browser/store/locales/tr/copy.resx | 6 +- 67 files changed, 212 insertions(+), 392 deletions(-) diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index 996142b5ad..6c83d771e9 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "قفل المخزن" }, - "privateModeWarning": { - "message": "دعم الوضع الخاص تجريبي وبعض الميزات محدودة." - }, "customFields": { "message": "الحقول المخصصة" }, diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index a58ada8eb1..18fc6acca8 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Anbarı kilidlə" }, - "privateModeWarning": { - "message": "Gizli rejim dəstəyi təcrübidir və bəzi özəlliklər limitlidir." - }, "customFields": { "message": "Özəl sahələr" }, diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index 82fd4fa5d4..44dc85d2b9 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Заблакіраваць сховішча" }, - "privateModeWarning": { - "message": "Прыватны рэжым - гэта эксперыментальная функцыя і некаторыя магчымасці ў ім абмежаваны." - }, "customFields": { "message": "Карыстальніцкія палі" }, diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index b6d41cb622..cb12459a44 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Заключване на трезора" }, - "privateModeWarning": { - "message": "Поддръжката на частния режим е експериментална и някои функционалности са ограничени." - }, "customFields": { "message": "Допълнителни полета" }, diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index dec1bc6cfa..8156e3c6f1 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "ভল্ট লক করুন" }, - "privateModeWarning": { - "message": "Private mode support is experimental and some features are limited." - }, "customFields": { "message": "পছন্দসই ক্ষেত্র" }, diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index 9d3113e3f6..a7334319b5 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Lock the vault" }, - "privateModeWarning": { - "message": "Private mode support is experimental and some features are limited." - }, "customFields": { "message": "Custom fields" }, diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index 8063ba79d8..d9cd14102e 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden Password Manager", + "message": "Bitwarden - Gestor de contrasenyes", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "message": "A casa, a la feina o en moviment, Bitwarden protegeix totes les contrasenyes, claus de pas i informació sensible", "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { @@ -173,10 +173,10 @@ "message": "Canvia la contrasenya mestra" }, "continueToWebApp": { - "message": "Continue to web app?" + "message": "Continua cap a l'aplicació web?" }, "changeMasterPasswordOnWebConfirmation": { - "message": "You can change your master password on the Bitwarden web app." + "message": "Podeu canviar la vostra contrasenya mestra a l'aplicació web de Bitwarden." }, "fingerprintPhrase": { "message": "Frase d'empremta digital", @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Tanca la caixa forta" }, - "privateModeWarning": { - "message": "El suport del mode privat és experimental i algunes funcions són limitades." - }, "customFields": { "message": "Camps personalitzats" }, @@ -3001,7 +2998,7 @@ "description": "Notification message for when saving credentials has failed." }, "success": { - "message": "Success" + "message": "Èxit" }, "removePasskey": { "message": "Suprimeix la clau de pas" @@ -3010,26 +3007,26 @@ "message": "Clau de pas suprimida" }, "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + "message": "Avís: els elements de l'organització no assignats ja no són visibles a la visualització de Totes les caixes fortes i només es poden accedir des de la Consola d'administració." }, "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." + "message": "Avís: el 16 de maig de 2024, els elements de l'organització no assignats deixaran de ser visibles a la visualització de Totes les caixes fortes i només es podran accedir des de la Consola d'administració." }, "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", + "message": "Assigna aquests elements a una col·lecció de", "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.", + "message": "per fer-los visibles.", "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" + "message": "Consola d'administració" }, "errorAssigningTargetCollection": { - "message": "Error assigning target collection." + "message": "S'ha produït un error en assignar la col·lecció de destinació." }, "errorAssigningTargetFolder": { - "message": "Error assigning target folder." + "message": "S'ha produït un error en assignar la carpeta de destinació." } } diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index ee58f3d263..058378ff17 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden Password Manager", + "message": "Bitwarden - Správce hesel", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "message": "Bitwarden zabezpečí všechna Vaše hesla, přístupové klíče a citlivé informace doma, v práci nebo na cestách", "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Zamkne trezor." }, - "privateModeWarning": { - "message": "Podpora soukromého režimu je experimentální a některé funkce jsou omezené." - }, "customFields": { "message": "Vlastní pole" }, diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index a80dca5f92..5ec7b9f483 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Cloi'r gell" }, - "privateModeWarning": { - "message": "Private mode support is experimental and some features are limited." - }, "customFields": { "message": "Meysydd addasedig" }, diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index 215d79eb21..4c90522e1e 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Lås boksen" }, - "privateModeWarning": { - "message": "Understøttelse af privat tilstand er eksperimentel, og nogle funktioner er begrænsede." - }, "customFields": { "message": "Brugerdefinerede felter" }, diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index fbc193dbae..35100a77d2 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Den Tresor sperren" }, - "privateModeWarning": { - "message": "Die Unterstützung des privaten Modus ist experimentell und einige Funktionen sind eingeschränkt." - }, "customFields": { "message": "Benutzerdefinierte Felder" }, diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index 5c85aeff58..de0cfb3f6c 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Κλειδώστε το vault" }, - "privateModeWarning": { - "message": "Η υποστήριξη ιδιωτικής λειτουργίας είναι πειραματική και ορισμένες δυνατότητες είναι περιορισμένες." - }, "customFields": { "message": "Προσαρμοσμένα Πεδία" }, diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index 087cd3faa8..a2ba76b3a6 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Lock the vault" }, - "privateModeWarning": { - "message": "Private mode support is experimental and some features are limited." - }, "customFields": { "message": "Custom fields" }, diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index f370af7f36..2a7f18ca01 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Lock the vault" }, - "privateModeWarning": { - "message": "Private mode support is experimental and some features are limited." - }, "customFields": { "message": "Custom fields" }, diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index 9e89f453df..e1199857c7 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Bloquear la caja fuerte" }, - "privateModeWarning": { - "message": "El soporte en modo privado es experimental y algunas características son limitadas." - }, "customFields": { "message": "Campos personalizados" }, diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index 5705a5a0d2..0f5b164717 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Lukusta hoidla" }, - "privateModeWarning": { - "message": "Privaatrežiimi toetus on katsejärgus, mistõttu mõned funktsioonid on piiratud." - }, "customFields": { "message": "Kohandatud väljad" }, diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index ee3b5f1329..4bc1373201 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Blokeatu kutxa gotorra" }, - "privateModeWarning": { - "message": "Modu pribatuko euskarria esperimentala da eta ezaugarri batzuk mugatuak dira." - }, "customFields": { "message": "Eremu pertsonalizatuak" }, diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index e2f0e96c86..8b09cb224f 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "قفل گاوصندوق" }, - "privateModeWarning": { - "message": "پشتیبانی حالت خصوصی آزمایشی است و برخی از ویژگی‌ها محدود هستند." - }, "customFields": { "message": "فیلدهای سفارشی" }, diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index 746f4f45be..95c77e0c09 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden – Salasanahallinta", + "message": "Bitwarden Salasanahallinta", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "message": "Kotona, töissä tai reissussa, Bitwarden suojaa helposti salasanasi, suojausavaimesi ja arkaluonteiset tietosi.", "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Lukitse holvi" }, - "privateModeWarning": { - "message": "Yksityisen tilan tuki on kokeellinen ja jotkin ominaisuudet toimivat rajoitetusti." - }, "customFields": { "message": "Lisäkentät" }, @@ -2828,7 +2825,7 @@ "message": "Korvataanko suojausavain?" }, "overwritePasskeyAlert": { - "message": "Kohde sisältää jo suojausavaimen. Haluatko varmasti korvata nykyisen salasanan?" + "message": "Kohde sisältää jo suojausavaimen. Haluatko varmasti korvata nykyisen suojausavaimen?" }, "featureNotSupported": { "message": "Ominaisuutta ei vielä tueta" diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index abb999d032..b536fa9c19 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "I-lock ang vault" }, - "privateModeWarning": { - "message": "Ang suporta sa private mode ay eksperimental at limitado ang ilang mga tampok." - }, "customFields": { "message": "Pasadyang mga patlang" }, diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index de35f71832..541541ab54 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden Password Manager", + "message": "Gestionnaire de mots de passe Bitwarden", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "message": "Chez vous, au travail, n'importe où, Bitwarden sécurise mots de passe, clés d'accès et informations sensibles", "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Verrouiller le coffre" }, - "privateModeWarning": { - "message": "La prise en charge de la navigation privée est expérimentale et certaines fonctionnalités sont limitées." - }, "customFields": { "message": "Champs personnalisés" }, @@ -3001,7 +2998,7 @@ "description": "Notification message for when saving credentials has failed." }, "success": { - "message": "Success" + "message": "Succès" }, "removePasskey": { "message": "Retirer la clé d'identification (passkey)" @@ -3016,15 +3013,15 @@ "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", + "message": "Ajouter ces éléments à une collection depuis la", "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.", + "message": "pour les rendre visibles.", "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" + "message": "Console Admin" }, "errorAssigningTargetCollection": { "message": "Error assigning target collection." diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index 3dd737f0a8..d23f0377bc 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Lock the vault" }, - "privateModeWarning": { - "message": "Private mode support is experimental and some features are limited." - }, "customFields": { "message": "Custom fields" }, diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index 5d343ae807..0a9d8a8106 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "נעל את הכספת" }, - "privateModeWarning": { - "message": "המצב הפרטי הוא במסגרת ניסוי וחלק מהיכולות מוגבלות." - }, "customFields": { "message": "שדות מותאמים אישית" }, diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index fa4051d3e9..042ef96ea2 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "वॉल्ट लॉक करें" }, - "privateModeWarning": { - "message": "निजी मोड समर्थन प्रायोगिक है और कुछ सुविधाएँ सीमित हैं।" - }, "customFields": { "message": "Custom Fields" }, diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index c9b8741509..09388672ff 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Zaključaj trezor" }, - "privateModeWarning": { - "message": "Podrška za privatni način rada je eksperimentalna, a neke su značajke ograničene." - }, "customFields": { "message": "Prilagođena polja" }, diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index 5d5b174435..5647f5d97d 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "A széf zárolása" }, - "privateModeWarning": { - "message": "A privát mód támogatása kísérleti és néhány funkció korlátozott." - }, "customFields": { "message": "Egyedi mezők" }, diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index b54e854d27..37961ba9a3 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Kunci brankas" }, - "privateModeWarning": { - "message": "Dukungan mode pribadi bersifat eksperimental dan beberapa fitur terbatas." - }, "customFields": { "message": "Ruas Khusus" }, diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index 91d10253a0..9420bca6ef 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -7,7 +7,7 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "message": "A casa, al lavoro, o in viaggio, Bitwarden protegge tutte le tue password, passkey, e informazioni sensibili", "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Blocca la cassaforte" }, - "privateModeWarning": { - "message": "Il supporto della modalità privata è sperimentale e alcune funzionalità sono limitate." - }, "customFields": { "message": "Campi personalizzati" }, diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index 967dc222e5..d1429a40e0 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "保管庫をロック" }, - "privateModeWarning": { - "message": "プライベートモードのサポートは実験的であり、一部機能は制限されています。" - }, "customFields": { "message": "カスタムフィールド" }, diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index c73c366195..416018cc92 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Lock the vault" }, - "privateModeWarning": { - "message": "Private mode support is experimental and some features are limited." - }, "customFields": { "message": "Custom fields" }, diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index b6384bb840..fec6ab713c 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Lock the vault" }, - "privateModeWarning": { - "message": "Private mode support is experimental and some features are limited." - }, "customFields": { "message": "Custom fields" }, diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index 178cd7c45f..f5e3b8d334 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "ವಾಲ್ಟ್ ಅನ್ನು ಲಾಕ್ ಮಾಡಿ" }, - "privateModeWarning": { - "message": "Private mode support is experimental and some features are limited." - }, "customFields": { "message": "ಕಸ್ಟಮ್ ಕ್ಷೇತ್ರಗಳು" }, diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index 95a7727b83..8b34a76833 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -176,7 +176,7 @@ "message": "웹 앱에서 계속하시겠용?" }, "changeMasterPasswordOnWebConfirmation": { - "message": "You can change your master password on the Bitwarden web app." + "message": "Bitwarden 웹 앱에서 마스터 비밀번호를 변경할 수 있습니다." }, "fingerprintPhrase": { "message": "지문 구절", @@ -229,10 +229,10 @@ "message": "Bitwarden 도움말 센터" }, "communityForums": { - "message": "Explore Bitwarden community forums" + "message": "Bitwarden 커뮤니티 포럼 탐색하기" }, "contactSupport": { - "message": "Contact Bitwarden support" + "message": "Bitwarden 지원에 문의하기" }, "sync": { "message": "동기화" @@ -275,7 +275,7 @@ "message": "길이" }, "passwordMinLength": { - "message": "Minimum password length" + "message": "최소 비밀번호 길이" }, "uppercase": { "message": "대문자 (A-Z)" @@ -333,7 +333,7 @@ "message": "비밀번호" }, "totp": { - "message": "Authenticator secret" + "message": "인증기 비밀 키" }, "passphrase": { "message": "패스프레이즈" @@ -375,10 +375,10 @@ "message": "기타" }, "unlockMethodNeededToChangeTimeoutActionDesc": { - "message": "Set up an unlock method to change your vault timeout action." + "message": "잠금 해제 방법을 설정하여 보관함의 시간 초과 동작을 변경하세요." }, "unlockMethodNeeded": { - "message": "Set up an unlock method in Settings" + "message": "설정에서 잠금 해제 수단 설정하기" }, "rateExtension": { "message": "확장 프로그램 평가" @@ -421,7 +421,7 @@ "message": "지금 잠그기" }, "lockAll": { - "message": "Lock all" + "message": "모두 잠그기" }, "immediately": { "message": "즉시" @@ -484,7 +484,7 @@ "message": "마스터 비밀번호를 재입력해야 합니다." }, "masterPasswordMinlength": { - "message": "Master password must be at least $VALUE$ characters long.", + "message": "마스터 비밀번호는 최소 $VALUE$자 이상이어야 합니다.", "description": "The Master Password must be at least a specific number of characters long.", "placeholders": { "value": { @@ -500,10 +500,10 @@ "message": "계정 생성이 완료되었습니다! 이제 로그인하실 수 있습니다." }, "youSuccessfullyLoggedIn": { - "message": "You successfully logged in" + "message": "로그인에 성공했습니다." }, "youMayCloseThisWindow": { - "message": "You may close this window" + "message": "이제 창을 닫으실 수 있습니다." }, "masterPassSent": { "message": "마스터 비밀번호 힌트가 담긴 이메일을 보냈습니다." @@ -528,16 +528,16 @@ "message": "선택한 항목을 이 페이지에서 자동 완성할 수 없습니다. 대신 정보를 직접 복사 / 붙여넣기하여 사용하십시오." }, "totpCaptureError": { - "message": "Unable to scan QR code from the current webpage" + "message": "현재 웹페이지에서 QR 코드를 스캔할 수 없습니다" }, "totpCaptureSuccess": { - "message": "Authenticator key added" + "message": "인증 키를 추가했습니다" }, "totpCapture": { - "message": "Scan authenticator QR code from current webpage" + "message": "현재 웹페이지에서 QR 코드 스캔하기" }, "copyTOTP": { - "message": "Copy Authenticator key (TOTP)" + "message": "인증서 키 (TOTP) 복사" }, "loggedOut": { "message": "로그아웃됨" @@ -644,7 +644,7 @@ "description": "This is the folder for uncategorized items" }, "enableAddLoginNotification": { - "message": "Ask to add login" + "message": "로그인을 추가할 건지 물어보기" }, "addLoginNotificationDesc": { "message": "\"로그인 추가 알림\"을 사용하면 새 로그인을 사용할 때마다 보관함에 그 로그인을 추가할 것인지 물어봅니다." @@ -653,7 +653,7 @@ "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, "showCardsCurrentTab": { - "message": "Show cards on Tab page" + "message": "탭 페이지에 카드 표시" }, "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy auto-fill." @@ -679,7 +679,7 @@ "message": "예, 지금 저장하겠습니다." }, "enableChangedPasswordNotification": { - "message": "Ask to update existing login" + "message": "현재 로그인으로 업데이트할 건지 묻기" }, "changedPasswordNotificationDesc": { "message": "Ask to update a login's password when a change is detected on a website." @@ -703,7 +703,7 @@ "message": "Unlock your Bitwarden vault to complete the auto-fill request." }, "notificationUnlock": { - "message": "Unlock" + "message": "잠금 해제" }, "enableContextMenuItem": { "message": "Show context menu options" @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "보관함 잠그기" }, - "privateModeWarning": { - "message": "시크릿 모드 지원은 실험적이며 일부 기능이 제한됩니다." - }, "customFields": { "message": "사용자 지정 필드" }, diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index a01c5069e8..c167d89006 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Užrakinti saugyklą" }, - "privateModeWarning": { - "message": "Privataus režimo palaikymas yra eksperimentinis, o kai kurios funkcijos yra ribotos." - }, "customFields": { "message": "Pasirinktiniai laukai" }, diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index f24f0a93fc..d8e42a1c50 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Aizslēgt glabātavu" }, - "privateModeWarning": { - "message": "Personiskā stāvokļa atbalsts ir izmēģinājuma, un dažas iespējas ir ierobežotas." - }, "customFields": { "message": "Pielāgoti lauki" }, diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index 334027b407..aa4fea96e3 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "നിലവറ പൂട്ടുക" }, - "privateModeWarning": { - "message": "Private mode support is experimental and some features are limited." - }, "customFields": { "message": "ഇഷ്‌ടാനുസൃത ഫീൽഡുകൾ" }, diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index b0e9f8abc1..463309789d 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Lock the vault" }, - "privateModeWarning": { - "message": "Private mode support is experimental and some features are limited." - }, "customFields": { "message": "Custom fields" }, diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index b6384bb840..fec6ab713c 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Lock the vault" }, - "privateModeWarning": { - "message": "Private mode support is experimental and some features are limited." - }, "customFields": { "message": "Custom fields" }, diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index 163154b2f2..ea7b939f63 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Lås hvelvet" }, - "privateModeWarning": { - "message": "Støtte for privatmodus er eksperimentelt, og noen funksjoner er begrenset." - }, "customFields": { "message": "Tilpassede felter" }, diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index b6384bb840..fec6ab713c 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Lock the vault" }, - "privateModeWarning": { - "message": "Private mode support is experimental and some features are limited." - }, "customFields": { "message": "Custom fields" }, diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index cd76fc9684..aa01ef2c67 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Kluis vergrendelen" }, - "privateModeWarning": { - "message": "Private mode ondersteuning is experimenteel en sommige functies zijn beperkt." - }, "customFields": { "message": "Aangepaste velden" }, diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index b6384bb840..fec6ab713c 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Lock the vault" }, - "privateModeWarning": { - "message": "Private mode support is experimental and some features are limited." - }, "customFields": { "message": "Custom fields" }, diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index b6384bb840..fec6ab713c 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Lock the vault" }, - "privateModeWarning": { - "message": "Private mode support is experimental and some features are limited." - }, "customFields": { "message": "Custom fields" }, diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index 1a56d32a35..83e19315e8 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Zablokuj sejf" }, - "privateModeWarning": { - "message": "Obsługa trybu prywatnego jest eksperymentalna, a niektóre funkcje są ograniczone." - }, "customFields": { "message": "Pola niestandardowe" }, diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index 0f40bc63bb..207c890130 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Bloquear o cofre" }, - "privateModeWarning": { - "message": "O suporte para modo privado é experimental e alguns recursos são limitados." - }, "customFields": { "message": "Campos Personalizados" }, diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index 6d6fd70276..26828b348a 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden Password Manager", + "message": "Bitwarden - Gestor de Palavras-passe", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "message": "Em casa, no trabalho, em todo o lado, o Bitwarden protege todas as suas palavras-passe e informações sensíveis", "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { @@ -303,10 +303,10 @@ "message": "Incluir número" }, "minNumbers": { - "message": "Números mínimos" + "message": "Mínimo de números" }, "minSpecial": { - "message": "Caracteres especiais minímos" + "message": "Mínimo de caracteres especiais" }, "avoidAmbChar": { "message": "Evitar caracteres ambíguos" @@ -1064,7 +1064,7 @@ "message": "Editar as definições do navegador." }, "autofillOverlayVisibilityOff": { - "message": "Desligado", + "message": "Desativado", "description": "Overlay setting select option for disabling autofill overlay" }, "autofillOverlayVisibilityOnFieldFocus": { @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Bloquear o cofre" }, - "privateModeWarning": { - "message": "O suporte do modo privado é experimental e algumas funcionalidades são limitadas." - }, "customFields": { "message": "Campos personalizados" }, @@ -1279,7 +1276,7 @@ "message": "Número do passaporte" }, "licenseNumber": { - "message": "Número da licença" + "message": "Número da carta de condução" }, "email": { "message": "E-mail" @@ -1303,7 +1300,7 @@ "message": "Cidade / Localidade" }, "stateProvince": { - "message": "Estado / Província" + "message": "Estado / Região" }, "zipPostalCode": { "message": "Código postal" @@ -1443,7 +1440,7 @@ "description": "ex. Date this item was updated" }, "dateCreated": { - "message": "Criado a", + "message": "Criado", "description": "ex. Date this item was created" }, "datePasswordUpdated": { diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index 780bf69b93..8bcf1a8430 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Blocare seif" }, - "privateModeWarning": { - "message": "Suportul pentru modul privat este experimental, iar unele caracteristici sunt limitate." - }, "customFields": { "message": "Câmpuri particularizate" }, diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index 927095a3f6..23d69f44ba 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Заблокировать хранилище" }, - "privateModeWarning": { - "message": "Частный режим - экспериментальный, некоторые функции ограничены." - }, "customFields": { "message": "Пользовательские поля" }, diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index 33b03f574b..f225e854aa 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "සුරක්ෂිතාගාරය ලොක් කරන්න" }, - "privateModeWarning": { - "message": "Private mode support is experimental and some features are limited." - }, "customFields": { "message": "අභිරුචි ක්ෂේත්ර" }, diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index c84cfbb778..7bcffc04ac 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Zamknúť trezor" }, - "privateModeWarning": { - "message": "Podpora privátneho režimu je experimentálna a niektoré funkcie sú obmedzené." - }, "customFields": { "message": "Vlastné polia" }, diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index 4a6b7cd214..170ee146f7 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Zakleni trezor" }, - "privateModeWarning": { - "message": "Private mode support is experimental and some features are limited." - }, "customFields": { "message": "Polja po meri" }, diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index a04a7ecd70..3827a9dedd 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Закључај сеф" }, - "privateModeWarning": { - "message": "Подршка за приватни режим је експериментална и неке функције су ограничене." - }, "customFields": { "message": "Прилагођена Поља" }, diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index 2b9ec59ec2..0939ac0280 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Lås valvet" }, - "privateModeWarning": { - "message": "Stöd för privat läge är experimentellt och vissa funktioner är begränsade." - }, "customFields": { "message": "Anpassade fält" }, diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index b6384bb840..fec6ab713c 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Lock the vault" }, - "privateModeWarning": { - "message": "Private mode support is experimental and some features are limited." - }, "customFields": { "message": "Custom fields" }, diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index 7e1dda99be..009685b14b 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "ล็อกตู้เซฟ" }, - "privateModeWarning": { - "message": "Private mode support is experimental and some features are limited." - }, "customFields": { "message": "Custom Fields" }, diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index 8a8bb6ea60..866125dbec 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -3,11 +3,11 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden Password Manager", + "message": "Bitwarden Parola Yöneticisi", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "message": "Bitwarden tüm parolalarınızı, geçiş anahtarlarınızı ve hassas bilgilerinizi güvenle saklar", "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { @@ -173,10 +173,10 @@ "message": "Ana parolayı değiştir" }, "continueToWebApp": { - "message": "Continue to web app?" + "message": "Web uygulamasına devam edilsin mi?" }, "changeMasterPasswordOnWebConfirmation": { - "message": "You can change your master password on the Bitwarden web app." + "message": "Ana parolanızı Bitwarden web uygulamasında değiştirebilirsiniz." }, "fingerprintPhrase": { "message": "Parmak izi ifadesi", @@ -333,7 +333,7 @@ "message": "Parola" }, "totp": { - "message": "Authenticator secret" + "message": "Kimlik doğrulama sırrı" }, "passphrase": { "message": "Uzun söz" @@ -528,16 +528,16 @@ "message": "Seçilen hesap bu sayfada otomatik olarak doldurulamadı. Lütfen bilgileri elle kopyalayıp yapıştırın." }, "totpCaptureError": { - "message": "Mevcut web sayfasından QR kodu taranamıyor" + "message": "Mevcut web sayfasındaki QR kodu taranamıyor" }, "totpCaptureSuccess": { "message": "Kimlik doğrulama anahtarı eklendi" }, "totpCapture": { - "message": "Mevcut web sayfasından kimlik doğrulayıcı QR kodunu tarayın" + "message": "Mevcut web sayfasındaki kimlik doğrulayıcı QR kodunu tarayın" }, "copyTOTP": { - "message": "Kimlik Doğrulayıcı anahtarını kopyala (TOTP)" + "message": "Kimlik doğrulama anahtarını kopyala (TOTP)" }, "loggedOut": { "message": "Çıkış yapıldı" @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Kasayı kilitle" }, - "privateModeWarning": { - "message": "Gizli mod desteği deneyseldir ve bazı özellikler kısıtlıdır." - }, "customFields": { "message": "Özel alanlar" }, @@ -2005,7 +2002,7 @@ "message": "Klasör seç..." }, "noFoldersFound": { - "message": "Herhangi bir klasör bulunamadı", + "message": "Hiçbir klasör bulunamadı", "description": "Used as a message within the notification bar when no folders are found" }, "orgPermissionsUpdatedMustSetPassword": { @@ -2652,13 +2649,13 @@ } }, "tryAgain": { - "message": "Tekrar deneyin" + "message": "Yeniden dene" }, "verificationRequiredForActionSetPinToContinue": { - "message": "Bu işlem için doğrulama gerekiyor. Devam etmek için bir PIN ayarlayın." + "message": "Bu işlem için doğrulama gerekiyor. Devam etmek için bir PIN belirleyin." }, "setPin": { - "message": "PIN Belirle" + "message": "PIN belirle" }, "verifyWithBiometrics": { "message": "Biyometri ile doğrula" @@ -2673,7 +2670,7 @@ "message": "Farklı bir yönteme mi ihtiyacınız var?" }, "useMasterPassword": { - "message": "Ana parolayı kullanın" + "message": "Ana parolayı kullan" }, "usePin": { "message": "PIN kullan" @@ -2685,7 +2682,7 @@ "message": "E-posta adresinize gönderilen doğrulama kodunu girin." }, "resendCode": { - "message": "Kodu tekrar gönder" + "message": "Kodu yeniden gönder" }, "total": { "message": "Toplam" @@ -2700,19 +2697,19 @@ } }, "launchDuoAndFollowStepsToFinishLoggingIn": { - "message": "DUO'yu başlatın ve oturum açmayı tamamlamak için adımları izleyin." + "message": "Duo'yu başlatın ve oturum açmayı tamamlamak için adımları izleyin." }, "duoRequiredForAccount": { - "message": "Hesabınız için Duo'ya iki adımlı giriş yapmanız gerekiyor." + "message": "Hesabınız için Duo iki adımlı giriş gereklidir." }, "popoutTheExtensionToCompleteLogin": { - "message": "Oturum açma işlemini tamamlamak için uzantıyı açın." + "message": "Giriş işlemini tamamlamak için uzantıyı dışarı alın." }, "popoutExtension": { - "message": "Popout uzantısı" + "message": "Uzantıyı dışarı al" }, "launchDuo": { - "message": "DUO'yu başlat" + "message": "Duo'yu başlat" }, "importFormatError": { "message": "Veriler doğru biçimlendirilmemiş. Lütfen içe aktarma dosyanızı kontrol edin ve tekrar deneyin." @@ -2795,7 +2792,7 @@ "message": "Geçiş anahtarı klonlanan öğeye kopyalanmayacaktır. Bu öğeyi klonlamaya devam etmek istiyor musunuz?" }, "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { - "message": "Açılan sitenin gerektirdiği doğrulama. Bu özellik henüz ana şifresi olmayan hesaplara uygulanmamaktadır." + "message": "Site kimlik doğrulaması gerektiriyor. Bu özellik henüz ana parolası olmayan hesaplarda kullanılamaz." }, "logInWithPasskey": { "message": "Geçiş anahtarı ile giriş yapılsın mı?" @@ -2828,13 +2825,13 @@ "message": "Geçiş anahtarının üzerine yazılsın mı?" }, "overwritePasskeyAlert": { - "message": "Bu öğe zaten bir şifre anahtarı içeriyor. Geçerli şifrenin üzerine yazmak istediğinizden emin misiniz?" + "message": "Bu kayıt zaten bir geçiş anahtarı içeriyor. Mevcut geçiş anahtarının üzerine yazmak istediğinizden emin misiniz?" }, "featureNotSupported": { "message": "Bu özellik henüz desteklenmiyor" }, "yourPasskeyIsLocked": { - "message": "Şifreyi kullanmak için kimlik doğrulama gerekiyor. Devam etmek için kimliğinizi doğrulayın." + "message": "Geçiş anahtarını kullanmak için kimlik doğrulama gerekiyor. Devam etmek için kimliğinizi doğrulayın." }, "multifactorAuthenticationCancelled": { "message": "Çok faktörlü kimlik doğrulama iptal edildi" @@ -2943,10 +2940,10 @@ "message": "konum" }, "useDeviceOrHardwareKey": { - "message": "Cihazınızı veya donanım anahtarınızı kullanın" + "message": "Cihazınızı veya donanımsal anahtarınızı kullanın" }, "justOnce": { - "message": "Yalnızca bir kez" + "message": "Yalnızca bir defa" }, "alwaysForThisSite": { "message": "Bu site için her zaman" @@ -2961,23 +2958,23 @@ } }, "commonImportFormats": { - "message": "Ortak formatlar", + "message": "Sık kullanılan biçimler", "description": "Label indicating the most common import formats" }, "overrideDefaultBrowserAutofillTitle": { - "message": "Bitwarden varsayılan şifre yöneticiniz yapılsın mı?", + "message": "Bitwarden varsayılan parola yöneticiniz yapılsın mı?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Bu seçeneğin göz ardı edilmesi, Bitwarden otomatik doldurma menüsü ile tarayıcınızınki arasında çakışmalara neden olabilir.", + "message": "Bu seçeneği göz ardı ederseniz Bitwarden otomatik doldurma menüsüyle tarayıcınızınki arasında çakışma yaşanabilir.", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { - "message": "Bitwarden'ı varsayılan şifre yöneticiniz yapın", + "message": "Bitwarden'ı varsayılan parola yöneticiniz yapın", "description": "Label for the setting that allows overriding the default browser autofill settings" }, "privacyPermissionAdditionNotGrantedTitle": { - "message": "Bitwarden varsayılan parola yöneticisi olarak ayarlanamıyor", + "message": "Bitwarden varsayılan parola yöneticisi olarak ayarlanamadı", "description": "Title for the dialog that appears when the user has not granted the extension permission to set privacy settings" }, "privacyPermissionAdditionNotGrantedDescription": { @@ -3001,13 +2998,13 @@ "description": "Notification message for when saving credentials has failed." }, "success": { - "message": "Success" + "message": "Başarılı" }, "removePasskey": { - "message": "Remove passkey" + "message": "Geçiş anahtarını kaldır" }, "passkeyRemoved": { - "message": "Passkey removed" + "message": "Geçiş anahtarı kaldırıldı" }, "unassignedItemsBannerNotice": { "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." @@ -3024,7 +3021,7 @@ "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" + "message": "Yönetici Konsolu" }, "errorAssigningTargetCollection": { "message": "Error assigning target collection." diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index 27293fc992..919066188a 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Заблокувати сховище" }, - "privateModeWarning": { - "message": "Приватний режим - це експериментальна функція і деякі можливості обмежені." - }, "customFields": { "message": "Власні поля" }, diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index 6e530412db..f7a0d50bd5 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Khoá kho lưu trữ" }, - "privateModeWarning": { - "message": "Hỗ trợ cho chế độ riêng tư đang được thử nghiệm và hạn chế một số tính năng." - }, "customFields": { "message": "Trường tùy chỉnh" }, diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index 3cf2f96da1..706bf7a851 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "锁定密码库" }, - "privateModeWarning": { - "message": "私密模式的支持是实验性的,某些功能会受到限制。" - }, "customFields": { "message": "自定义字段" }, @@ -3016,11 +3013,11 @@ "message": "注意:从 2024 年 5 月 16 日起,未分配的组织项目在「所有密码库」视图中将不再可见,只能通过管理控制台访问。" }, "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", + "message": "将这些项目分配到集合,通过", "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": "以使其可见。", + "message": ",以使其可见。", "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": { diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index eb35cd08c7..9300289795 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "鎖定密碼庫" }, - "privateModeWarning": { - "message": "私密模式的支援是實驗性功能,部分功能無法完全發揮作用。" - }, "customFields": { "message": "自訂欄位" }, diff --git a/apps/browser/store/locales/ca/copy.resx b/apps/browser/store/locales/ca/copy.resx index 27e685841b..5a06f818e3 100644 --- a/apps/browser/store/locales/ca/copy.resx +++ b/apps/browser/store/locales/ca/copy.resx @@ -118,58 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden Password Manager</value> + <value>Bitwarden - Gestor de contrasenyes</value> </data> <data name="Summary" xml:space="preserve"> - <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> + <value>A casa, a la feina o en moviment, Bitwarden protegeix totes les contrasenyes, claus de pas i informació sensible.</value> </data> <data name="Description" xml:space="preserve"> - <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! + <value>Reconegut com el millor gestor de contrasenyes per PCMag, WIRED, The Verge, CNET, G2 i més! -SECURE YOUR DIGITAL LIFE -Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. +ASSEGURA LA TEUA VIDA DIGITAL +Assegureu-vos la vostra vida digital i protegiu-vos de les violacions de dades generant i desant contrasenyes úniques i fortes per a cada compte. Mantingueu-ho tot en una caixa de contrasenyes xifrada d'extrem a extrem a la qual només podeu accedir. -ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE -Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. +ACCEDEIX A LES SEUES DADES, ON, EN QUALSEVOL MOMENT, EN QUALSEVOL DISPOSITIU +Gestiona, emmagatzema, protegeix i comparteix fàcilment contrasenyes il·limitades en dispositius il·limitats sense restriccions. -EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE -Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. +TOTHOM HA DE TENIR LES EINES PER ESTAR SEGURETAT EN LÍNIA +Utilitzeu Bitwarden de forma gratuïta sense anuncis ni dades de venda. Bitwarden creu que tothom hauria de tenir la capacitat de mantenir-se segur en línia. Els plans Prèmium ofereixen accés a funcions avançades. -EMPOWER YOUR TEAMS WITH BITWARDEN -Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. +EMPODERA ELS TEUS EQUIPS AMB BITWARDEN +Els plans per a equips i empreses inclouen funcions empresarials professionals. Alguns exemples inclouen integració SSO, autoallotjament, integració de directoris i subministrament SCIM, polítiques globals, accés a API, registres d'esdeveniments i molt més. -Use Bitwarden to secure your workforce and share sensitive information with colleagues. +Utilitzeu Bitwarden per protegir la vostra força de treball i compartir informació crítica amb els companys. -More reasons to choose Bitwarden: +Més raons per a triar Bitwarden: -World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. +Xifratge de classe mundial +Les contrasenyes estan protegides amb un xifratge avançat d'extrem a extrem (AES-256 bits, hashtag salat i PBKDF2 SHA-256) perquè les vostres dades es mantinguen segures i privades. -3rd-party Audits -Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. +Auditories de tercers +Bitwarden realitza regularment auditories de seguretat exhaustives de tercers amb empreses de seguretat notables. Aquestes auditories anuals inclouen avaluacions del codi font i proves de penetració a les IP, servidors i aplicacions web de Bitwarden. -Advanced 2FA -Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. +2FA avançat +Assegureu el vostre inici de sessió amb un autenticador de tercers, codis enviats per correu electrònic o credencials FIDO2 WebAuthn, com ara una clau de seguretat de maquinari o una clau de pas. -Bitwarden Send -Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. +Bitwarden Enviar +Transmet dades directament a altres, mantenint la seguretat xifrada d'extrem a extrem i limitant l'exposició. -Built-in Generator -Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. +Generador incorporat +Creeu contrasenyes llargues, complexes i diferents i noms d'usuari únics per a cada lloc que visiteu. Integració amb proveïdors d'àlies de correu electrònic per obtenir més privadesa. -Global Translations -Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. +Traduccions globals +Les traduccions de Bitwarden existeixen per a més de 60 idiomes, traduïdes per la comunitat global mitjançant Crowdin. -Cross-Platform Applications -Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. +Aplicacions multiplataforma +Assegureu-vos i compartiu dades confidencials a la vostra caixa forta Bitwarden des de qualsevol navegador, dispositiu mòbil o sistema operatiu d'escriptori i molt més. -Bitwarden secures more than just passwords -End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! +Bitwarden assegura més que només contrasenyes +Les solucions de gestió de credencials xifrades d'extrem a extrem de Bitwarden permeten a les organitzacions protegir-ho tot, inclosos els secrets dels desenvolupadors i les experiències de clau de pas. Visiteu Bitwarden.com per obtenir més informació sobre Bitwarden gestor de secrets i Bitwarden Passwordless.dev! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> + <value>A casa, a la feina o en moviment, Bitwarden protegeix totes les contrasenyes, claus de pas i informació sensible.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Sincronitzeu i accediu a la vostra caixa forta des de diversos dispositius</value> diff --git a/apps/browser/store/locales/cs/copy.resx b/apps/browser/store/locales/cs/copy.resx index 59d8c60b40..c3a58379dc 100644 --- a/apps/browser/store/locales/cs/copy.resx +++ b/apps/browser/store/locales/cs/copy.resx @@ -118,64 +118,64 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden Password Manager</value> + <value>Bitwarden - Správce hesel</value> </data> <data name="Summary" xml:space="preserve"> - <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> + <value>Bitwarden zabezpečí všechna Vaše hesla, přístupové klíče a citlivé informace doma, v práci nebo na cestách.</value> </data> <data name="Description" xml:space="preserve"> - <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! + <value>PCMag, WIRED, The Verge, CNET, G2 a další ocenili tohoto správce hesel jako nejlepší! -SECURE YOUR DIGITAL LIFE -Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. +ZABEZPEČTE SVŮJ DIGITÁLNÍ ŽIVOT +Zabezpečte svůj digitální život a chraňte se před únikem dat tím, že si pro každý účet vytvoříte a uložíte jedinečná, silná hesla. Vše uchováváte v end-to-end šifrovaném trezoru hesel, ke kterému máte přístup jen Vy. -ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE -Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. +PŘÍSTUP K DATŮM ODKUDKOLI, KDYKOLI A Z JAKÉHOKOLI ZAŘÍZENÍ +Snadno spravujte, ukládejte, zabezpečujte a sdílejte neomezený počet hesel na neomezeném počtu zařízení bez omezení. -EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE -Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. +KAŽDÝ BY MĚL MÍT K DISPOZICI NÁSTROJE, KTERÉ MU UMOŽNÍ ZŮSTAT V BEZPEČÍ ONLINE +Využívejte Bitwarden zdarma bez reklam a prodeje dat. Bitwarden věří, že každý by měl mít možnost zůstat v bezpečí online. Prémiové plány nabízejí přístup k pokročilým funkcím. -EMPOWER YOUR TEAMS WITH BITWARDEN -Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. +POSILTE SVÉ TÝMY POMOCÍ BITWARDEN +Plány pro týmy a podniky jsou vybaveny profesionálními podnikovými funkcemi. Mezi příklady patří integrace SSO, selfhosting, integrace adresářů a poskytování SCIM, globální zásady, přístup k API, protokoly událostí a další. -Use Bitwarden to secure your workforce and share sensitive information with colleagues. +Použijte Bitwarden k zabezpečení svých zaměstnanců a sdílení citlivých informací s kolegy. -More reasons to choose Bitwarden: +Další důvody, proč si vybrat Bitwarden: -World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. +Šifrování na světové úrovni +Hesla jsou chráněna pokročilým end-to-end šifrováním (AES-256 bitů, solený hashtag a PBKDF2 SHA-256), takže Vaše data zůstanou v bezpečí a soukromí. -3rd-party Audits -Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. +Audity třetích stran +Společnost Bitwarden pravidelně provádí komplexní bezpečnostní audity třetích stran s významnými bezpečnostními firmami. Tyto každoroční audity zahrnují posouzení zdrojového kódu a penetrační testy napříč IP adresami, servery a webovými aplikacemi společnosti Bitwarden. -Advanced 2FA -Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. +Pokročilé 2FA +Zabezpečte své přihlášení pomocí ověření třetí strany, e-mailových kódů nebo ověření FIDO2 WebAuthn, jako je hardwarový bezpečnostní klíč nebo přístupový klíč. Bitwarden Send -Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. +Přenášejte data přímo ostatním při zachování end-to-end šifrovaného zabezpečení a omezení odhalení. -Built-in Generator -Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. +Vestavěný generátor +Vytvářejte dlouhá, složitá a odlišná hesla a jedinečná uživatelská jména pro každou navštívenou stránku. Integrace s poskytovateli e-mailových aliasů pro zajištění dalšího soukromí. -Global Translations -Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. +Globální překlady +Pro Bitwarden existují překlady ve více než 60 jazycích, které překládá globální komunita prostřednictvím služby Crowdin. -Cross-Platform Applications -Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. +Aplikace pro více platforem +Zabezpečte a sdílejte citlivá data v rámci svého trezoru Bitwarden z jakéhokoli prohlížeče, mobilního zařízení nebo desktopového operačního systému a dalších. -Bitwarden secures more than just passwords -End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! +Bitwarden zabezpečuje více než jen hesla +Řešení pro komplexní správu šifrovaných pověření od společnosti Bitwarden umožňují organizacím zabezpečit vše, včetně přístupových a/nebo tajných klíčů pro vývojáře. Navštivte Bitwarden.com a dozvíte se více o Bitwarden Secrets Manager a Bitwarden Passwordless.dev! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> + <value>Bitwarden zabezpečí všechna Vaše hesla, přístupové klíče a citlivé informace doma, v práci nebo na cestách.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Synchronizujte a přistupujte ke svému trezoru z různých zařízení</value> </data> <data name="ScreenshotVault" xml:space="preserve"> - <value>Spravujte veškeré své přihlašovací údaje z bezpečného trezoru</value> + <value>Spravujte veškeré své přihlašovací údaje v bezpečném trezoru</value> </data> <data name="ScreenshotAutofill" xml:space="preserve"> <value>Rychle vyplňte své přihlašovací údaje na webových stránkách</value> diff --git a/apps/browser/store/locales/fi/copy.resx b/apps/browser/store/locales/fi/copy.resx index 42e914a13f..a50cedbdac 100644 --- a/apps/browser/store/locales/fi/copy.resx +++ b/apps/browser/store/locales/fi/copy.resx @@ -118,10 +118,10 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden – Salasanahallinta</value> + <value>Bitwarden Salasanahallinta</value> </data> <data name="Summary" xml:space="preserve"> - <value>Kotona, töissä tai reissussa, Bitwarden suojaa helposti kaikki salasanasi, avainkoodisi ja arkaluonteiset tietosi.</value> + <value>Kotona, töissä tai reissussa, Bitwarden suojaa helposti salasanasi, suojausavaimesi ja arkaluonteiset tietosi.</value> </data> <data name="Description" xml:space="preserve"> <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! @@ -169,7 +169,7 @@ End-to-end encrypted credential management solutions from Bitwarden empower orga </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>Kotona, töissä tai reissussa, Bitwarden suojaa helposti kaikki salasanasi, avainkoodisi ja arkaluonteiset tietosi.</value> + <value>Kotona, töissä tai reissussa, Bitwarden suojaa helposti salasanasi, suojausavaimesi ja arkaluonteiset tietosi.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Synkronoi ja hallitse holviasi useilla laitteilla</value> diff --git a/apps/browser/store/locales/fr/copy.resx b/apps/browser/store/locales/fr/copy.resx index 9927f885d3..5c690049d0 100644 --- a/apps/browser/store/locales/fr/copy.resx +++ b/apps/browser/store/locales/fr/copy.resx @@ -118,10 +118,10 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden Password Manager</value> + <value>Gestionnaire de mots de passe Bitwarden</value> </data> <data name="Summary" xml:space="preserve"> - <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> + <value>À la maison, au travail ou en déplacement, Bitwarden sécurise facilement tous vos mots de passe, clés d'accès et informations sensibles.</value> </data> <data name="Description" xml:space="preserve"> <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! @@ -169,7 +169,7 @@ End-to-end encrypted credential management solutions from Bitwarden empower orga </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> + <value>À la maison, au travail ou en déplacement, Bitwarden sécurise facilement tous vos mots de passe, clés d'accès et informations sensibles.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Synchroniser et accéder à votre coffre depuis plusieurs appareils</value> diff --git a/apps/browser/store/locales/it/copy.resx b/apps/browser/store/locales/it/copy.resx index bcbbe10512..cfa7111c3f 100644 --- a/apps/browser/store/locales/it/copy.resx +++ b/apps/browser/store/locales/it/copy.resx @@ -121,55 +121,55 @@ <value>Bitwarden Password Manager</value> </data> <data name="Summary" xml:space="preserve"> - <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> + <value>A casa, al lavoro, o in viaggio, Bitwarden protegge facilmente tutte le tue password, passkey, e informazioni sensibili.</value> </data> <data name="Description" xml:space="preserve"> - <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! + <value>Riconosciuto come il miglior password manager da PCMag, WIRED, The Verge, CNET, G2, e altri! -SECURE YOUR DIGITAL LIFE -Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. +PROTEGGI LA TUA VITA DIGITALE +Proteggi la tua vita digitale e proteggiti dalle violazioni dei dati generando e salvando password uniche e complesse per ogni account. Mantieni tutto in un archivio di password crittografato end-to-end a cui solo tu puoi accedere. -ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE -Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. +ACCEDI AI TUOI DATI, OVUNQUE, IN QUALSIASI MOMENTO, SU QUALSIASI DISPOSITIVO +Gestisci, archivia, proteggi, e condividi facilmente password illimitate su un numero illimitato di dispositivi senza restrizioni. -EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE -Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. +TUTTI DOVREBBERO POTER RIMANERE AL SICURO ONLINE +Usa Bitwarden gratuitamente senza pubblicità o vendita di dati. Bitwarden ritiene che tutti dovrebbero avere la possibilità di rimanere al sicuro online. I piani premium offrono l'accesso a funzionalità avanzate. -EMPOWER YOUR TEAMS WITH BITWARDEN -Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. +POTENZIA I TUOI TEAM CON BITWARDEN +I piani per Teams ed Enterprise includono funzionalità aziendali professionali. Alcuni esempi includono integrazione SSO, self-hosting, integrazione di directory e provisioning SCIM, politiche globali, accesso API, registri eventi, e altro. -Use Bitwarden to secure your workforce and share sensitive information with colleagues. +Utilizza Bitwarden per proteggere i tuoi dipendenti e condividere informazioni sensibili con i colleghi. -More reasons to choose Bitwarden: +Altri motivi per scegliere Bitwarden: -World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. +Crittografia di livello mondiale +Le password sono protette con crittografia end-to-end avanzata (AES-256 bit, salted hashtag, e PBKDF2 SHA-256) in modo che i tuoi dati rimangano sicuri e privati. -3rd-party Audits -Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. +Controlli di terze parti +Bitwarden conduce regolarmente controlli di sicurezza completi di terze parti con importanti società di sicurezza. Questi controlli annuali includono valutazioni del codice sorgente e test di penetrazione su IP, server, e applicazioni web di Bitwarden. -Advanced 2FA -Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. +2FA avanzato +Proteggi il tuo accesso con un autenticatore di terze parti, codici inviati via email, o credenziali FIDO2 WebAuthn come una chiave di sicurezza hardware o una passkey. Bitwarden Send -Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. +Trasmetti dati sensibili direttamente ad altri mantenendo la sicurezza crittografata end-to-end e limitando l'esposizione. -Built-in Generator -Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. +Generatore incorporato +Crea password lunghe, complesse, e distinte e nomi utente univoci per ogni sito che visiti. Integrazione con fornitori di alias e-mail per una maggiore privacy. -Global Translations -Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. +Traduzioni globali +Le traduzioni di Bitwarden esistono per più di 60 lingue, tradotte dalla comunità globale tramite Crowdin. -Cross-Platform Applications -Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. +App multipiattaforma +Proteggi e condividi i dati sensibili all'interno della tua cassaforte di Bitwarden da qualsiasi browser, dispositivo mobile, o sistema operativo desktop. -Bitwarden secures more than just passwords -End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! +Bitwarden protegge molto più che semplici password +Le soluzioni di gestione delle credenziali crittografate end-to-end di Bitwarden consentono alle organizzazioni di proteggere tutto, compresi i segreti degli sviluppatori ed esperienze con le passkey. Visita Bitwarden.com per saperne di più su Bitwarden Secrets Manager e Bitwarden Passwordless.dev! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> + <value>A casa, al lavoro, o in viaggio, Bitwarden protegge facilmente tutte le tue password, passkey, e informazioni sensibili.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Sincronizza e accedi alla tua cassaforte da più dispositivi</value> diff --git a/apps/browser/store/locales/pt_PT/copy.resx b/apps/browser/store/locales/pt_PT/copy.resx index d310629612..34461983bc 100644 --- a/apps/browser/store/locales/pt_PT/copy.resx +++ b/apps/browser/store/locales/pt_PT/copy.resx @@ -118,58 +118,58 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden Password Manager</value> + <value>Bitwarden - Gestor de Palavras-passe</value> </data> <data name="Summary" xml:space="preserve"> - <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> + <value>Em casa, no trabalho, em todo o lado, o Bitwarden protege facilmente todas as suas palavras-passe, chaves de acesso e informações sensíveis.</value> </data> <data name="Description" xml:space="preserve"> - <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! + <value>Reconhecido como o melhor gestor de palavras-passe pela PCMag, WIRED, The Verge, CNET, G2 e muito mais! -SECURE YOUR DIGITAL LIFE -Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. +PROTEJA A SUA VIDA DIGITAL +Proteja a sua vida digital e proteja-se contra violações de dados, ao gerar e guardar palavras-passe únicas e fortes para cada conta. Guarde tudo num cofre de palavras-passe encriptadas ponto a ponto a que só você pode aceder. -ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE -Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. +ACEDA AOS SEUS DADOS, EM QUALQUER LUGAR, A QUALQUER HORA, EM QUALQUER DISPOSITIVO +Gerir, armazenar, proteger e partilhar facilmente palavras-passe ilimitadas em dispositivos ilimitados, sem restrições. -EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE -Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. +TODOS DEVEM TER AS FERRAMENTAS PARA SE MANTEREM SEGUROS ONLINE +Utilize o Bitwarden gratuitamente, sem anúncios ou venda de dados. O Bitwarden acredita que todos devem ter a capacidade de se manterem seguros online. Os planos Premium oferecem acesso a funcionalidades avançadas. -EMPOWER YOUR TEAMS WITH BITWARDEN -Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. +CAPACITE AS SUAS EQUIPAS COM O BITWARDEN +Os planos Equipas e Empresarial vêm com funcionalidades profissionais de negócios. Alguns exemplos incluem a integração SSO, auto-hospedagem, integração de diretório e provisionamento SCIM, políticas globais, acesso à API, logs de eventos e muito mais. -Use Bitwarden to secure your workforce and share sensitive information with colleagues. +Utilize o Bitwarden para proteger a sua equipa de trabalho e partilhar informações sensíveis com os colegas. -More reasons to choose Bitwarden: +Mais motivos para escolher o Bitwarden: -World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. +Encriptação de classe mundial +As palavras-passe são protegidas com encriptação avançada ponto a ponto (AES-256 bits, salted hashtag e PBKDF2 SHA-256) para que os seus dados permaneçam seguros e privados. -3rd-party Audits -Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. +Auditorias de terceiros +O Bitwarden realiza regularmente auditorias abrangentes de segurança de terceiros com empresas de segurança notáveis. Estas auditorias anuais incluem avaliações de código-fonte e testes de penetração em IPs, servidores e aplicações Web do Bitwarden. -Advanced 2FA -Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. +2FA avançado +Proteja o seu início de sessão com um autenticador de terceiros, códigos enviados por e-mail ou credenciais FIDO2 WebAuthn, como uma chave de segurança de hardware ou uma chave de acesso. Bitwarden Send -Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. +Envie dados diretamente a outros, mantendo a segurança encriptada ponto a ponto e limitando a exposição. -Built-in Generator -Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. +Gerador incorporado +Crie palavras-passe longas, complexas e distintas e nomes de utilizador únicos para cada site que visita. Integre-se com fornecedores de pseudónimos de e-mail para privacidade adicional. -Global Translations -Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. +Traduções globais +Existem traduções do Bitwarden para mais de 60 idiomas, traduzidas pela comunidade global através do Crowdin. -Cross-Platform Applications -Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. +Aplicações multiplataforma +Proteja e partilhe dados confidenciais dentro do seu cofre Bitwarden a partir de qualquer navegador, dispositivo móvel, ou SO de computador, e muito mais. -Bitwarden secures more than just passwords -End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! +O Bitwarden protege mais do que apenas palavras-passe +As soluções de gestão de credenciais encriptadas ponto a ponto do Bitwarden permitem que as organizações protejam tudo, incluindo segredos de programadores e experiências com chaves de acesso. Visite Bitwarden.com para saber mais sobre o Gestor de Segredos do Bitwarden e o Bitwarden Passwordless.dev! </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> + <value>Em casa, no trabalho, em todo o lado, o Bitwarden protege facilmente todas as suas palavras-passe, chaves de acesso e informações sensíveis.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Sincronize e aceda ao seu cofre através de vários dispositivos</value> diff --git a/apps/browser/store/locales/tr/copy.resx b/apps/browser/store/locales/tr/copy.resx index 539aad3aee..fa53d09ee1 100644 --- a/apps/browser/store/locales/tr/copy.resx +++ b/apps/browser/store/locales/tr/copy.resx @@ -118,10 +118,10 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="Name" xml:space="preserve"> - <value>Bitwarden Password Manager</value> + <value>Bitwarden Parola Yöneticisi</value> </data> <data name="Summary" xml:space="preserve"> - <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> + <value>İster evde ister işte veya yolda olun; Bitwarden tüm parolalarınızı, geçiş anahtarlarınızı ve hassas bilgilerinizi güvenle saklar.</value> </data> <data name="Description" xml:space="preserve"> <value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! @@ -169,7 +169,7 @@ End-to-end encrypted credential management solutions from Bitwarden empower orga </value> </data> <data name="AssetTitle" xml:space="preserve"> - <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> + <value>İster evde ister işte veya yolda olun; Bitwarden tüm parolalarınızı, geçiş anahtarlarınızı ve hassas bilgilerinizi güvenle saklar.</value> </data> <data name="ScreenshotSync" xml:space="preserve"> <value>Hesabınızı senkronize ederek kasanıza tüm cihazlarınızdan ulaşın</value> From 1f6eec4dd5caecb817448eadf0ebb369235eeaf2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 3 May 2024 07:22:44 +0000 Subject: [PATCH 349/351] Autosync the updated translations (#9028) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/web/src/locales/af/messages.json | 46 ++++-- apps/web/src/locales/ar/messages.json | 46 ++++-- apps/web/src/locales/az/messages.json | 52 ++++-- apps/web/src/locales/be/messages.json | 46 ++++-- apps/web/src/locales/bg/messages.json | 46 ++++-- apps/web/src/locales/bn/messages.json | 46 ++++-- apps/web/src/locales/bs/messages.json | 46 ++++-- apps/web/src/locales/ca/messages.json | 192 +++++++++++++---------- apps/web/src/locales/cs/messages.json | 52 ++++-- apps/web/src/locales/cy/messages.json | 46 ++++-- apps/web/src/locales/da/messages.json | 46 ++++-- apps/web/src/locales/de/messages.json | 46 ++++-- apps/web/src/locales/el/messages.json | 46 ++++-- apps/web/src/locales/en_GB/messages.json | 46 ++++-- apps/web/src/locales/en_IN/messages.json | 46 ++++-- apps/web/src/locales/eo/messages.json | 46 ++++-- apps/web/src/locales/es/messages.json | 46 ++++-- apps/web/src/locales/et/messages.json | 46 ++++-- apps/web/src/locales/eu/messages.json | 46 ++++-- apps/web/src/locales/fa/messages.json | 46 ++++-- apps/web/src/locales/fi/messages.json | 50 ++++-- apps/web/src/locales/fil/messages.json | 46 ++++-- apps/web/src/locales/fr/messages.json | 46 ++++-- apps/web/src/locales/gl/messages.json | 46 ++++-- apps/web/src/locales/he/messages.json | 46 ++++-- apps/web/src/locales/hi/messages.json | 46 ++++-- apps/web/src/locales/hr/messages.json | 46 ++++-- apps/web/src/locales/hu/messages.json | 46 ++++-- apps/web/src/locales/id/messages.json | 46 ++++-- apps/web/src/locales/it/messages.json | 80 ++++++---- apps/web/src/locales/ja/messages.json | 46 ++++-- apps/web/src/locales/ka/messages.json | 46 ++++-- apps/web/src/locales/km/messages.json | 46 ++++-- apps/web/src/locales/kn/messages.json | 46 ++++-- apps/web/src/locales/ko/messages.json | 52 ++++-- apps/web/src/locales/lv/messages.json | 46 ++++-- apps/web/src/locales/ml/messages.json | 46 ++++-- apps/web/src/locales/mr/messages.json | 46 ++++-- apps/web/src/locales/my/messages.json | 46 ++++-- apps/web/src/locales/nb/messages.json | 46 ++++-- apps/web/src/locales/ne/messages.json | 46 ++++-- apps/web/src/locales/nl/messages.json | 46 ++++-- apps/web/src/locales/nn/messages.json | 46 ++++-- apps/web/src/locales/or/messages.json | 46 ++++-- apps/web/src/locales/pl/messages.json | 46 ++++-- apps/web/src/locales/pt_BR/messages.json | 40 ++++- apps/web/src/locales/pt_PT/messages.json | 52 ++++-- apps/web/src/locales/ro/messages.json | 46 ++++-- apps/web/src/locales/ru/messages.json | 48 ++++-- apps/web/src/locales/si/messages.json | 46 ++++-- apps/web/src/locales/sk/messages.json | 46 ++++-- apps/web/src/locales/sl/messages.json | 46 ++++-- apps/web/src/locales/sr/messages.json | 46 ++++-- apps/web/src/locales/sr_CS/messages.json | 46 ++++-- apps/web/src/locales/sv/messages.json | 46 ++++-- apps/web/src/locales/te/messages.json | 46 ++++-- apps/web/src/locales/th/messages.json | 46 ++++-- apps/web/src/locales/tr/messages.json | 106 ++++++++----- apps/web/src/locales/uk/messages.json | 46 ++++-- apps/web/src/locales/vi/messages.json | 46 ++++-- apps/web/src/locales/zh_CN/messages.json | 46 ++++-- apps/web/src/locales/zh_TW/messages.json | 46 ++++-- 62 files changed, 2364 insertions(+), 752 deletions(-) diff --git a/apps/web/src/locales/af/messages.json b/apps/web/src/locales/af/messages.json index 2c226ec0b8..2c05de9954 100644 --- a/apps/web/src/locales/af/messages.json +++ b/apps/web/src/locales/af/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Onbeveiligde webwerwe gevind" }, - "unsecuredWebsitesFoundDesc": { - "message": "We found $COUNT$ items in your vault with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Logins without two-step login found" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Blootgestelde wagwoorde gevind" }, - "exposedPasswordsFoundDesc": { - "message": "Ons het $COUNT$ wagwoorde in u kluis gevind wat in databreuke blootgestel is. U behoort dit te verander en ’n nuwe wagwoord te gebruik.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Swak wagwoorde gevind" }, - "weakPasswordsFoundDesc": { - "message": "Ons het $COUNT$ swak wagwoorde in u kluis gevind. U behoort dit by te werk en sterker wagwoorde te gebruik.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Hergebruikte wagwoorde gevind" }, - "reusedPasswordsFoundDesc": { - "message": "Ons het $COUNT$ wagwoorde in u kluis gewind wat hergebruik word. U behoort dit na ’n unieke waarde te verander.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Verleen toegang tot versamelings deur dit tot hierdie groep toe te voeg." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Verleen toegang tot alle huidige en toekomstige versamelings." }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/ar/messages.json b/apps/web/src/locales/ar/messages.json index be25dead79..9353797bb4 100644 --- a/apps/web/src/locales/ar/messages.json +++ b/apps/web/src/locales/ar/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Unsecured websites found" }, - "unsecuredWebsitesFoundDesc": { - "message": "We found $COUNT$ items in your vault with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Logins without two-step login found" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Exposed passwords found" }, - "exposedPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault that have passwords that were exposed in known data breaches. You should change them to use a new password.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Weak passwords found" }, - "weakPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault with passwords that are not strong. You should update them to use stronger passwords.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "تم العثور على كلمات مرور معاد استخدامها" }, - "reusedPasswordsFoundDesc": { - "message": "We found $COUNT$ passwords that are being reused in your vault. You should change them to a unique value.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/az/messages.json b/apps/web/src/locales/az/messages.json index c3ae7b07b6..19e38577a4 100644 --- a/apps/web/src/locales/az/messages.json +++ b/apps/web/src/locales/az/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Güvənli olmayan veb sayt tapıldı" }, - "unsecuredWebsitesFoundDesc": { - "message": "Anbarınızda güvənli olmayan URI-lərə sahib $COUNT$ element tapıldı. Veb sayt icazə verirsə, onların URI sxemini https:// olaraq dəyişdirməlisiniz.", + "unsecuredWebsitesFoundReportDesc": { + "message": "$VAULT$ daxilində güvənli olmayan URI-lərə sahib $COUNT$ element tapdıq. Veb sayt icazə verirsə, onların URI sxemini https:// olaraq dəyişdirməlisiniz.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "2FA olmayan hesablar tapıldı" }, - "inactive2faFoundDesc": { - "message": "Anbarınızda (\"2fa.directory\"ə uyğun olaraq) iki addımlı kimlik doğrulama ilə konfiqurasiya edilmədiyini düşündüyümüz $COUNT$ veb sayt tapdıq. Bu hesabları daha çox qorumaq üçün iki addımlı girişi qurmalısınız.", + "inactive2faFoundReportDesc": { + "message": "$VAULT$ daxilində (\"2fa.directory\"ə uyğun olaraq) iki addımlı kimlik doğrulama ilə konfiqurasiya edilmədiyini düşündüyümüz $COUNT$ veb sayt tapdıq. Bu hesabları daha çox qorumaq üçün iki addımlı girişi qurmalısınız.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "İfşa olunmuş parollar tapıldı" }, - "exposedPasswordsFoundDesc": { - "message": "Anbarınızda, bilinən məlumat pozuntusunda parolları ifşa olunmuş $COUNT$ element tapdıq. Yeni bir parol istifadə etməzdən əvvəl onları dəyişdirməlisiniz.", + "exposedPasswordsFoundReportDesc": { + "message": "$VAULT$ daxilində, bilinən məlumat pozuntusunda parolları ifşa olunmuş $COUNT$ element tapdıq. Yeni bir parol istifadə etməzdən əvvəl onları dəyişdirməlisiniz.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Zəif parol tapıldı" }, - "weakPasswordsFoundDesc": { - "message": "Anbarınızda, şifrələri güclü olmayan $COUNT$ element tapdıq. Güclü şifrələr istifadə etmək üçün onları güncəlləməlisiniz.", + "weakPasswordsFoundReportDesc": { + "message": "$VAULT$ daxilində, parolları güclü olmayan $COUNT$ element tapdıq. Güclü parollar istifadə etmək üçün onları güncəlləməlisiniz.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Təkrar istifadə olunmuş parollar tapıldı" }, - "reusedPasswordsFoundDesc": { - "message": "Anbarınızda təkrar istifadə edilən $COUNT$ parol tapdıq. Onları unikal bir dəyərlə dəyişdirməlisiniz.", + "reusedPasswordsFoundReportDesc": { + "message": "$VAULT$ daxilində təkrar istifadə edilən $COUNT$ parol tapdıq. Onları unikal bir dəyərlə dəyişdirməlisiniz.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -4247,7 +4267,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendProtectedPasswordDontKnow": { - "message": "Şifrəni bilmirsiniz? Bu \"Send\"ə müraciət etmək üçün parolu göndərən şəxsdən istəyin.", + "message": "Parolu bilmirsiniz? Bu \"Send\"ə müraciət etmək üçün parolu göndərən şəxsdən istəyin.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendHiddenByDefault": { @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Bu qrupa əlavə edərək kolleksiyalara müraciət icazəsi verin." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "Yalnız idarə etdiyiniz kolleksiyaları təyin edə bilərsiniz." + }, "accessAllCollectionsDesc": { "message": "Hazırkı və gələcəkdəki bütün kolleksiyalara müraciət icazəsi verin." }, @@ -7973,13 +7996,13 @@ "message": "İnteqrasiyalar" }, "integrationsDesc": { - "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + "message": "\"Bitwarden Sirr Meneceri\"ndəki sirləri avtomatik olaraq üçüncü tərəf xidmətlə sinxronlaşdır." }, "sdks": { "message": "SDK-lar" }, "sdksDesc": { - "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + "message": "Öz tətbiqlərinizi qurmaq üçün \"Bitwarden Sirr Meneceri SDK\"sını aşağıdakı proqramlaşdırma dillərində istifadə edin." }, "setUpGithubActions": { "message": "Github Actions qur" @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Kolleksiya elementini seç" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Fakturanı \"Provayder Portalı\"ndan idarə et" } } diff --git a/apps/web/src/locales/be/messages.json b/apps/web/src/locales/be/messages.json index 7c3b76b610..de563800ea 100644 --- a/apps/web/src/locales/be/messages.json +++ b/apps/web/src/locales/be/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Знойдзены неабароненыя вэб-сайты" }, - "unsecuredWebsitesFoundDesc": { - "message": "У сховішчы ёсць элементы ($COUNT$ шт.) з неабароненымі URI. Вам неабходна змяніць іх схему URI на https://, калі вэб-сайт дазваляе гэта зрабіць.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Знойдзены лагіны без 2ФА" }, - "inactive2faFoundDesc": { - "message": "У сховішчы ёсць вэб-сайты ($COUNT$ шт.), якія могуць быць не наладжаны для двухэтапнай аўтэнтыфікацыі (згодна з 2fa.directory). Для дадатковай абароны гэтых уліковых запісаў уключыце двухэтапную аўтэнтыфікацыю.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Знойдзены скампраметаваныя паролі" }, - "exposedPasswordsFoundDesc": { - "message": "У сховішчы выяўлены элементы ($COUNT$ шт.) з паролямі, якія былі скампраметаваны ў вядомых базах уцечак. Вам неабходна выбраць для іх іншы пароль.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Знойдзены ненадзейныя паролі" }, - "weakPasswordsFoundDesc": { - "message": "У сховішчы ёсць элементы ($COUNT$ шт.) з ненадзейнымі паролямі. Вам неабходна замяніць іх на больш надзейныя.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Знойдзены паўторныя паролі" }, - "reusedPasswordsFoundDesc": { - "message": "У сховішчы ёсць паўторныя паролі ($COUNT$ шт.). Вам неабходна згенерыраваць унікальныя паролі і замяніць іх.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Забяспечыць доступ да калекцый, дадаўшы іх у гэту групу." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Даць доступ да ўсіх бягучых і будучых калекцый." }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/bg/messages.json b/apps/web/src/locales/bg/messages.json index e0d0f00174..f5465fb658 100644 --- a/apps/web/src/locales/bg/messages.json +++ b/apps/web/src/locales/bg/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Открити са записи без защита" }, - "unsecuredWebsitesFoundDesc": { - "message": "Открити са $COUNT$ записи в трезора с адрес без защита. Трябва да смените схемата за URI-то им на „https://“, ако сайтовете го поддържат.", + "unsecuredWebsitesFoundReportDesc": { + "message": "Във Вашия $VAULT$ има $COUNT$ елемента с незащитени адреси. Препоръчително е да смените протокола им на „https://“, ако уеб сайтовете го поддържат.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Открити са записи без двустепенна идентификация" }, - "inactive2faFoundDesc": { - "message": "В трезора ви има $COUNT$ уебсайт(а), които е възможно да не са настроени за вписване чрез двустепенно удостоверяване (според сайта 2fa.directory). За да защитите тези регистрации, трябва да настроите двустепенното удостоверяване.", + "inactive2faFoundReportDesc": { + "message": "Във Вашия $VAULT$ има $COUNT$ уебсайт(а), които е възможно да не са настроени за вписване чрез двустепенно удостоверяване (според сайта 2fa.directory). За да защитите тези регистрации, трябва да настроите двустепенното удостоверяване.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Открити са разкрити пароли" }, - "exposedPasswordsFoundDesc": { - "message": "Открити са $COUNT$ записа в трезора ви, които са с разкрити пароли. Трябва да смените тези пароли.", + "exposedPasswordsFoundReportDesc": { + "message": "Във Вашия $VAULT$ са открити $COUNT$ елемента с пароли, които са открити в известни случаи на изтичане на данни. Препоръчително е да смените тези пароли.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Открити са слаби пароли" }, - "weakPasswordsFoundDesc": { - "message": "В трезора ви има $COUNT$записи със слаби пароли. Сменете паролите с надеждни.", + "weakPasswordsFoundReportDesc": { + "message": "Във Вашия $VAULT$ има $COUNT$ елемент(а) със слаби пароли. Препоръчително е да ги смените с по-надеждни.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Отрити са повтарящи се пароли" }, - "reusedPasswordsFoundDesc": { - "message": "В трезора ви има $COUNT$ повтарящи се пароли. Задължително ги сменете с други, уникални пароли.", + "reusedPasswordsFoundReportDesc": { + "message": "Във Вашия $VAULT$ има $COUNT$ повтарящи се пароли. Препоръчително е да ги смените, така че всяка да е уникална.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Дайте достъп до колекциите, като ги добавите към тази група." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "Можете да свързвате само колекции, които имате право да управлявате." + }, "accessAllCollectionsDesc": { "message": "Дайте достъп до всички текущи и бъдещи колекции." }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Изберете елемент от колекцията" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Управление на плащанията от Портала за доставчици" } } diff --git a/apps/web/src/locales/bn/messages.json b/apps/web/src/locales/bn/messages.json index 9b60b1149b..1b21a9a04d 100644 --- a/apps/web/src/locales/bn/messages.json +++ b/apps/web/src/locales/bn/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Unsecured websites found" }, - "unsecuredWebsitesFoundDesc": { - "message": "We found $COUNT$ items in your vault with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Logins without two-step login found" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Exposed passwords found" }, - "exposedPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault that have passwords that were exposed in known data breaches. You should change them to use a new password.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Weak passwords found" }, - "weakPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault with passwords that are not strong. You should update them to use stronger passwords.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Reused passwords found" }, - "reusedPasswordsFoundDesc": { - "message": "We found $COUNT$ passwords that are being reused in your vault. You should change them to a unique value.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/bs/messages.json b/apps/web/src/locales/bs/messages.json index 3b2b0f91bf..2382a31caf 100644 --- a/apps/web/src/locales/bs/messages.json +++ b/apps/web/src/locales/bs/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Unsecured websites found" }, - "unsecuredWebsitesFoundDesc": { - "message": "We found $COUNT$ items in your vault with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Logins without two-step login found" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Exposed passwords found" }, - "exposedPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault that have passwords that were exposed in known data breaches. You should change them to use a new password.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Weak passwords found" }, - "weakPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault with passwords that are not strong. You should update them to use stronger passwords.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Reused passwords found" }, - "reusedPasswordsFoundDesc": { - "message": "We found $COUNT$ passwords that are being reused in your vault. You should change them to a unique value.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/ca/messages.json b/apps/web/src/locales/ca/messages.json index 0f958846cd..80fd0802d3 100644 --- a/apps/web/src/locales/ca/messages.json +++ b/apps/web/src/locales/ca/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "S'han trobat llocs web no segurs" }, - "unsecuredWebsitesFoundDesc": { - "message": "Hem trobat $COUNT$ elements a la vostra caixa forta amb URI no segures. Heu de canviar el seu esquema URI a https:// si el lloc web ho permet.", + "unsecuredWebsitesFoundReportDesc": { + "message": "Hem trobat $COUNT$ elements a $VAULT$ amb URI no segures. Heu de canviar el seu esquema URI a https:// si el lloc web ho permet.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "S'han trobat registres sense 2FA" }, - "inactive2faFoundDesc": { - "message": "Hem trobat $COUNT$ llocs web a la vostra caixa forta que no es poden configurar amb l'autenticació de dos factors (d'acord amb 2fa.directory). Per protegir encara més aquests comptes, haureu d'habilitar l'autenticació de dos factors.", + "inactive2faFoundReportDesc": { + "message": "Hem trobat $COUNT$ llocs web a $VAULT$ que no es poden configurar amb l'autenticació de dos factors (d'acord amb 2fa.directory). Per protegir encara més aquests comptes, haureu d'habilitar l'autenticació de dos factors.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "S'han trobat contrasenyes exposades" }, - "exposedPasswordsFoundDesc": { - "message": "Hem trobat $COUNT$ elements a la vostra caixa forta que tenen contrasenyes que van ser exposades a filtracions de dades conegudes. Heu de canviar-les amb una contrasenya nova.", + "exposedPasswordsFoundReportDesc": { + "message": "Hem trobat $COUNT$ elements a $VAULT$ que tenen contrasenyes que van ser exposades a filtracions de dades conegudes. Heu de canviar-les amb una contrasenya nova.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "S'han trobat contrasenyes poc segures" }, - "weakPasswordsFoundDesc": { - "message": "Hem trobat $COUNT$ elements a la vostra caixa forta amb contrasenyes que no són fortes. Heu d'actualitzar-les i utilitzar contrasenyes més fortes.", + "weakPasswordsFoundReportDesc": { + "message": "Hem trobat $COUNT$ elements a $VAULT$ amb contrasenyes que no són fortes. Heu d'actualitzar-les i utilitzar contrasenyes més fortes.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "S'han trobat contrasenyes reutilitzades" }, - "reusedPasswordsFoundDesc": { - "message": "Hem trobat $COUNT$ contrasenyes que s'estan reutilitzant a la vostra caixa forta. Heu de canviar-les a un valor únic.", + "reusedPasswordsFoundReportDesc": { + "message": "Hem trobat $COUNT$ contrasenyes que s'estan reutilitzant a $VAULT$. Heu de canviar-les a un valor únic.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Doneu accés a les col·leccions afegint-les a aquest grup." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "Només podeu assignar col·leccions que gestioneu." + }, "accessAllCollectionsDesc": { "message": "Doneu accés a totes les col·leccions actuals i futures." }, @@ -7776,7 +7799,7 @@ "description": "Action to view the details of a machine account." }, "deleteMachineAccountDialogMessage": { - "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "message": "La supressió del compte de màquina $MACHINE_ACCOUNT$ és permanent i irreversible.", "placeholders": { "machine_account": { "content": "$1", @@ -7785,10 +7808,10 @@ } }, "deleteMachineAccountsDialogMessage": { - "message": "Deleting machine accounts is permanent and irreversible." + "message": "La supressió de comptes de màquina és permanent i irreversible." }, "deleteMachineAccountsConfirmMessage": { - "message": "Delete $COUNT$ machine accounts", + "message": "Suprimeix $COUNT$ comptes de màquina", "placeholders": { "count": { "content": "$1", @@ -7797,60 +7820,60 @@ } }, "deleteMachineAccountToast": { - "message": "Machine account deleted" + "message": "S'ha suprimit el compte de màquina" }, "deleteMachineAccountsToast": { - "message": "Machine accounts deleted" + "message": "S'han suprimit els comptes de màquina" }, "searchMachineAccounts": { - "message": "Search machine accounts", + "message": "Cerca els comptes de màquina", "description": "Placeholder text for searching machine accounts." }, "editMachineAccount": { - "message": "Edit machine account", + "message": "Edita el compte de màquina", "description": "Title for editing a machine account." }, "machineAccountName": { - "message": "Machine account name", + "message": "Nom del compte de màquina", "description": "Label for the name of a machine account" }, "machineAccountCreated": { - "message": "Machine account created", + "message": "S'ha creat el compte de màquina", "description": "Notifies that a new machine account has been created" }, "machineAccountUpdated": { - "message": "Machine account updated", + "message": "S'ha actualitzat el compte de màquina", "description": "Notifies that a machine account has been updated" }, "projectMachineAccountsDescription": { - "message": "Grant machine accounts access to this project." + "message": "Dona accés a aquest projecte als comptes de màquina." }, "projectMachineAccountsSelectHint": { - "message": "Type or select machine accounts" + "message": "Escriu o selecciona comptes de màquina" }, "projectEmptyMachineAccountAccessPolicies": { - "message": "Add machine accounts to grant access" + "message": "Afig comptes de màquina per concedir accés" }, "machineAccountPeopleDescription": { - "message": "Grant groups or people access to this machine account." + "message": "Concediu accés a aquest compte de màquina a grups o persones." }, "machineAccountProjectsDescription": { - "message": "Assign projects to this machine account. " + "message": "Assigna projectes a aquest compte de màquina. " }, "createMachineAccount": { - "message": "Create a machine account" + "message": "Crea un compte de màquina" }, "maPeopleWarningMessage": { - "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + "message": "La supressió de persones d'un compte de màquina no suprimeix els testimonis d'accés que han creat. Per a les millors pràctiques de seguretat, es recomana revocar els testimonis d'accés creats per persones suprimides d'un compte de màquina." }, "smAccessRemovalWarningMaTitle": { - "message": "Remove access to this machine account" + "message": "Suprimeix l'accés a aquest compte de màquina" }, "smAccessRemovalWarningMaMessage": { - "message": "This action will remove your access to the machine account." + "message": "Aquesta acció suprimirà l'accés al compte de màquina." }, "machineAccountsIncluded": { - "message": "$COUNT$ machine accounts included", + "message": "$COUNT$ comptes de màquina inclosos", "placeholders": { "count": { "content": "$1", @@ -7859,7 +7882,7 @@ } }, "additionalMachineAccountCost": { - "message": "$COST$ per month for additional machine accounts", + "message": "$COST$ al mes per a comptes de màquina addicionals", "placeholders": { "cost": { "content": "$1", @@ -7868,10 +7891,10 @@ } }, "additionalMachineAccounts": { - "message": "Additional machine accounts" + "message": "Comptes de màquina addicionals" }, "includedMachineAccounts": { - "message": "Your plan comes with $COUNT$ machine accounts.", + "message": "El vostre pla inclou $COUNT$ comptes de màquina.", "placeholders": { "count": { "content": "$1", @@ -7880,7 +7903,7 @@ } }, "addAdditionalMachineAccounts": { - "message": "You can add additional machine accounts for $COST$ per month.", + "message": "Podeu afegir comptes de màquina addicionals per $COST$ al mes.", "placeholders": { "cost": { "content": "$1", @@ -7889,48 +7912,48 @@ } }, "limitMachineAccounts": { - "message": "Limit machine accounts (optional)" + "message": "Limita els comptes de màquina (opcional)" }, "limitMachineAccountsDesc": { - "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + "message": "Estableix un límit pels comptes de màquina. Una vegada assolit aquest límit, no en podreu crear de nous." }, "machineAccountLimit": { - "message": "Machine account limit (optional)" + "message": "Límit del compte de màquina (opcional)" }, "maxMachineAccountCost": { - "message": "Max potential machine account cost" + "message": "Cost potencial màxim del compte de màquina" }, "machineAccountAccessUpdated": { - "message": "Machine account access updated" + "message": "Accés al compte de màquina actualitzat" }, "restrictedGroupAccessDesc": { - "message": "You cannot add yourself to a group." + "message": "No podeu afegir-vos vosaltres mateix a un grup." }, "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "message": "Avís: el 2 de maig de 2024, els elements de l'organització no assignats deixaran de ser visibles a la vista \"Totes les caixes fortes\" en tots els dispositius i només es podran accedir des de la Consola d'administració. Assigna aquests elements a una col·lecció des de la Consola d'administració per fer-los visibles." }, "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + "message": "Avís: els elements de l'organització no assignats ja no són visibles a la visualització de Totes les caixes fortes en tots els dispositius i ara només es poden accedir des de la Consola d'administració." }, "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + "message": "Avís: el 16 de maig de 2024, els elements de l'organització no assignats deixaran de ser visibles a la visualització de Totes les caixes fortes en tots els dispositius i només es podran accedir des de la Consola d'administració." }, "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", + "message": "Assigna aquests elements a una col·lecció de", "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.", + "message": "per fer-los visibles.", "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." }, "deleteProvider": { - "message": "Delete provider" + "message": "Suprimeix proveïdor" }, "deleteProviderConfirmation": { - "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + "message": "La supressió d'un proveïdor és permanent i irreversible. Introduïu la contrasenya mestra per confirmar la supressió del proveïdor i totes les dades associades." }, "deleteProviderName": { - "message": "Cannot delete $ID$", + "message": "No es pot suprimir $ID$", "placeholders": { "id": { "content": "$1", @@ -7939,7 +7962,7 @@ } }, "deleteProviderWarningDesc": { - "message": "You must unlink all clients before you can delete $ID$", + "message": "Heu de desenllaçar tots els clients abans de poder suprimir $ID$", "placeholders": { "id": { "content": "$1", @@ -7948,106 +7971,109 @@ } }, "providerDeleted": { - "message": "Provider deleted" + "message": "S'ha suprimit el proveïdor" }, "providerDeletedDesc": { - "message": "The Provider and all associated data has been deleted." + "message": "S'han suprimit el proveïdor i totes les dades associades." }, "deleteProviderRecoverConfirmDesc": { - "message": "You have requested to delete this Provider. Use the button below to confirm." + "message": "Heu sol·licitat suprimir aquest proveïdor. Utilitzeu el botó següent per confirmar." }, "deleteProviderWarning": { - "message": "Deleting your provider is permanent. It cannot be undone." + "message": "La supressió del proveïdor és permanent. No es pot desfer." }, "errorAssigningTargetCollection": { - "message": "Error assigning target collection." + "message": "S'ha produït un error en assignar la col·lecció de destinació." }, "errorAssigningTargetFolder": { - "message": "Error assigning target folder." + "message": "S'ha produït un error en assignar la carpeta de destinació." }, "integrationsAndSdks": { - "message": "Integrations & SDKs", + "message": "Integracions i SDK", "description": "The title for the section that deals with integrations and SDKs." }, "integrations": { - "message": "Integrations" + "message": "Integracions" }, "integrationsDesc": { - "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + "message": "Sincronitza automàticament els secrets de Bitwarden Gestor de secrets amb un servei de tercers." }, "sdks": { - "message": "SDKs" + "message": "SDK" }, "sdksDesc": { - "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + "message": "Utilitzeu Bitwarden gestor de secrets SDK en els següents llenguatges de programació per crear les vostres aplicacions." }, "setUpGithubActions": { - "message": "Set up Github Actions" + "message": "Configura les accions de Github" }, "setUpGitlabCICD": { - "message": "Set up GitLab CI/CD" + "message": "Configura GitLab CI/CD" }, "setUpAnsible": { - "message": "Set up Ansible" + "message": "Configura Ansible" }, "cSharpSDKRepo": { - "message": "View C# repository" + "message": "Veure el repositori C#" }, "cPlusPlusSDKRepo": { - "message": "View C++ repository" + "message": "Veure el repositori C++" }, "jsWebAssemblySDKRepo": { - "message": "View JS WebAssembly repository" + "message": "Veure el repositori JS WebAssembly" }, "javaSDKRepo": { - "message": "View Java repository" + "message": "Veure el repositori de Java" }, "pythonSDKRepo": { - "message": "View Python repository" + "message": "Veure el repositori de Python" }, "phpSDKRepo": { - "message": "View php repository" + "message": "Veure el repositori php" }, "rubySDKRepo": { - "message": "View Ruby repository" + "message": "Veure el repositori Ruby" }, "goSDKRepo": { - "message": "View Go repository" + "message": "Veure el repositori Go" }, "createNewClientToManageAsProvider": { - "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + "message": "Creeu una nova organització client per gestionar com a proveïdor. Els seients addicionals es reflectiran en el proper cicle de facturació." }, "selectAPlan": { - "message": "Select a plan" + "message": "Seleccioneu un pla" }, "thirtyFivePercentDiscount": { - "message": "35% Discount" + "message": "35% de descompte" }, "monthPerMember": { - "message": "month per member" + "message": "mes per membre" }, "seats": { - "message": "Seats" + "message": "Seients" }, "addOrganization": { - "message": "Add organization" + "message": "Afig organització" }, "createdNewClient": { - "message": "Successfully created new client" + "message": "S'ha creat un nou client correctament" }, "noAccess": { - "message": "No access" + "message": "Sense accés" }, "collectionAdminConsoleManaged": { - "message": "This collection is only accessible from the admin console" + "message": "Aquesta col·lecció només és accessible des de la consola d'administració" }, "organizationOptionsMenu": { - "message": "Toggle Organization Menu" + "message": "Canvia el menú d'organització" }, "vaultItemSelect": { - "message": "Select vault item" + "message": "Seleccioneu element de la caixa forta" }, "collectionItemSelect": { - "message": "Select collection item" + "message": "Seleccioneu element de la col·lecció" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Gestioneu la facturació des del portal de proveïdors" } } diff --git a/apps/web/src/locales/cs/messages.json b/apps/web/src/locales/cs/messages.json index 72e621b1a5..32f1f68d45 100644 --- a/apps/web/src/locales/cs/messages.json +++ b/apps/web/src/locales/cs/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Nalezena nezabezpečená webová stránka" }, - "unsecuredWebsitesFoundDesc": { - "message": "Nalezli jsme $COUNT$ položek ve Vašem trezoru, které používají nezabezpečené URI. Schémata URI by měla být změněna na https://, pokud to web umožňuje.", + "unsecuredWebsitesFoundReportDesc": { + "message": "Nalezli jsme $COUNT$ položek ve Vašem trezoru $VAULT$, které používají nezabezpečené URI. Schémata URI by měla být změněna na https://, pokud to web umožňuje.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Nalezena přihlášení bez dvoufázového ověření" }, - "inactive2faFoundDesc": { - "message": "Nalezli jsme $COUNT$ webových stránek ve Vašem trezoru, které zřejmě nejsou nakonfigurovány pro použití dvoufaktorového přihlášení. Pro lepší ochranu Vašich účtů byste měli dvoufaktorové přihlášení povolit.", + "inactive2faFoundReportDesc": { + "message": "Nalezli jsme $COUNT$ webových stránek ve Vašem trezoru $VAULT$, které zřejmě nejsou nakonfigurovány pro použití dvoufaktorového přihlášení. Pro lepší ochranu Vašich účtů byste měli dvoufaktorové přihlášení povolit.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Nalezena odhalená hesla" }, - "exposedPasswordsFoundDesc": { - "message": "Nalezli jsme $COUNT$ položek ve Vašem trezoru, jejichž hesla byla odhalena během známých úniků dat. Měli byste u nich použít nové heslo.", + "exposedPasswordsFoundReportDesc": { + "message": "Našli jsme $COUNT$ položek ve Vašem trezoru $VAULT$, která mají hesla, které byly odhaleny ve známých únicích dat. Měli byste je změnit tak, aby používaly nové heslo.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Nalezena slabá hesla" }, - "weakPasswordsFoundDesc": { - "message": "Našli jsme $COUNT$ položek ve Vašem trezoru se slabým heslem. Měli byste je aktualizovat a použit silnější hesla.", + "weakPasswordsFoundReportDesc": { + "message": "Našli jsme $COUNT$ položek ve Vašem trezoru $VAULT$ se slabým heslem. Měli byste je aktualizovat a použit silnější hesla.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Nalezena opakovaně použitá hesla" }, - "reusedPasswordsFoundDesc": { - "message": "Nalezli jsme $COUNT$ opakovaně použitých hesel ve Vašem trezoru. Doporučujeme je změnit, aby byly unikátní.", + "reusedPasswordsFoundReportDesc": { + "message": "Nalezli jsme $COUNT$ opakovaně použitých hesel ve Vašem trezoru $VAULT$. Doporučujeme je změnit, aby byly unikátní.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Udělí členům přístup ke kolekcím přidáním do této skupiny." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "Můžete přiřadit jen Vámi spravované kolekce." + }, "accessAllCollectionsDesc": { "message": "Udělí přístup ke všem aktuálním i budoucím kolekcím." }, @@ -7910,10 +7933,10 @@ "message": "Upozornění: Dne 2. května 2024 již nebudou nepřiřazené položky organizace viditelné v zobrazení Všechny trezory ve všech zařízeních 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é." }, "unassignedItemsBannerNotice": { - "message": "Upozornění: Nepřiřazené položky organizace již nejsou viditelné ve vašem zobrazení všech trezorů napříč zařízeními a jsou nyní přístupné pouze v konzoli správce." + "message": "Upozornění: Nepřiřazené položky organizace již nejsou viditelné ve Vašem zobrazení všech trezorů napříč zařízeními a jsou nyní přístupné pouze v konzoli správce." }, "unassignedItemsBannerSelfHostNotice": { - "message": "Upozornění: 16. květba 2024 již nebudou nepřiřazené položky organizace viditelné ve vašem zobrazení všech trezorů napříč zařízeními a budou přístupné pouze v konzoli správce." + "message": "Upozornění: 16. květba 2024 již nebudou nepřiřazené položky organizace viditelné ve Vašem zobrazení všech trezorů napříč zařízeními a budou přístupné pouze v konzoli správce." }, "unassignedItemsBannerCTAPartOne": { "message": "Přiřadit tyto položky ke kolekci z", @@ -8039,7 +8062,7 @@ "message": "Žádný přístup" }, "collectionAdminConsoleManaged": { - "message": "Tato kolekce je přístupná pouze z konzole administrátora" + "message": "Tato kolekce je přístupná pouze z konzole správce" }, "organizationOptionsMenu": { "message": "Přepnout menu organizace" @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Vybrat položku kolekce" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Spravovat fakturaci z portálu poskytovatele" } } diff --git a/apps/web/src/locales/cy/messages.json b/apps/web/src/locales/cy/messages.json index 961b3c93a3..09cc963d67 100644 --- a/apps/web/src/locales/cy/messages.json +++ b/apps/web/src/locales/cy/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Unsecured websites found" }, - "unsecuredWebsitesFoundDesc": { - "message": "We found $COUNT$ items in your vault with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Logins without two-step login found" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Exposed passwords found" }, - "exposedPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault that have passwords that were exposed in known data breaches. You should change them to use a new password.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Weak passwords found" }, - "weakPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault with passwords that are not strong. You should update them to use stronger passwords.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Reused passwords found" }, - "reusedPasswordsFoundDesc": { - "message": "We found $COUNT$ passwords that are being reused in your vault. You should change them to a unique value.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/da/messages.json b/apps/web/src/locales/da/messages.json index 4c8cbf108a..30ba2d09e4 100644 --- a/apps/web/src/locales/da/messages.json +++ b/apps/web/src/locales/da/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Ikke-sikrede websteder fundet" }, - "unsecuredWebsitesFoundDesc": { - "message": "$COUNT$ emner fundet i din boks med ikke-sikrede URI'er. Deres URI-protokoller bør ændres til https://, såfremt webstedet tillader det.", + "unsecuredWebsitesFoundReportDesc": { + "message": "$COUNT$ emner fundet i $VAULT$ med ikke-sikreded URI'er. Hvis tilladt af webstedet, bør deres URI-protokol ændres til https://.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Logins uden totrins-login fundet" }, - "inactive2faFoundDesc": { - "message": "$COUNT$ websted(er) fundet i boksen, som muligvis ikke er opsat med totrins-login (jf. 2fa.directory). For yderligere at beskytte disse konti, bør totrins-login opsættes.", + "inactive2faFoundReportDesc": { + "message": "$COUNT$ websted(er) muligvis uden opsat med tofaktorgodkendelse (iflg. 2fa.directory) fundet i $VAULT$. For yderligere beskyttelse af disse konti bør tofaktorgodkendelse opsættes.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Kompromitterede adgangskoder fundet" }, - "exposedPasswordsFoundDesc": { - "message": "$COUNT$ emner fundet i din boks med adgangskoder kompromitteret i kendte datalæk. En ny adgangskode bør opsættes for disse emner.", + "exposedPasswordsFoundReportDesc": { + "message": "$COUNT$ emner fundet i $VAULT$ med adgangskoder kompromitteret via kendte datalæk. Deres adgangskoder bør skiftes.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Svage adgangskoder fundet" }, - "weakPasswordsFoundDesc": { - "message": "$COUNT$ emner fundet i din boks med adgangskoder, som ikke er stærke. Disse bør opdateres med stærkere adgangskoder.", + "weakPasswordsFoundReportDesc": { + "message": "$COUNT$ emner med svage adgangskoder fundet i $VAULT$. Disse bør skiftes til stærke adgangskoder.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Genbrugte adgangskoder fundet" }, - "reusedPasswordsFoundDesc": { - "message": "Vi fandt $COUNT$ adgangskoder, der genbruges i din boks. Du bør ændre dem til unikke koder.", + "reusedPasswordsFoundReportDesc": { + "message": "$COUNT$ adgangskoder, som genbruges, fundet i $VAULT$. Disse bør skiftes til unikke adgangskoder.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Tildel adgang til samlinger ved at føje dem til denne gruppe." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "Der kan kun tildeles samlinger, man selv håndterer." + }, "accessAllCollectionsDesc": { "message": "Tildel adgang til alle nuværende og fremtidige samlinger." }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Vælg samlingsemne" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Håndter fakturering via udbyderportalen" } } diff --git a/apps/web/src/locales/de/messages.json b/apps/web/src/locales/de/messages.json index 1c7dd34329..1f520f546d 100644 --- a/apps/web/src/locales/de/messages.json +++ b/apps/web/src/locales/de/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Ungesicherte Websites gefunden" }, - "unsecuredWebsitesFoundDesc": { - "message": "Wir haben $COUNT$ Einträge in deinem Tresor mit ungesicherten URIs gefunden. Du solltest ihr URI-Präfix auf https:// ändern, wenn die Website dies zulässt.", + "unsecuredWebsitesFoundReportDesc": { + "message": "Wir haben $COUNT$ Einträge in deinem/deinen $VAULT$ mit ungeschützen URIs gefunden. Du solltest deren URI-Präfix in https:// ändern, wenn die Webseite dies zulässt.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Zugangsdaten ohne 2FA gefunden" }, - "inactive2faFoundDesc": { - "message": "Wir haben $COUNT$ Webseite(n) in Ihrem Tresor gefunden, die eine Zwei-Faktor Authentifizierung anbieten (laut 2fa.directory), aber bei denen diese Funktion möglicherweise nicht aktiviert ist. Um diese Accounts abzusichern, sollten Sie die Zwei-Faktor Authentifizierung aktivieren.", + "inactive2faFoundReportDesc": { + "message": "Wir haben $COUNT$ Website(s) in deinem/deinen $VAULT$ gefunden, die eine Zwei-Faktor-Authentifizierung anbieten (laut 2fa.directory), aber bei denen diese Funktion möglicherweise nicht aktiviert ist. Um diese Konten weiter abzusichern, solltest du die Zwei-Faktor-Authentifizierung aktivieren.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Es wurden kompromittierte Passwörter gefunden" }, - "exposedPasswordsFoundDesc": { - "message": "Wir haben $COUNT$ Einträge in deinem Tresor gefunden, die in bekannten Passwortdiebstählen veröffentlicht wurden. Du solltest diese Passwörter so schnell wie möglich ändern.", + "exposedPasswordsFoundReportDesc": { + "message": "Wir haben $COUNT$ Einträge in deinem/deinen $VAULT$ gefunden, die Passwörter enthalten, die in bekannten Passwortdiebstahl-Datenbanken veröffentlicht wurden. Du solltest diese ändern und ein neues Passwort verwenden.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Schwache Passwörter gefunden" }, - "weakPasswordsFoundDesc": { - "message": "Wir haben $COUNT$ Einträge mit schwachen Passwörtern in deinem Tresor gefunden. Du solltest diese aktualisieren und ein sicheres Passwort verwenden.", + "weakPasswordsFoundReportDesc": { + "message": "Wir haben $COUNT$ Einträge in deinem/deinen $VAULT$ mit unsicheren Passwörtern gefunden. Du solltest diese ändern und sicherere Passwörter verwenden.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Wiederverwendete Passwörter gefunden" }, - "reusedPasswordsFoundDesc": { - "message": "Wir haben $COUNT$ Passwörter in deinem Tresor gefunden, die mehrfach benutzt wurden. Du solltest diese ändern und jedes Passwort nur ein einziges Mal benutzen.", + "reusedPasswordsFoundReportDesc": { + "message": "Wir haben $COUNT$ Passwörter in deinem/deinen $VAULT$ gefunden, die mehrfach verwendet werden. Du solltest diese ändern und einzigartige Passwörter verwenden.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Gewähre Zugriff auf Sammlungen, indem du diese zu dieser Gruppe hinzufügst." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Gewähre Zugriff auf alle aktuellen und zukünftigen Sammlungen." }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Sammlungseintrag auswählen" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Rechnungen über das Anbieter-Portal verwalten" } } diff --git a/apps/web/src/locales/el/messages.json b/apps/web/src/locales/el/messages.json index ed374326fe..b57f8728e5 100644 --- a/apps/web/src/locales/el/messages.json +++ b/apps/web/src/locales/el/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Βρέθηκαν μη ασφαλής ιστοσελίδες" }, - "unsecuredWebsitesFoundDesc": { - "message": "Βρήκαμε $COUNT$ στοιχεία στο vault σας, με μη ασφαλές URI. Θα πρέπει να αλλάξετε το URI σε https:/ εφόσον το επιτρέπει η ιστοσελίδα.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Βρέθηκαν Συνδέσεις Χωρίς 2FA" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Βρέθηκαν Εκτεθειμένοι Κωδικοί" }, - "exposedPasswordsFoundDesc": { - "message": "Βρήκαμε $COUNT$ στοιχεία στο vault σας, που έχουν εκτεθειμένους κωδικούς σε γνωστές παραβιάσεις δεδομένων. Θα πρέπει να τους αλλάξετε προκειμένου να χρησιμοποιήσετε έναν νέο κωδικό.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Βρέθηκαν Αδύναμοι Κωδικοί" }, - "weakPasswordsFoundDesc": { - "message": "Βρήκαμε $COUNT$ στοιχεία στο vault σας, με κωδικούς που δεν είναι ισχυροί. Θα πρέπει να τους ενημερώσετε και να χρησιμοποιήσετε ισχυρότερους κωδικούς.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Βρέθηκαν Επανα-χρησιμοποιούμενοι Κωδικοί" }, - "reusedPasswordsFoundDesc": { - "message": "Βρήκαμε $COUNT$ κωδικούς που επανα-χρησιμοποιούνται στο vault σας. Θα πρέπει να αλλάξετε τον κάθε ένα, σε κάτι μοναδικό.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/en_GB/messages.json b/apps/web/src/locales/en_GB/messages.json index 3b0a99715f..6910230fc4 100644 --- a/apps/web/src/locales/en_GB/messages.json +++ b/apps/web/src/locales/en_GB/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Unsecured websites found" }, - "unsecuredWebsitesFoundDesc": { - "message": "We found $COUNT$ items in your vault with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Logins without two-step login found" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Exposed passwords found" }, - "exposedPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault that have passwords that were exposed in known data breaches. You should change them to use a new password.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Weak passwords found" }, - "weakPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault with passwords that are not strong. You should update them to use stronger passwords.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Reused passwords found" }, - "reusedPasswordsFoundDesc": { - "message": "We found $COUNT$ passwords that are being reused in your vault. You should change them to a unique value.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/en_IN/messages.json b/apps/web/src/locales/en_IN/messages.json index 1868130a1c..14d1b49c83 100644 --- a/apps/web/src/locales/en_IN/messages.json +++ b/apps/web/src/locales/en_IN/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Unsecured websites found" }, - "unsecuredWebsitesFoundDesc": { - "message": "We found $COUNT$ items in your vault with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Logins without 2FA found" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Exposed passwords found" }, - "exposedPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault that have passwords that were exposed in known data breaches. You should change them to use a new password.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Weak passwords found" }, - "weakPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault with passwords that are not strong. You should update them to use stronger passwords.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Reused passwords found" }, - "reusedPasswordsFoundDesc": { - "message": "We found $COUNT$ passwords that are being reused in your vault. You should change them to a unique value.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/eo/messages.json b/apps/web/src/locales/eo/messages.json index 88509a8299..51374f8432 100644 --- a/apps/web/src/locales/eo/messages.json +++ b/apps/web/src/locales/eo/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Trovitaj Nesekurigitaj Retejoj" }, - "unsecuredWebsitesFoundDesc": { - "message": "Ni trovis $COUNT$ erojn en via trezorejo kun nesekurigitaj URI-oj. Vi devas ŝanĝi ilian URI-skemon al https: // se la retejo permesas ĝin.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Ensalutoj Sen 2FA Trovitaj" }, - "inactive2faFoundDesc": { - "message": "Ni trovis $COUNT$ retejon (j) en via trezorejo, kiu eble ne estas agordita kun dufakta aŭtentokontrolo (laŭ 2fa.directory). Por plue protekti ĉi tiujn kontojn, vi devas ebligi dufaktoran aŭtentikigon.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Trovitaj Pasvortoj Trovitaj" }, - "exposedPasswordsFoundDesc": { - "message": "Ni trovis $COUNT$ erojn en via trezorejo, kiuj havas pasvortojn elmontritajn en konataj rompo de datumoj. Vi devas ŝanĝi ilin por uzi novan pasvorton.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Malfortaj Pasvortoj Trovitaj" }, - "weakPasswordsFoundDesc": { - "message": "Ni trovis $COUNT$ erojn en via trezorejo kun pasvortoj ne fortaj. Vi devas ĝisdatigi ilin por uzi pli fortajn pasvortojn.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Reuzitaj Pasvortoj Trovitaj" }, - "reusedPasswordsFoundDesc": { - "message": "Ni trovis $COUNT$ pasvortojn reuzatajn en via trezorejo. Vi devas ŝanĝi ilin al unika valoro.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/es/messages.json b/apps/web/src/locales/es/messages.json index e851539df1..d49dbb6000 100644 --- a/apps/web/src/locales/es/messages.json +++ b/apps/web/src/locales/es/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Sitios web no seguros encontrados" }, - "unsecuredWebsitesFoundDesc": { - "message": "Hemos encontrado $COUNT$ elemento(s) en su caja fuerte con URIs no seguras. Si el sitio web lo permite debe cambiar su esquema URI a https://.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Inicios de sesión sin 2FA encontrados" }, - "inactive2faFoundDesc": { - "message": "Hemos encontrado $COUNT$ sitios web en tu bóveda que pueden no estar configurados con inicio de sesión en dos pasos (de acuerdo a 2fa. irectory). Para proteger aún más estas cuentas, debe configurar el inicio de sesión en dos pasos.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Contraseñas comprometidas encontradas" }, - "exposedPasswordsFoundDesc": { - "message": "Hemos encontrado $COUNT$ elementos en su caja fuerte que tienen contraseñas que fueron comprometidas en filtraciones de datos conocidas. Debe cambiarlos para utilizar una contraseña nueva.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Contraseñas débiles encontradas" }, - "weakPasswordsFoundDesc": { - "message": "Hemos encontrado $COUNT$ elemento(s) en su caja fuerte con contraseñas que no son fuertes. Se deben actualizar para usar contraseñas más fuertes.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Contraseñas reutilizadas encontradas" }, - "reusedPasswordsFoundDesc": { - "message": "Hemos encontrado $COUNT$ contraseña(s) que están siendo reutilizadas en su caja fuerte. Debe cambiarlas a un valor único.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Conceder acceso a las colecciones añadiéndolas a este grupo." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Otorgar acceso a todas las colecciones actuales y futuras." }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/et/messages.json b/apps/web/src/locales/et/messages.json index 1cef83a823..ef4d3f8ad3 100644 --- a/apps/web/src/locales/et/messages.json +++ b/apps/web/src/locales/et/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Leiti ebaturvalisi veebilehti" }, - "unsecuredWebsitesFoundDesc": { - "message": "Leidsime hoidlast $COUNT$ ebaturvalist veebilehte.\nKui võimalik, soovitame nende veebilehtede alguse tungivalt https:// -ks muuta. ", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Kaheastmelise kinnituseta kontod" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Avastatud on lekkinud paroole" }, - "exposedPasswordsFoundDesc": { - "message": "Leidsime sinu hoidlast $COUNT$ kirjet, millede paroolid on teadaolevate andmelekete tagajärjel avalikustatud. Soovitame tungivalt need paroolid ära vahetada.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Avastatud on nõrgad paroolid" }, - "weakPasswordsFoundDesc": { - "message": "Leidsime sinu hoidlast $COUNT$ kirjet, milledel on nõrgad paroolid. Soovitame tungivalt need paroolid tugevamate vastu välja vahetada.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Leiti korduvalt kasutatud paroole" }, - "reusedPasswordsFoundDesc": { - "message": "Leidsime sinu hoidlast $COUNT$ parooli, mis on kasutusel rohkem kui üks kord Soovitame need paroolid unikaalseteks muuta.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/eu/messages.json b/apps/web/src/locales/eu/messages.json index bf79583e58..9509214f0e 100644 --- a/apps/web/src/locales/eu/messages.json +++ b/apps/web/src/locales/eu/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Webgune ez seguruak aurkituak" }, - "unsecuredWebsitesFoundDesc": { - "message": "$COUNT$ artikulu aurkitu d(it)ugu kutxa gotorrean bermatuta ez dauden URI-ekin. Zure URI eskema https://-ra aldatu behar duzu, web guneak onartzen badu.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "2FA-rik gabeko saio hasierak aurkituak" }, - "inactive2faFoundDesc": { - "message": "$COUNT$ webgune aurkitu ditugu zure kutxa gotorrean bi urratseko autentifikazioarekin konfiguratu ezin direnak (twofactorauth.org-en arabera). Kontu horiek babesteko, bi urratseko autentifikazioa gaitu behar duzu.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Ikusgai dauden pasahitzak aurkituak" }, - "exposedPasswordsFoundDesc": { - "message": "Kutxa gotorrean $COUNT$ elementuren pasahitz daude, datu bortxaketa ezagunetan erakutsi direnak. Aldatu beharko zenituzke eta pasahitz berria erabili.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Pasahitz ahulak aurkituak" }, - "weakPasswordsFoundDesc": { - "message": "Zure kutxa gotorrean $COUNT$ elementu aurkitu ditugu pasahitz ahulekin. Eguneratu egin behar dituzu pasahitz seguruagoak erabiltzeko.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Pasahitz berrerabiliak aurkituak" }, - "reusedPasswordsFoundDesc": { - "message": "Zure kutxa gotorrean $COUNT$ elementu aurkitu ditugu pasahitz berrerabiliekin. Aldatu beharko zenituzke pasahitz bakarrak erabiltzeko.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/fa/messages.json b/apps/web/src/locales/fa/messages.json index 0621239c5e..d8f2fd947a 100644 --- a/apps/web/src/locales/fa/messages.json +++ b/apps/web/src/locales/fa/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "وب‌سایت های نا امن پیدا شد" }, - "unsecuredWebsitesFoundDesc": { - "message": "ما $COUNT$ مورد را با نشانی‌های اینترنتی نا امن در خزانه شما پیدا کردیم. اگر وب‌سایت اجازه می‌دهد، باید طرح نشانی اینترنتی آن‌ها را به //:https‌ تغییر دهید.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "ورود‌های بدون ورود دو مرحله ای یافت شد" }, - "inactive2faFoundDesc": { - "message": "ما $COUNT$ وب‌سایت را در گاوصندوق شما پیدا کردیم که ممکن است با ورود دو مرحله‌ای پیکربندی نشده باشند (بر اساس 2fa.directory). برای محافظت بیشتر از این حساب‌ها، باید ورود دو مرحله ای را تنظیم کنید.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "کلمه‌های عبور افشا شده یافت شد" }, - "exposedPasswordsFoundDesc": { - "message": "ما $COUNT$ موردی را در خزانه شما پیدا کردیم که دارای کلمه‌های عبوری هستند که در نقض‌های اطلاعاتی شناخته شده افشا شده‌اند. شما باید آن‌ها را تغییر دهید تا از یک کلمه عبور جدید استفاده کنید.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "کلمه‌های عبور ضعیف پیدا شد" }, - "weakPasswordsFoundDesc": { - "message": "ما $COUNT$ مورد با کلمه‌های عبوری که قوی نیستند در گاوصندوق شما پیدا کردیم. شما باید آن‌ها را به‌روز کنید تا از کلمه‌های عبور قوی تر استفاده کنید.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "کلمه‌های عبور مجدد استفاده شده یافت شد" }, - "reusedPasswordsFoundDesc": { - "message": "ما $COUNT$ کلمه عبور پیدا کردیم که در گاوضندوق شما دوباره استفاده می‌شود. شما باید آن‌ها را به یک مقدار منحصر به فرد تغییر دهید.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "با افزودن مجموعه‌ها به این گروه، اجازه دسترسی به مجموعه‌ها را بدهید." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "اجازه دسترسی به تمام مجموعه‌های فعلی و آینده." }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/fi/messages.json b/apps/web/src/locales/fi/messages.json index 4cc682caa2..17b629efd5 100644 --- a/apps/web/src/locales/fi/messages.json +++ b/apps/web/src/locales/fi/messages.json @@ -648,10 +648,10 @@ "message": "Pidä tämä ikkuna avoinna ja seuraa selaimesi opasteita." }, "errorCreatingPasskey": { - "message": "Virhe suojausavaimen luonnissa" + "message": "Virhe luotaessa suojausavainta" }, "errorCreatingPasskeyInfo": { - "message": "Suojausavaimesi luonnissa kohdattiin ongelma." + "message": "Suojausavaintasi luotaessa kohdattiin ongelma." }, "passkeySuccessfullyCreated": { "message": "Suojausavain on luotu" @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Suojaamattomia verkkosivustoja löytyi" }, - "unsecuredWebsitesFoundDesc": { - "message": "Löysimme holvistasi $COUNT$ kohdetta suojaamattomilla URI-osoitteilla. Sinun tulisi muuttaa niiden URI suojattuun \"https://\" -muotoon, jos sivusto tukee sitä.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Löytyi kirjautumistietoja, joille ei ole määritetty kaksivaiheista kirjautumista" }, - "inactive2faFoundDesc": { - "message": "Löysimme holvistasi $COUNT$ sivustoa, joita ei ehkä ole määritetty käyttämään kaksivaiheista kirjautumista (2fa.directory-sivuston mukaan). Suojataksesi nämä tilit paremmin, sinun tulisi määrittää niille kaksivaiheinen kirjautuminen.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Paljastuneita salasanoja löytyi" }, - "exposedPasswordsFoundDesc": { - "message": "Löysimme holvistasi $COUNT$ kohdetta, jotka sisältävät tunnetuissa tietovuodoissa paljastuneita salasanoja. Näiden palveluiden salasanat tulisi vaihtaa.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Heikkoja salasanoja löytyi" }, - "weakPasswordsFoundDesc": { - "message": "Löysimme holvistasi $COUNT$ kohdetta, joiden salasanat eivät ole vahvoja. Nämä tulisi korvata vahvemmilla salasanoilla.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Toistuvia salasanoja löytyi" }, - "reusedPasswordsFoundDesc": { - "message": "Löysimme holvistasi $COUNT$ toistuvasti käytettyä salasanaa. Ne tulisi vaihtaa yksilöllisiksi.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Myönnä käyttöoikeudet kokoelmiin lisäämällä heidät tähän ryhmään." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Myönnä käyttöoikeudet kaikkiin nykyisiin ja tuleviin kokoelmiin" }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Valitse kokoelman kohde" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/fil/messages.json b/apps/web/src/locales/fil/messages.json index eb3094e043..aed2665fcc 100644 --- a/apps/web/src/locales/fil/messages.json +++ b/apps/web/src/locales/fil/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "May nakitang mga website na hindi ligtas" }, - "unsecuredWebsitesFoundDesc": { - "message": "May $COUNT$ item sa vault mo na gumagamit ng hindi ligtas na URI. Dapat gawing https:// ang URI scheme nito kung maa-access mo ang website gamit nito.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "May mga login na nakasara ang dalawang-hakbang na pag-log in" }, - "inactive2faFoundDesc": { - "message": "May $COUNT$ website sa vault mo na maaaring hindi pa gumagamit ng dalawang-hakbang na pag-log in (ayon sa 2fa.directory). I-set up ang dalawang-hakbang na pag-log in sa mga account na ito para sa karagdagang proteksyon.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "May nakitang mga nakompromisong password" }, - "exposedPasswordsFoundDesc": { - "message": "May $COUNT$ item sa vault mo na may password na nakompromiso sa mga naitalang data breach. Kailangan mo silang baguhin.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "May mga mahinang password" }, - "weakPasswordsFoundDesc": { - "message": "May $COUNT$ item sa vault mo na may mahinang password. Dapat mo silang palakasin.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "May mga naulit na password" }, - "reusedPasswordsFoundDesc": { - "message": "May $COUNT$ item sa vault mo na may password na pinapaulit-ulit. Kailangan mo silang gawing unique.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Magbigay ng access sa mga koleksyon sa pamamagitan ng pagdaragdag ng mga ito sa grupong ito." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Magbigay ng access sa lahat ng kasalukuyan at hinaharap na mga koleksyon." }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/fr/messages.json b/apps/web/src/locales/fr/messages.json index ffb1fe436b..bd0144d7a5 100644 --- a/apps/web/src/locales/fr/messages.json +++ b/apps/web/src/locales/fr/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Sites web non sécurisés trouvés" }, - "unsecuredWebsitesFoundDesc": { - "message": "Nous avons trouvé $COUNT$ éléments dans votre coffre avec des URI non sécurisés. Vous devriez remplacer leur schéma URI par https:// si le site Web le permet.", + "unsecuredWebsitesFoundReportDesc": { + "message": "Nous avons $COUNT$ éléments dans $VAULT$ avec des URI non sécurisées. Vous devriez modifier leur schéma URI en https:// si le site le permet.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Identifiants sans authentification à deux facteurs trouvés" }, - "inactive2faFoundDesc": { - "message": "Nous avons trouvé $COUNT$ site(s) web dans votre coffre peut-être non configuré(s) avec l'authentification à deux facteurs (selon 2fa.directory). Pour protéger davantage ces comptes, vous devriez mettre en place l'authentification à deux facteurs.", + "inactive2faFoundReportDesc": { + "message": "Nous avons trouvé $COUNT$ site(s) web dans $VAULT$ qui ne sont peut-être pas configurés avec une connexion en deux étapes (selon le 2fa.directory). Pour protéger davantage ces comptes, vous devez configurer une connexion en deux étapes.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Mots de passe exposés trouvés" }, - "exposedPasswordsFoundDesc": { - "message": "Nous avons trouvé $COUNT$ éléments dans votre coffre qui ont des mots de passe qui ont été exposés dans des fuites de données connues. Vous devriez les changer pour utiliser un nouveau mot de passe.", + "exposedPasswordsFoundReportDesc": { + "message": "Nous avons trouvé $COUNT$ éléments dans $VAULT$ qui ont des mots de passe qui ont été exposés dans des fuites de données connues. Vous devriez les changer pour utiliser un nouveau mot de passe.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Mots de passe faibles trouvés" }, - "weakPasswordsFoundDesc": { - "message": "Nous avons trouvé $COUNT$ éléments dans votre coffre avec des mots de passe qui ne sont pas forts. Vous devriez les mettre à jour pour utiliser des mots de passe plus forts.", + "weakPasswordsFoundReportDesc": { + "message": "Nous avons trouvé $COUNT$ éléments dans $VAULT$ avec des mots de passe faibles. Vous devriez les mettre à jour pour utiliser des mots de passe plus forts.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Mots de passe réutilisés trouvés" }, - "reusedPasswordsFoundDesc": { - "message": "Nous avons trouvé $COUNT$ mots de passe qui sont réutilisés dans votre coffre. Vous devriez les changer pour utiliser des mots de passe différents.", + "reusedPasswordsFoundReportDesc": { + "message": "Nous avons trouvé $COUNT$ mots de passe qui sont réutilisés dans $VAULT$. Vous devriez les changer pour utiliser des mots de passe uniques.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Accorder l'accès aux collections en les ajoutant à ce groupe." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "Vous ne pouvez affecter que les collections que vous gérez." + }, "accessAllCollectionsDesc": { "message": "Accorder l'accès à toutes les collections actuelles et futures." }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Sélectionner un élément de la collection" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Gérer la facturation depuis le Portail Fournisseur" } } diff --git a/apps/web/src/locales/gl/messages.json b/apps/web/src/locales/gl/messages.json index 13fa5f60c0..2d7c32595c 100644 --- a/apps/web/src/locales/gl/messages.json +++ b/apps/web/src/locales/gl/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Unsecured websites found" }, - "unsecuredWebsitesFoundDesc": { - "message": "We found $COUNT$ items in your vault with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Logins without two-step login found" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Exposed passwords found" }, - "exposedPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault that have passwords that were exposed in known data breaches. You should change them to use a new password.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Weak passwords found" }, - "weakPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault with passwords that are not strong. You should update them to use stronger passwords.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Reused passwords found" }, - "reusedPasswordsFoundDesc": { - "message": "We found $COUNT$ passwords that are being reused in your vault. You should change them to a unique value.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/he/messages.json b/apps/web/src/locales/he/messages.json index b57005a76b..e5a57df2c3 100644 --- a/apps/web/src/locales/he/messages.json +++ b/apps/web/src/locales/he/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "נמצאו אתרים לא מאובטחים" }, - "unsecuredWebsitesFoundDesc": { - "message": "מצאנו $COUNT$ פריטים בכספת שלך המכילים כתובות לא מאובטחות. אנו ממליצים לשנות את הכתובות לתחילית https:// אם האתר מאפשר זאת.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "נמצאו פרטי כניסות שלא פעילה בהן אופציית 2FA" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "נמצאו סיסמאות שנחשפו" }, - "exposedPasswordsFoundDesc": { - "message": "מצאנו $COUNT$ פריטים בכספת שלך שיש להם סיסמאות שנחשפו בפרצות אבטחה. מומלץ לשנות אותן וליצור סיסמאות חדשות.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "נמצאו סיסמאות חלשות" }, - "weakPasswordsFoundDesc": { - "message": "מצאנו $COUNT$ פריטים בכספת שלך עם סיסמאות חלשות. מומלץ להשתמש בסיסמאות חזקות יותר.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "נמצאו סיסמאות משומשות" }, - "reusedPasswordsFoundDesc": { - "message": "מצאנו $COUNT$ סיסמאות משומשות בכספת שלך. כדאי שתשנה אותם כך שלכל פריט תהיה סיסמה ייחודית.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/hi/messages.json b/apps/web/src/locales/hi/messages.json index 818cc1cef5..5439aee5e5 100644 --- a/apps/web/src/locales/hi/messages.json +++ b/apps/web/src/locales/hi/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Unsecured websites found" }, - "unsecuredWebsitesFoundDesc": { - "message": "We found $COUNT$ items in your vault with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Logins without two-step login found" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Exposed passwords found" }, - "exposedPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault that have passwords that were exposed in known data breaches. You should change them to use a new password.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Weak passwords found" }, - "weakPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault with passwords that are not strong. You should update them to use stronger passwords.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Reused passwords found" }, - "reusedPasswordsFoundDesc": { - "message": "We found $COUNT$ passwords that are being reused in your vault. You should change them to a unique value.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/hr/messages.json b/apps/web/src/locales/hr/messages.json index ffeb8c2201..ed5fb33615 100644 --- a/apps/web/src/locales/hr/messages.json +++ b/apps/web/src/locales/hr/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Pronađena neosigurana web mjesta" }, - "unsecuredWebsitesFoundDesc": { - "message": "Pronašli smo $COUNT$ stavki u tvom trezoru koje koriste neosigurane URI-je (http://). Ako web mjesto omogućuje trebalo bi URI-je promijeniti na https://", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Prijave na kojima nije omogućena dvostruka autentifikacija" }, - "inactive2faFoundDesc": { - "message": "Pronašli smo $COUNT$ web mjesta u tvom trezoru za koje nije omogućena prijava dvostrukom autentifikacijom (izvor: 2fa.directory). Za bolju zaštitu ovih računa, potrebno je omogućiti dvostruku autentifikaciju.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Pronađene izložene lozinke" }, - "exposedPasswordsFoundDesc": { - "message": "Pronašli smo $COUNT$ stavki u tvom trezoru koje imaju lozinke koje su otkrivene prilikom znanih curenja podataka. Trebalo bi ih zamijentii novim lozinkama.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Pronađene slabe lozinke" }, - "weakPasswordsFoundDesc": { - "message": "Pronašli smo $COUNT$ stavki u tvom trezoru s lozinkama koje nisu jake. Trebalo bi ih zamijeniti jakim lozinkama.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Pronađene iste lozinke" }, - "reusedPasswordsFoundDesc": { - "message": "Pronašli smo $COUNT$ istih lozinki u tvom trezoru. Trebalo bi ih zamijeniti jedinstvenim lozinkama.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Odobri pristup zbirkama dodavanjem u ovu grupu." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Odobri pristup svim postojećim i budućim zbirkama." }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/hu/messages.json b/apps/web/src/locales/hu/messages.json index 008a20b4f0..ca5fa97736 100644 --- a/apps/web/src/locales/hu/messages.json +++ b/apps/web/src/locales/hu/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Nem-biztonságos webhelyek találhatók." }, - "unsecuredWebsitesFoundDesc": { - "message": "$COUNT$ elem található a széfben nem-biztonságos URI-val. Ezeket URI sémáját célszerű módosítani https://-re.", + "unsecuredWebsitesFoundReportDesc": { + "message": "$COUNT$ elem van a $VAULT$ széfben nem biztonságos URI-val. Az URI sémát célszeűrű módosítani https://-re, ha a webhely azt engedélyezi.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Kétlépések bejelentkezés nélküli bejelentkezések találhatók." }, - "inactive2faFoundDesc": { - "message": "$COUNT$ olyan webhelyet találtunk a széfben, amely nincs kétlépcsős hitelesítéssel konfigurálva (a 2fa.directory adatbázisa alapján). Ezen fiókok további védelme érdekében, javasolt a kétlépcsős hitelesítés használata.", + "inactive2faFoundReportDesc": { + "message": "$COUNT$ olyan webhelye(ke)t találtunk a $VAULT$ széfben, amely nincs kétlépcsős bejelentkezéssel konfigurálva (a 2fa.directory adatbázisa alapján). Ezen fiókok további védelme érdekében, célszerű a kétlépcsős bejelentkezés használata.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Kiszivárgott jelszavak találhatók." }, - "exposedPasswordsFoundDesc": { - "message": "$COUNT$ elem található a széfben, amelyek érintve voltak ismert adatszivárgásban. Célszerű új jelszavakra lecserélni ezeket.", + "exposedPasswordsFoundReportDesc": { + "message": "$COUNT$ elem található a $VAULT$ széfben, amelyek érintve voltak ismert adatszivárgásban. Célszerű ezeket megváltoztatni új jelszó használatával.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Gyenge jelszavak találhatók." }, - "weakPasswordsFoundDesc": { - "message": "$COUNT$ gyenge jelszó van a széfben. Célszerű lenne ezeket lecserélni erősebb jelszóra.", + "weakPasswordsFoundReportDesc": { + "message": "$COUNT$ gyenge jelszót találtunk a $VAULT$ széfben. Célszerű lenne ezeknél erősebb jelszóra áttérni.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Újrahasznált jelszavak találhatók." }, - "reusedPasswordsFoundDesc": { - "message": "$COUNT$ újrahasznált jelszó van a széfben. Változtassuk meg ezeket egyedi értékűre.", + "reusedPasswordsFoundReportDesc": { + "message": "$COUNT$ többször is használt jelszót találtunk a $VAULT$ széfben. Célszerű ezeknél a jelszót egyedi jelszavakra cseréni.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Hozzáférés kiosztása gyűjteményekhez csoporthoz adásukkal." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "Csak a saját kezelésű gyűjteményeket lehet hozzárendelni." + }, "accessAllCollectionsDesc": { "message": "Hozzáférés kiosztása az összes jelenlegi és jövőbeli gyűjteményhez." }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Gyűjtemény elem választás" + }, + "manageBillingFromProviderPortalMessage": { + "message": "A számlázás kezelése a szolgáltatói portálon keresztül" } } diff --git a/apps/web/src/locales/id/messages.json b/apps/web/src/locales/id/messages.json index df1af27271..dae66593f9 100644 --- a/apps/web/src/locales/id/messages.json +++ b/apps/web/src/locales/id/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Situs Web Tidak Aman Ditemukan" }, - "unsecuredWebsitesFoundDesc": { - "message": "Kami menemukan $COUNT$ item di lemari besi Anda dengan URI yang tidak aman. Anda harus mengubah skema URI mereka menjadi https: // jika situs web mengizinkannya.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Login Tanpa Ditemukan 2FA" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Kata Sandi Terkena Ditemukan" }, - "exposedPasswordsFoundDesc": { - "message": "Kami menemukan $COUNT$ item di lemari besi Anda yang memiliki sandi yang diketahui dalam pelanggaran data yang diketahui. Anda harus mengubahnya untuk menggunakan kata sandi baru.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Kata Sandi Lemah Ditemukan" }, - "weakPasswordsFoundDesc": { - "message": "Kami menemukan $COUNT$ item di lemari besi Anda dengan sandi yang tidak kuat. Anda harus memperbaruinya untuk menggunakan kata sandi yang lebih kuat.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Kata Sandi yang Digunakan Kembali Ditemukan" }, - "reusedPasswordsFoundDesc": { - "message": "Kami menemukan $COUNT$ sandi yang digunakan kembali di lemari besi Anda. Anda harus mengubahnya menjadi nilai unik.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/it/messages.json b/apps/web/src/locales/it/messages.json index 04342f13b1..9a55317978 100644 --- a/apps/web/src/locales/it/messages.json +++ b/apps/web/src/locales/it/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Siti web non protetti trovati" }, - "unsecuredWebsitesFoundDesc": { + "unsecuredWebsitesFoundReportDesc": { "message": "Abbiamo trovato $COUNT$ elementi nella tua cassaforte con URI non protetti. Dovresti cambiare il loro schema URL in https:// se il sito lo consente.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Login senza verifica in due passaggi trovati" }, - "inactive2faFoundDesc": { + "inactive2faFoundReportDesc": { "message": "Abbiamo trovato $COUNT$ siti web nella tua cassaforte che potrebbero non essere configurati con la verifica in due passaggi (secondo 2fa.directory). Per proteggere ulteriormente questi account, abilita la verifica in due passaggi.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Password esposte trovate" }, - "exposedPasswordsFoundDesc": { - "message": "Abbiamo trovato $COUNT$ elementi nella tua cassaforte contenenti password che sono state esposte in violazioni di dati. Dovresti modificarli per usare una nuova password.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Password deboli trovate" }, - "weakPasswordsFoundDesc": { - "message": "Abbiamo trovato $COUNT$ elementi nella tua cassaforte con password deboli. Aggiornali usando password più sicure.", + "weakPasswordsFoundReportDesc": { + "message": "Abbiamo trovato $COUNT$ elementi nella tua cassaforte con password che non sono forti. Dovresti aggiornarli per usare password più forti.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Password riutilizzate trovate" }, - "reusedPasswordsFoundDesc": { - "message": "Abbiamo trovato $COUNT$ password riutilizzate nella tua cassaforte. Cambiale in modo che ognuna sia unica.", + "reusedPasswordsFoundReportDesc": { + "message": "Abbiamo trovato $COUNT$ password che vengono riutilizzate nella tua cassaforte. Dovresti cambiarli in un valore univoco.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Concedi accesso alle raccolte aggiungendo gli utenti a questo gruppo." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "Puoi assegnare solo le raccolte che gestisci." + }, "accessAllCollectionsDesc": { "message": "Concedi accesso a tutte le raccolte esistenti e future." }, @@ -7907,7 +7930,7 @@ "message": "Non puoi aggiungerti a un gruppo." }, "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "message": "Avviso: dal 2 maggio 2024, gli elementi dell'organizzazione non assegnati non saranno più visibili nella tua 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." }, "unassignedItemsBannerNotice": { "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." @@ -7966,53 +7989,53 @@ "message": "Errore nell'assegnazione della cartella di destinazione." }, "integrationsAndSdks": { - "message": "Integrations & SDKs", + "message": "Integrazioni e SDK", "description": "The title for the section that deals with integrations and SDKs." }, "integrations": { - "message": "Integrations" + "message": "Integrazioni" }, "integrationsDesc": { - "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + "message": "Sincronizza automaticamente i segreti da Bitwarden Secrets Manager a un servizio di terze parti." }, "sdks": { - "message": "SDKs" + "message": "SDK" }, "sdksDesc": { - "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + "message": "Usa l'SDK di Bitwarden Secrets Manager nei seguenti linguaggi di programmazione per creare le tue applicazioni." }, "setUpGithubActions": { - "message": "Set up Github Actions" + "message": "Configura GitHub Actions" }, "setUpGitlabCICD": { - "message": "Set up GitLab CI/CD" + "message": "Configura GitLab CI/CD" }, "setUpAnsible": { - "message": "Set up Ansible" + "message": "Configura Ansible" }, "cSharpSDKRepo": { - "message": "View C# repository" + "message": "Visualizza la repository C#" }, "cPlusPlusSDKRepo": { - "message": "View C++ repository" + "message": "Visualizza la repository C++" }, "jsWebAssemblySDKRepo": { - "message": "View JS WebAssembly repository" + "message": "Visualizza la repository JS WebAssembly" }, "javaSDKRepo": { - "message": "View Java repository" + "message": "Visualizza la repository Java" }, "pythonSDKRepo": { - "message": "View Python repository" + "message": "Visualizza la repository Python" }, "phpSDKRepo": { - "message": "View php repository" + "message": "Visualizza la repository php" }, "rubySDKRepo": { - "message": "View Ruby repository" + "message": "Visualizza la repository Ruby" }, "goSDKRepo": { - "message": "View Go repository" + "message": "Visualizza la repository Go" }, "createNewClientToManageAsProvider": { "message": "Crea una nuova organizzazione cliente da gestire come fornitore. Gli slot aggiuntivi saranno riflessi nel prossimo ciclo di fatturazione." @@ -8036,10 +8059,10 @@ "message": "Nuovo cliente creato con successo" }, "noAccess": { - "message": "No access" + "message": "Nessun accesso" }, "collectionAdminConsoleManaged": { - "message": "This collection is only accessible from the admin console" + "message": "Questa raccolta è accessibile solo dalla console di amministrazione" }, "organizationOptionsMenu": { "message": "Attiva/Disattiva Menu Organizzazione" @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Seleziona elemento della raccolta" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Gestisci la fatturazione dal Portale del Fornitore" } } diff --git a/apps/web/src/locales/ja/messages.json b/apps/web/src/locales/ja/messages.json index 1861743320..b93dbe7a60 100644 --- a/apps/web/src/locales/ja/messages.json +++ b/apps/web/src/locales/ja/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "セキュリティ保護されていないウェブサイトが見つかりました" }, - "unsecuredWebsitesFoundDesc": { - "message": "セキュアでないURIが$COUNT$個のアイテムで見つかりました。ウェブサイトが対応しているならば、URIをhttps://形式に変更すべきです。", + "unsecuredWebsitesFoundReportDesc": { + "message": "セキュアでない URI が $VAULT$ 内で $COUNT$ 個見つかりました。ウェブサイトが対応している場合は、URI を https:// 形式に変更すべきです。", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "二段階認証を利用していないアイテムが見つかりました" }, - "inactive2faFoundDesc": { - "message": "保管庫に $COUNT$ 個のウェブサイトがあり、2段階認証でのログインが設定されていない可能性があります (2fa.directory によれば)。これらのアカウントをさらに保護するには、2段階認証のログインを設定する必要があります。", + "inactive2faFoundReportDesc": { + "message": "$VAULT$ に $COUNT$ 個のウェブサイトがあり、(2fa.directory によれば) 2段階認証でのログインを設定できる可能性があります 。これらのアカウントをさらに保護するには、2段階認証のログインを設定してください。", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "流出したパスワードが見つかりました" }, - "exposedPasswordsFoundDesc": { - "message": "既知のデータ流出で公開されていたパスワードが $COUNT$ 個のアイテムで見つかりました。これらは新しいパスワードへ変更すべきです。", + "exposedPasswordsFoundReportDesc": { + "message": "既知の流出済みパスワードが $VAULT$ 内の $COUNT$ 個のアイテムで見つかりました。これらは新しいパスワードへ変更すべきです。", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "脆弱なパスワードが見つかりました" }, - "weakPasswordsFoundDesc": { - "message": "保管庫内に$COUNT$個の強力でないパスワードが見つかりました。もっと強力なパスワードへ更新すべきです。", + "weakPasswordsFoundReportDesc": { + "message": "$VAULT$ 内に $COUNT$ 個の脆弱なパスワードが見つかりました。もっと強力なパスワードへ更新すべきです。", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "再利用しているパスワードが見つかりました。" }, - "reusedPasswordsFoundDesc": { - "message": "$COUNT$個の再利用しているパスワードが見つかりました。それぞれ異なるパスワードへ変更すべきです。", + "reusedPasswordsFoundReportDesc": { + "message": "$VAULT$ 内に $COUNT$ 個の再利用しているパスワードが見つかりました。それぞれ異なるパスワードへ変更すべきです。", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "このグループに追加してコレクションへのアクセスを許可します。" }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "現在および将来のすべてのコレクションへのアクセスを許可します。" }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "コレクションのアイテムを選択" + }, + "manageBillingFromProviderPortalMessage": { + "message": "プロバイダーポータルからの請求を管理" } } diff --git a/apps/web/src/locales/ka/messages.json b/apps/web/src/locales/ka/messages.json index defe9e621d..c05eab5314 100644 --- a/apps/web/src/locales/ka/messages.json +++ b/apps/web/src/locales/ka/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Unsecured websites found" }, - "unsecuredWebsitesFoundDesc": { - "message": "We found $COUNT$ items in your vault with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "მომხმარებლები ორსაფეხურიანი სისტემაში შესვლის გარეშე ნაპოვნია" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Exposed passwords found" }, - "exposedPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault that have passwords that were exposed in known data breaches. You should change them to use a new password.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Weak passwords found" }, - "weakPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault with passwords that are not strong. You should update them to use stronger passwords.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Reused passwords found" }, - "reusedPasswordsFoundDesc": { - "message": "We found $COUNT$ passwords that are being reused in your vault. You should change them to a unique value.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/km/messages.json b/apps/web/src/locales/km/messages.json index 13fa5f60c0..2d7c32595c 100644 --- a/apps/web/src/locales/km/messages.json +++ b/apps/web/src/locales/km/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Unsecured websites found" }, - "unsecuredWebsitesFoundDesc": { - "message": "We found $COUNT$ items in your vault with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Logins without two-step login found" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Exposed passwords found" }, - "exposedPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault that have passwords that were exposed in known data breaches. You should change them to use a new password.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Weak passwords found" }, - "weakPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault with passwords that are not strong. You should update them to use stronger passwords.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Reused passwords found" }, - "reusedPasswordsFoundDesc": { - "message": "We found $COUNT$ passwords that are being reused in your vault. You should change them to a unique value.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/kn/messages.json b/apps/web/src/locales/kn/messages.json index cfb188b580..a7f71b2ef9 100644 --- a/apps/web/src/locales/kn/messages.json +++ b/apps/web/src/locales/kn/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "ಅಸುರಕ್ಷಿತ ವೆಬ್‌ಸೈಟ್‌ಗಳು ಕಂಡುಬಂದಿವೆ" }, - "unsecuredWebsitesFoundDesc": { - "message": "ಅಸುರಕ್ಷಿತ URI ಗಳೊಂದಿಗೆ ನಿಮ್ಮ ವಾಲ್ಟ್‌ನಲ್ಲಿ $COUNT$ ವಸ್ತುಗಳನ್ನು ನಾವು ಕಂಡುಕೊಂಡಿದ್ದೇವೆ. ವೆಬ್‌ಸೈಟ್ ಅನುಮತಿಸಿದರೆ ನೀವು ಅವರ ಯುಆರ್‌ಐ ಯೋಜನೆಯನ್ನು https: // ಗೆ ಬದಲಾಯಿಸಬೇಕು.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "2FA ಇಲ್ಲದೆ ಲಾಗಿನ್‌ಗಳು ಕಂಡುಬಂದಿಲ್ಲ" }, - "inactive2faFoundDesc": { - "message": "ನಿಮ್ಮ ವಾಲ್ಟ್‌ನಲ್ಲಿ $COUNT$ ವೆಬ್‌ಸೈಟ್ (ಗಳನ್ನು) ನಾವು ಕಂಡುಕೊಂಡಿದ್ದೇವೆ, ಅದನ್ನು ಎರಡು ಅಂಶಗಳ ದೃಢೀಕರಣದೊಂದಿಗೆ ಕಾನ್ಫಿಗರ್ ಮಾಡಲಾಗುವುದಿಲ್ಲ (2fa.directory ಪ್ರಕಾರ). ಈ ಖಾತೆಗಳನ್ನು ಮತ್ತಷ್ಟು ರಕ್ಷಿಸಲು, ನೀವು ಎರಡು ಅಂಶಗಳ ದೃಢೀಕರಣವನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಬೇಕು.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "ಬಹಿರಂಗಪಡಿಸಿದ ಪಾಸ್‌ವರ್ಡ್‌ಗಳು ಕಂಡುಬಂದಿವೆ" }, - "exposedPasswordsFoundDesc": { - "message": "ತಿಳಿದಿರುವ ದತ್ತಾಂಶ ಉಲ್ಲಂಘನೆಗಳಲ್ಲಿ ಬಹಿರಂಗಗೊಂಡ ಪಾಸ್‌ವರ್ಡ್‌ಗಳನ್ನು ಹೊಂದಿರುವ ನಿಮ್ಮ ವಾಲ್ಟ್‌ನಲ್ಲಿ $COUNT$ ವಸ್ತುಗಳನ್ನು ನಾವು ಕಂಡುಕೊಂಡಿದ್ದೇವೆ. ಹೊಸ ಪಾಸ್‌ವರ್ಡ್ ಬಳಸಲು ನೀವು ಅವುಗಳನ್ನು ಬದಲಾಯಿಸಬೇಕು.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "ದುರ್ಬಲ ಪಾಸ್‌ವರ್ಡ್‌ಗಳು ಕಂಡುಬಂದಿವೆ" }, - "weakPasswordsFoundDesc": { - "message": "ನಿಮ್ಮ ವಾಲ್ಟ್‌ನಲ್ಲಿ ಪ್ರಬಲವಲ್ಲದ ಪಾಸ್‌ವರ್ಡ್‌ಗಳೊಂದಿಗೆ $COUNT$ ವಸ್ತುಗಳನ್ನು ನಾವು ಕಂಡುಕೊಂಡಿದ್ದೇವೆ. ಬಲವಾದ ಪಾಸ್‌ವರ್ಡ್‌ಗಳನ್ನು ಬಳಸಲು ನೀವು ಅವುಗಳನ್ನು ನವೀಕರಿಸಬೇಕು.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "ಮರುಬಳಕೆ ಮಾಡಿದ ಪಾಸ್‌ವರ್ಡ್‌ಗಳು ಕಂಡುಬಂದಿವೆ" }, - "reusedPasswordsFoundDesc": { - "message": "ನಿಮ್ಮ ವಾಲ್ಟ್‌ನಲ್ಲಿ ಮರುಬಳಕೆ ಮಾಡಲಾಗುತ್ತಿರುವ $COUNT$ ಪಾಸ್‌ವರ್ಡ್‌ಗಳನ್ನು ನಾವು ಕಂಡುಕೊಂಡಿದ್ದೇವೆ. ನೀವು ಅವುಗಳನ್ನು ಅನನ್ಯ ಮೌಲ್ಯಕ್ಕೆ ಬದಲಾಯಿಸಬೇಕು.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/ko/messages.json b/apps/web/src/locales/ko/messages.json index 9dd856e656..86695a392a 100644 --- a/apps/web/src/locales/ko/messages.json +++ b/apps/web/src/locales/ko/messages.json @@ -573,13 +573,13 @@ "message": "폴더 삭제함" }, "editInfo": { - "message": "Edit info" + "message": "정보 편집" }, "access": { "message": "접근" }, "accessLevel": { - "message": "Access level" + "message": "접근 권한" }, "loggedOut": { "message": "로그아웃됨" @@ -615,7 +615,7 @@ "message": "마스터 비밀번호로 로그인" }, "readingPasskeyLoading": { - "message": "Reading passkey..." + "message": "패스키를 읽는 중..." }, "readingPasskeyLoadingInfo": { "message": "Keep this window open and follow prompts from your browser." @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "안전하지 않은 웹사이트가 발견됨" }, - "unsecuredWebsitesFoundDesc": { - "message": "보관함에 안전하지 않은 URI를 가진 항목 $COUNT$개를 발견했습니다. 웹 사이트에서 허용하는 경우 URI 스키마를 https://로 변경하십시오.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "2단계 인증이 없는 로그인이 발견됨" }, - "inactive2faFoundDesc": { - "message": "보관함에 (2fa.directory에 따른) 2단계 인증이 설정되지 않은 웹 사이트를 $COUNT$개 발견했습니다. 이러한 계정을 더욱 보호하려면 2단계 인증을 사용하십시오.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "노출된 비밀번호가 발견됨" }, - "exposedPasswordsFoundDesc": { - "message": "보관함에 알려진 데이터 유출로 노출된 비밀번호가 있는 $COUNT$개의 항목을 발견했습니다. 새 암호를 사용하도록 암호를 변경해야합니다.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "취약한 비밀번호가 발견됨" }, - "weakPasswordsFoundDesc": { - "message": "강력한 비밀번호가 아닌 $COUNT$개의 항목을 보관함에서 찾았습니다. 더 강력한 암호를 사용하도록 업데이트해야 합니다.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "재사용된 비밀번호가 발견됨" }, - "reusedPasswordsFoundDesc": { - "message": "보관함에서 재사용중인 $COUNT$개의 비밀번호를 찾았습니다. 고유한 값으로 변경해야 합니다.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/lv/messages.json b/apps/web/src/locales/lv/messages.json index 1ad03eeb82..bf47a1009d 100644 --- a/apps/web/src/locales/lv/messages.json +++ b/apps/web/src/locales/lv/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Atrastas nedrošas tīmekļvietnes" }, - "unsecuredWebsitesFoundDesc": { - "message": "Glabātavā tika atrasts(i) $COUNT$ vienums(i) ar nedrošām adresēm. Ir ieteicams tās mainīt uz URI ar https://, ja tīmekļvietne to nodrošina.", + "unsecuredWebsitesFoundReportDesc": { + "message": "$VAULT$ tika atrasts(i) $COUNT$ vienums(i) ar nedrošām adresēm. Ir ieteicams tās mainīt uz URI ar https://, ja tīmekļvietne to nodrošina.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Atrastie pieteikšanās vienumi bez 2FA" }, - "inactive2faFoundDesc": { - "message": "Glabātavā tika atrasta(s) $COUNT$ tīmekļvietne(s), kurā(s) nav uzstādīta divpakāpju pieteikšanās (vadoties pēc 2fa.directory). Lai labāk aizsargātu šos kontus, ir ieteicams uzstādīt divpakāpju pieteikšanos.", + "inactive2faFoundReportDesc": { + "message": "$VAULT$ tika atrasta(s) $COUNT$ tīmekļvietne(s), kurā(s) nav uzstādīta divpakāpju pieteikšanās (vadoties pēc 2fa.directory). Lai labāk aizsargātu šos kontus, ir ieteicams uzstādīt divpakāpju pieteikšanos.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Atrastas atklātās paroles" }, - "exposedPasswordsFoundDesc": { - "message": "Glabātavā tika atrasts(i) $COUNT$ vienums(i), kuros ir paroles, kas ir atklātas zināmās datu noplūdēs. Tos vajadzētu mainīt, lai izmantotu jaunu paroli.", + "exposedPasswordsFoundReportDesc": { + "message": "$VAULT$ tika atrasts(i) $COUNT$ vienums(i), kuros ir paroles, kas ir atklātas zināmās datu noplūdēs. Tos vajadzētu mainīt, lai izmantotu jaunu paroli.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Atrastas vājas paroles" }, - "weakPasswordsFoundDesc": { - "message": "Glabātavā tika atrasts(i) $COUNT$ vienums(i) ar parolēm, kas nav spēcīgas. Tos vajadzētu atjaunināt, lai izmantotu spēcīgākas paroles.", + "weakPasswordsFoundReportDesc": { + "message": "$VAULT$ tika atrasts(i) $COUNT$ vienums(i) ar parolēm, kas nav spēcīgas. Tos vajadzētu atjaunināt, lai izmantotu spēcīgākas paroles.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Atrastās vairākkārt izmantotās paroles" }, - "reusedPasswordsFoundDesc": { - "message": "Glabātavā tika atrasta(s) $COUNT$ parole(s), kas tiek vairākkārt izmantotas. Ir ieteicams tās nomainīt uz vērtību, kas neatkārtojas citur.", + "reusedPasswordsFoundReportDesc": { + "message": "$VAULT$ tika atrasta(s) $COUNT$ parole(s), kas tiek vairākkārt izmantotas. Ir ieteicams tās nomainīt uz vērtību, kas neatkārtojas citur.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Piešķirt piekļuvi krājumiem, pievienojot tos šai grupai." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "Var piešķirt tikai pārvaldītos krājumus." + }, "accessAllCollectionsDesc": { "message": "Piešķirt piekļuvi visiem pašreizējiem un turpmākajiem krājumiem." }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Atlasīt krājuma vienumu" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Norēķinus var pārvaldīt Nodrošinātāju portālā" } } diff --git a/apps/web/src/locales/ml/messages.json b/apps/web/src/locales/ml/messages.json index c2b83a5e28..d3a958a95b 100644 --- a/apps/web/src/locales/ml/messages.json +++ b/apps/web/src/locales/ml/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Unsecured Websites Found" }, - "unsecuredWebsitesFoundDesc": { - "message": "We found $COUNT$ items in your vault with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Logins Without 2FA Found" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Exposed Passwords Found" }, - "exposedPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault that have passwords that were exposed in known data breaches. You should change them to use a new password.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "ദുർബലമായ പാസ്‌വേഡുകൾ കണ്ടെത്തി" }, - "weakPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault with passwords that are not strong. You should update them to use stronger passwords.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Reused Passwords Found" }, - "reusedPasswordsFoundDesc": { - "message": "We found $COUNT$ passwords that are being reused in your vault. You should change them to a unique value.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/mr/messages.json b/apps/web/src/locales/mr/messages.json index 13fa5f60c0..2d7c32595c 100644 --- a/apps/web/src/locales/mr/messages.json +++ b/apps/web/src/locales/mr/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Unsecured websites found" }, - "unsecuredWebsitesFoundDesc": { - "message": "We found $COUNT$ items in your vault with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Logins without two-step login found" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Exposed passwords found" }, - "exposedPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault that have passwords that were exposed in known data breaches. You should change them to use a new password.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Weak passwords found" }, - "weakPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault with passwords that are not strong. You should update them to use stronger passwords.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Reused passwords found" }, - "reusedPasswordsFoundDesc": { - "message": "We found $COUNT$ passwords that are being reused in your vault. You should change them to a unique value.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/my/messages.json b/apps/web/src/locales/my/messages.json index 13fa5f60c0..2d7c32595c 100644 --- a/apps/web/src/locales/my/messages.json +++ b/apps/web/src/locales/my/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Unsecured websites found" }, - "unsecuredWebsitesFoundDesc": { - "message": "We found $COUNT$ items in your vault with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Logins without two-step login found" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Exposed passwords found" }, - "exposedPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault that have passwords that were exposed in known data breaches. You should change them to use a new password.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Weak passwords found" }, - "weakPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault with passwords that are not strong. You should update them to use stronger passwords.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Reused passwords found" }, - "reusedPasswordsFoundDesc": { - "message": "We found $COUNT$ passwords that are being reused in your vault. You should change them to a unique value.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/nb/messages.json b/apps/web/src/locales/nb/messages.json index 2ffdc5214f..adb0dc144d 100644 --- a/apps/web/src/locales/nb/messages.json +++ b/apps/web/src/locales/nb/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Usikrede nettsteder ble funnet" }, - "unsecuredWebsitesFoundDesc": { - "message": "Vi fant $COUNT$ objekter i hvelvet ditt som benytter usikrede URI-er. Du burde endre deres URI-er til å benytte https://, dersom det nettstedet tillater det.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Pålogginger som støtter 2FA ble funnet" }, - "inactive2faFoundDesc": { - "message": "Vi fant $COUNT$ nettsted(er) i hvelvet ditt som kanskje ikke har blitt satt opp med 2-trinnspålogging (i følge 2fa.directory). For å beskytte disse kontoene ytterligere, burde du sette opp 2-trinnspålogging.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Eksponerte passord ble funnet" }, - "exposedPasswordsFoundDesc": { - "message": "Vi fant $COUNT$ elementer i hvelvet ditt med passord eksponert i kjente datainnbrudd. Du bør endre passordet på dem.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Svake passord ble funnet" }, - "weakPasswordsFoundDesc": { - "message": "Vi fant $COUNT$ objekter i hvelvet ditt som har passord som ikke er sterke. Du burde oppdatere dem til å bruke sterkere passord.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Gjenbrukte passord ble funnet" }, - "reusedPasswordsFoundDesc": { - "message": "Vi fant $COUNT$ passord som blir gjenbrukt i hvelvet ditt. Du burde endre dem slik at de er unike.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Gi tilgang til samlinger ved å legge dem til i denne gruppen." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Gi tilgang til alle nåværende og fremtidige samlinger." }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/ne/messages.json b/apps/web/src/locales/ne/messages.json index 7854d49a7b..6590b35547 100644 --- a/apps/web/src/locales/ne/messages.json +++ b/apps/web/src/locales/ne/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Unsecured websites found" }, - "unsecuredWebsitesFoundDesc": { - "message": "We found $COUNT$ items in your vault with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Logins without two-step login found" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Exposed passwords found" }, - "exposedPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault that have passwords that were exposed in known data breaches. You should change them to use a new password.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Weak passwords found" }, - "weakPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault with passwords that are not strong. You should update them to use stronger passwords.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Reused passwords found" }, - "reusedPasswordsFoundDesc": { - "message": "We found $COUNT$ passwords that are being reused in your vault. You should change them to a unique value.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/nl/messages.json b/apps/web/src/locales/nl/messages.json index b9f405cb06..17d396386c 100644 --- a/apps/web/src/locales/nl/messages.json +++ b/apps/web/src/locales/nl/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Onveilige websites gevonden" }, - "unsecuredWebsitesFoundDesc": { - "message": "We hebben $COUNT$ items met onbeveiligde URIs in je kluis gevonden. Als de website het ondersteunt, moet je de URI naar https:// wijzigen.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We vonden $COUNT$ items met onbeveiligde URIs in $VAULT$. Als de website het ondersteunt, kun je de URI beter aanpassen naar https://.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Logins zonder 2FA gevonden" }, - "inactive2faFoundDesc": { - "message": "We hebben $COUNT$ website(s) in je kluis gevonden waar je (volgens 2fa.directory) nog tweestapsaanmelding kunt instellen. Om deze accounts beter te beschermen, zou je tweestapsaanmelding moeten inschakelen.", + "inactive2faFoundReportDesc": { + "message": "We vonden $COUNT$ website(s) in $VAULT$ waar je tweestapsaanmelding nog niet gebruikt (terwijl dat volgens 2fa.directory wel kan). Om deze accounts beter te beschermen, kun je tweestapsaanmelding instellen.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Gelekte wachtwoorden gevonden" }, - "exposedPasswordsFoundDesc": { - "message": "We hebben in je kluis $COUNT$ wachtwoorden gevonden die zijn gelekt. Je zou voor deze accounts een nieuw wachtwoord moeten instellen.", + "exposedPasswordsFoundReportDesc": { + "message": "We vonden $COUNT$ items in $VAULT$ met wachtwoorden die voorkomen in bekende datalekken. Je kunt die wachtwoorden het beste vervangen door een nieuw wachtwoord.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Zwakke wachtwoorden gevonden" }, - "weakPasswordsFoundDesc": { - "message": "We hebben $COUNT$ zwakke wachtwoorden in je kluis gevonden. Je zou ze moeten veranderen in sterke wachtwoorden.", + "weakPasswordsFoundReportDesc": { + "message": "We vonden $COUNT$ items in $VAULT$ met zwakke wachtwoorden. Je kunt die wachtwoorden het beste vervangen door sterke wachtwoorden.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Dubbele wachtwoorden gevonden" }, - "reusedPasswordsFoundDesc": { - "message": "We hebben $COUNT$ dubbele wachtwoorden in je kluis gevonden. Je zou deze moeten veranderen in unieke wachtwoorden.", + "reusedPasswordsFoundReportDesc": { + "message": "We vonden $COUNT$ wachtwoorden die vaker voorkomen in $VAULT$. Je kunt die wachtwoorden het beste vervangen door een uniek wachtwoord.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Geef toegang tot collecties door ze aan deze groep toe te voegen." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Geef toegang tot alle huidige en toekomstige collecties." }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/nn/messages.json b/apps/web/src/locales/nn/messages.json index 53ac66738e..35c43669a4 100644 --- a/apps/web/src/locales/nn/messages.json +++ b/apps/web/src/locales/nn/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Unsecured websites found" }, - "unsecuredWebsitesFoundDesc": { - "message": "We found $COUNT$ items in your vault with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Logins without two-step login found" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Exposed passwords found" }, - "exposedPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault that have passwords that were exposed in known data breaches. You should change them to use a new password.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Weak passwords found" }, - "weakPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault with passwords that are not strong. You should update them to use stronger passwords.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Gjenbrukte passord blei funne" }, - "reusedPasswordsFoundDesc": { - "message": "We found $COUNT$ passwords that are being reused in your vault. You should change them to a unique value.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/or/messages.json b/apps/web/src/locales/or/messages.json index 13fa5f60c0..2d7c32595c 100644 --- a/apps/web/src/locales/or/messages.json +++ b/apps/web/src/locales/or/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Unsecured websites found" }, - "unsecuredWebsitesFoundDesc": { - "message": "We found $COUNT$ items in your vault with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Logins without two-step login found" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Exposed passwords found" }, - "exposedPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault that have passwords that were exposed in known data breaches. You should change them to use a new password.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Weak passwords found" }, - "weakPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault with passwords that are not strong. You should update them to use stronger passwords.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Reused passwords found" }, - "reusedPasswordsFoundDesc": { - "message": "We found $COUNT$ passwords that are being reused in your vault. You should change them to a unique value.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/pl/messages.json b/apps/web/src/locales/pl/messages.json index 8fc603c1d5..66aacde732 100644 --- a/apps/web/src/locales/pl/messages.json +++ b/apps/web/src/locales/pl/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Znaleźliśmy niezabezpieczone strony" }, - "unsecuredWebsitesFoundDesc": { - "message": "Znaleźliśmy elementy w Twoim sejfie zawierające niezabezpieczone adresy URI. Jeśli witryna to umożliwia, zmień schemat adresu na protokół HTTPS.", + "unsecuredWebsitesFoundReportDesc": { + "message": "Znaleźliśmy $COUNT$ elementów w Twoim $VAULT$, zawierających niezabezpieczone identyfikatory URI. Jeśli witryna to umożliwia, należy zmienić schemat identyfikatora URI na https://.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Znaleźliśmy elementy bez włączonej opcji logowania dwustopniowego" }, - "inactive2faFoundDesc": { - "message": "Znaleźliśmy $COUNT$ witryn(y) w sejfie, które mogą nie być skonfigurowane z wykorzystaniem logowania dwustopniowego (wg 2fa.directory). Aby dodatkowo zabezpieczyć te konta, należy skonfigurować logowanie dwustopniowe.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Znaleźliśmy ujawnione hasła" }, - "exposedPasswordsFoundDesc": { - "message": "Znaleźliśmy elementy w sejfie, które zawierają ujawnione hasła w znanych wyciekach danych. Zmień te hasła.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Znaleźliśmy słabe hasła" }, - "weakPasswordsFoundDesc": { - "message": "Znaleźliśmy elementy w sejfie, które zawierają słabe hasła. Zaktualizuj je na silniejsze hasła.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Znaleźliśmy identyczne hasła" }, - "reusedPasswordsFoundDesc": { - "message": "Znaleźliśmy hasła, które powtarzają się w sejfie. Zmień je, aby były unikalne.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Przyznaj dostęp do kolekcji, dodając je do tej grupy." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Udziel dostępu do wszystkich bieżących i przyszłych kolekcji." }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Wybierz element kolekcji" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Zarządzaj płatnościami z portalu dostawcy" } } diff --git a/apps/web/src/locales/pt_BR/messages.json b/apps/web/src/locales/pt_BR/messages.json index 06b117ed06..7c94396334 100644 --- a/apps/web/src/locales/pt_BR/messages.json +++ b/apps/web/src/locales/pt_BR/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Sites Inseguros Encontrados" }, - "unsecuredWebsitesFoundDesc": { - "message": "Nós encontramos $COUNT$ item(ns) no seu cofre com URIs não protegido(s). Você deve alterar o esquema de URI para https:// se o site permitir.", + "unsecuredWebsitesFoundReportDesc": { + "message": "Nós encontramos $COUNT$ item(ns) no seu cofre com URIs não segura(s). Você deve alterar o esquema de URI para https:// se o site permitir.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Credenciais Sem 2FA Encontradas" }, - "inactive2faFoundDesc": { - "message": "Encontramos $COUNT$ site(s) no seu cofre que pode(m) não estar configurados com login em duas etapas (segundo o site 2fa.directory). Para proteger ainda mais essas contas, você deve configurar o login em duas etapas.", + "inactive2faFoundReportDesc": { + "message": "Encontramos $COUNT$ site(s) no seu $VAULT$ que pode(m) não estar configurado com o login em duas etapas (de acordo com 2fa. irectory). Para proteger ainda mais essas contas, você deve configurar o login em duas etapas.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Senhas Expostas Encontradas" }, - "exposedPasswordsFoundDesc": { + "exposedPasswordsFoundReportDesc": { "message": "Encontramos no seu cofre $COUNT$ item(ns) com senha(s) que foi(ram) exposta(s) em violações de dado conhecida. Você deve alterá-las para usar uma nova senha.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Senhas Fracas Encontradas" }, - "weakPasswordsFoundDesc": { + "weakPasswordsFoundReportDesc": { "message": "Encontramos $COUNT$ item(ns) no seu cofre com senha(s) que não é/são fortes. Você deve atualizá-las para usar senhas mais fortes.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Senhas Reutilizadas Encontradas" }, - "reusedPasswordsFoundDesc": { + "reusedPasswordsFoundReportDesc": { "message": "Nós encontramos $COUNT$ senha(s) que esta(ão) sendo reutilizadas no seu cofre. Você deve alterá-los para um valor único.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Conceder acesso às coleções adicionando-as a este grupo." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "Você só pode atribuir coleções que você gerencia." + }, "accessAllCollectionsDesc": { "message": "Conceder acesso a todas as coleções atuais e futuras." }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Selecionar item da coleção" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Gerenciar faturamento a partir do Portal do Provedor" } } diff --git a/apps/web/src/locales/pt_PT/messages.json b/apps/web/src/locales/pt_PT/messages.json index 961d4531fa..38bc977d3a 100644 --- a/apps/web/src/locales/pt_PT/messages.json +++ b/apps/web/src/locales/pt_PT/messages.json @@ -67,7 +67,7 @@ "message": "Número do passaporte" }, "licenseNumber": { - "message": "Número da licença" + "message": "Número da carta de condução" }, "email": { "message": "E-mail" @@ -365,7 +365,7 @@ "message": "Cidade / Localidade" }, "stateProvince": { - "message": "Estado / Província" + "message": "Estado / Região" }, "zipPostalCode": { "message": "Código postal" @@ -1135,10 +1135,10 @@ "message": "Pontuação mínima de complexidade" }, "minNumbers": { - "message": "Números mínimos" + "message": "Mínimo de números" }, "minSpecial": { - "message": "Caracteres especiais minímos", + "message": "Mínimo de caracteres especiais", "description": "Minimum special characters" }, "ambiguous": { @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Sites inseguros encontrados" }, - "unsecuredWebsitesFoundDesc": { + "unsecuredWebsitesFoundReportDesc": { "message": "Encontrámos $COUNT$ itens no seu cofre com URIs não seguros. Deve alterar o esquema de URI para https:// se o site o permitir.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Credenciais sem verificação de dois passos encontrados" }, - "inactive2faFoundDesc": { - "message": "Encontrámos $COUNT$ site(s) no seu cofre que podem não ter a verificação de dois passos configurada (de acordo com 2fa.directory). Para proteger ainda mais essas contas, deve configurar a verificação de dois passos.", + "inactive2faFoundReportDesc": { + "message": "Encontrámos $COUNT$ site(s) no seu cofre que podem não estar configurados com a verificação de dois passos (de acordo com a 2fa.directory). Para proteger ainda mais estas contas, deve configurar a verificação de dois passos.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Palavras-passe expostas encontradas" }, - "exposedPasswordsFoundDesc": { - "message": "Encontrámos $COUNT$ itens no seu cofre que têm palavras-passe que foram expostas em violações de dados conhecidas. Deve alterá-los para utilizar uma nova palavra-passe.", + "exposedPasswordsFoundReportDesc": { + "message": "Encontrámos $COUNT$ itens no seu cofre que têm palavras-passe que foram expostas em violações de dados conhecidas. Deve alterá-los para utilizarem uma nova palavra-passe.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Palavras-passe fracas encontradas" }, - "weakPasswordsFoundDesc": { + "weakPasswordsFoundReportDesc": { "message": "Encontrámos $COUNT$ itens no seu cofre com palavras-passe que não são fortes. Deve atualizá-los para utilizarem palavras-passe mais fortes.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Palavras-passe reutilizadas encontradas" }, - "reusedPasswordsFoundDesc": { + "reusedPasswordsFoundReportDesc": { "message": "Encontrámos $COUNT$ palavras-passe que estão a ser reutilizadas no seu cofre. Deve alterá-las para um valor único.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -3706,7 +3726,7 @@ "description": "ex. Date this item was updated" }, "dateCreated": { - "message": "Criado a", + "message": "Criado", "description": "ex. Date this item was created" }, "datePasswordUpdated": { @@ -5955,7 +5975,7 @@ "message": "Ligado" }, "off": { - "message": "Desligado" + "message": "Desativado" }, "members": { "message": "Membros" @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Conceder aos membros acesso às coleções adicionando-os a este grupo." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "Só pode atribuir colecções que gere." + }, "accessAllCollectionsDesc": { "message": "Conceder acesso a todas as coleções atuais e futuras." }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Selecionar item da coleção" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Gira a faturação a partir do Portal do fornecedor" } } diff --git a/apps/web/src/locales/ro/messages.json b/apps/web/src/locales/ro/messages.json index bd735d8f75..1d54a578d5 100644 --- a/apps/web/src/locales/ro/messages.json +++ b/apps/web/src/locales/ro/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "S-au găsit website-uri nesecurizate" }, - "unsecuredWebsitesFoundDesc": { - "message": "Am găsit $COUNT$ articole în seiful dvs. cu URI-uri nesecurizate. Ar trebui să schimbați schema URl-urilor lor în https:// dacă saitul o permite.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "S-au găsit autentificări fără autentificare în două etape" }, - "inactive2faFoundDesc": { - "message": "Am găsit $COUNT$ website(-uri) în seiful dvs. care s-ar putea să nu fie configurate cu autentificare în două etape (în conformitate cu 2fa.directory). Pentru a proteja în continuare aceste conturi, ar trebui să configurați autentificarea în două etape.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Parolele expuse găsite" }, - "exposedPasswordsFoundDesc": { - "message": "Am găsit $COUNT$ articole în seiful dvs. care folosesc parole dezvăluite în scurgeri de date cunoscute. Ar trebui să le schimbați pentru a utiliza o parolă nouă.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Parole slabe găsite" }, - "weakPasswordsFoundDesc": { - "message": "Am găsit $COUNT$ articole cu parole slabe articole în seiful dvs. Ar trebui să le actualizați ca să folosească parole puternice.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "S-au găsit parole refolosite" }, - "reusedPasswordsFoundDesc": { - "message": "Am găsit $COUNT$ parole reutilizate în seiful dvs. Ar trebui să le schimbați la o valoare unică.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/ru/messages.json b/apps/web/src/locales/ru/messages.json index 213a9d1965..bc1ff8d6eb 100644 --- a/apps/web/src/locales/ru/messages.json +++ b/apps/web/src/locales/ru/messages.json @@ -1,6 +1,6 @@ { "whatTypeOfItem": { - "message": "Какой это тип элемента?" + "message": "Выберите тип элемента" }, "name": { "message": "Название" @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Найдены незащищенные сайты" }, - "unsecuredWebsitesFoundDesc": { - "message": "В хранилище обнаружены элементы ($COUNT$ шт.) с незащищенными URI. Вам следует изменить их схему URI на https://, если сайт это позволяет.", + "unsecuredWebsitesFoundReportDesc": { + "message": "В вашем $VAULT$ обнаружены элементы ($COUNT$ шт.) с незащищенными URI. Вам следует изменить их схему URI на https://, если сайт это позволяет.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Найдены логины без двухэтапной аутентификации" }, - "inactive2faFoundDesc": { - "message": "В хранилище обнаружены сайты ($COUNT$ шт.), у которых может быть не настроена двухэтапная аутентификация (согласно 2fa.directory). Для дополнительной защиты этих аккаунтов следует настроить двухэтапную аутентификацию.", + "inactive2faFoundReportDesc": { + "message": "В вашем $VAULT$ обнаружены сайты ($COUNT$ шт.), у которых может быть не настроена двухэтапная аутентификация (согласно 2fa.directory). Для дополнительной защиты этих аккаунтов следует настроить двухэтапную аутентификацию.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Найдены скомпрометированные пароли" }, - "exposedPasswordsFoundDesc": { - "message": "В хранилище обнаружены элементы ($COUNT$ шт.), пароли которых скомпрометированы. Вам следует задать для них новые пароли.", + "exposedPasswordsFoundReportDesc": { + "message": "В вашем $VAULT$ обнаружены элементы ($COUNT$ шт.), пароли которых скомпрометированы. Вам следует задать для них новые пароли.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Обнаружены слабые пароли" }, - "weakPasswordsFoundDesc": { - "message": "В хранилище есть элементы ($COUNT$ шт.) с ненадежными паролями. Следует задать для них более сильные пароли.", + "weakPasswordsFoundReportDesc": { + "message": "В вашем $VAULT$ обнаружены элементы ($COUNT$ шт.) с ненадежными паролями. Следует задать для них более сильные пароли.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Обнаружены повторно использованные пароли" }, - "reusedPasswordsFoundDesc": { - "message": "В хранилище есть элементы ($COUNT$ шт.) с повторно использованными паролями. Следует изменить их на уникальные.", + "reusedPasswordsFoundReportDesc": { + "message": "В вашем $VAULT$ обнаружена элементы ($COUNT$ шт.) с повторно использованными паролями. Следует изменить их на уникальные.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Предоставить доступ к коллекциям, при добавлении их в эту группу." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "Вы можете назначать только те коллекции, которыми управляете." + }, "accessAllCollectionsDesc": { "message": "Предоставить доступ ко всем текущим и будущим коллекциям." }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Выбрать элемент коллекции" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Управление биллингом на портале провайдера" } } diff --git a/apps/web/src/locales/si/messages.json b/apps/web/src/locales/si/messages.json index 0cd67041d3..f74146210e 100644 --- a/apps/web/src/locales/si/messages.json +++ b/apps/web/src/locales/si/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Unsecured websites found" }, - "unsecuredWebsitesFoundDesc": { - "message": "We found $COUNT$ items in your vault with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Logins without two-step login found" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Exposed passwords found" }, - "exposedPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault that have passwords that were exposed in known data breaches. You should change them to use a new password.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Weak passwords found" }, - "weakPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault with passwords that are not strong. You should update them to use stronger passwords.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Reused passwords found" }, - "reusedPasswordsFoundDesc": { - "message": "We found $COUNT$ passwords that are being reused in your vault. You should change them to a unique value.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/sk/messages.json b/apps/web/src/locales/sk/messages.json index 652b1b08fd..c47022d785 100644 --- a/apps/web/src/locales/sk/messages.json +++ b/apps/web/src/locales/sk/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Našli sa nezabezpečené stránky" }, - "unsecuredWebsitesFoundDesc": { - "message": "Našli sme $COUNT$ položky vo vašom trezore s nezabezpečenými URI. Ak to stránka podporuje, môžete zmeniť schému URI na https://.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Našli sa prihlásenia bez dvojstupňového overenia" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Našli sme uniknuté heslá" }, - "exposedPasswordsFoundDesc": { - "message": "Našli sme $COUNT$ položiek vo vašom trezore ktoré používajú uniknuté heslá. Mali by ste ich zmeniť aby používali nové heslá.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Našli sa slabé heslá" }, - "weakPasswordsFoundDesc": { - "message": "Našli sme $COUNT$ položiek vo vašom trezore, ktoré nepoužívajú silné heslá. Mali by ste ich aktualizovať a použiť silnejšie heslá.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Našli sa viacnásobne použité heslá" }, - "reusedPasswordsFoundDesc": { - "message": "Vo vašom trezore sme našli $COUNT$ hesiel, ktoré sú použité na viacerých stránkach. Mali by ste ich zmeniť aby každá stránka mala unikátne heslo.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Povoľte prístup k zbierkam ich pridaním do tejto skupiny." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Povoliť prístup k všetkým súčasným a budúcim zbierkam." }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Vyberte položku zo zbierky" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Spravujte fakturáciu cez portál poskytovateľa" } } diff --git a/apps/web/src/locales/sl/messages.json b/apps/web/src/locales/sl/messages.json index dfde437380..8f8102d14e 100644 --- a/apps/web/src/locales/sl/messages.json +++ b/apps/web/src/locales/sl/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Unsecured websites found" }, - "unsecuredWebsitesFoundDesc": { - "message": "We found $COUNT$ items in your vault with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Logins without two-step login found" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Našli smo izpostavljena gesla" }, - "exposedPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault that have passwords that were exposed in known data breaches. You should change them to use a new password.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Našli smo šibka gesla" }, - "weakPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault with passwords that are not strong. You should update them to use stronger passwords.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Našli smo podvojena gesla" }, - "reusedPasswordsFoundDesc": { - "message": "Našli smo toliko gesel, ki se uporabljajo na več mestih: $COUNT$. Morali bi jih spremeniti tako, da bo vsako drugačno.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/sr/messages.json b/apps/web/src/locales/sr/messages.json index de563afe50..1b060d0d3f 100644 --- a/apps/web/src/locales/sr/messages.json +++ b/apps/web/src/locales/sr/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Пронађене су незаштићене веб странице" }, - "unsecuredWebsitesFoundDesc": { - "message": "Нашли смо $COUNT$ ставке у вашем сефу са незаштићеним УРЛ. Требали би да промените шеме у https:// ако веб страница то дозвољава.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Нађене пријаве без 2FA" }, - "inactive2faFoundDesc": { - "message": "Нашли смо $COUNT$ сајта у вашем сефу који можда нису конфигурисани за пријаву у два корака (према 2fa.directory). Да бисте додатно заштитили ове налоге, требало би да подесите пријаву у два корака.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Пронађене изложене лозинке" }, - "exposedPasswordsFoundDesc": { - "message": "Пронашли смо у вашем сефу $COUNT$ предмета који садрже лозинке откривене у познатим повредама података. Требали би да их промените да бисте користили нову лозинку.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Пронађене су слабе лозинке" }, - "weakPasswordsFoundDesc": { - "message": "Пронашли смо у вашем сефу $COUNT$ ставки са слабим лозинкама. Требали бисте их ажурирати да би користили јаче лозинке.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Пронађене поновне лозинке" }, - "reusedPasswordsFoundDesc": { - "message": "Нашли смо $COUNT$ лозинке које се поново користе у вашем сефу. Требали бисте да их промените у јединствену вредност.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Изаберите ставку колекције" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/sr_CS/messages.json b/apps/web/src/locales/sr_CS/messages.json index d6c354f4da..d4d435759a 100644 --- a/apps/web/src/locales/sr_CS/messages.json +++ b/apps/web/src/locales/sr_CS/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Unsecured websites found" }, - "unsecuredWebsitesFoundDesc": { - "message": "We found $COUNT$ items in your vault with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Logins without two-step login found" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Exposed passwords found" }, - "exposedPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault that have passwords that were exposed in known data breaches. You should change them to use a new password.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Weak passwords found" }, - "weakPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault with passwords that are not strong. You should update them to use stronger passwords.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Reused passwords found" }, - "reusedPasswordsFoundDesc": { - "message": "We found $COUNT$ passwords that are being reused in your vault. You should change them to a unique value.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/sv/messages.json b/apps/web/src/locales/sv/messages.json index 21a100c5af..427ea5f9c3 100644 --- a/apps/web/src/locales/sv/messages.json +++ b/apps/web/src/locales/sv/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Osäkra webbplatser hittades" }, - "unsecuredWebsitesFoundDesc": { - "message": "Vi hittade $COUNT$ objekt i ditt valv med osäkra URI:er. Om webbplatsen stödjer det bör du ändra deras URI-protokoll till https://.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Inloggningar utan 2FA hittades" }, - "inactive2faFoundDesc": { - "message": "Vi hittade $COUNT$ webbplats(er) i ditt valv som kanske inte har tvåstegsverifiering konfigurerat (enligt 2fa.directory). För att skydda dessa konton ytterligare bör du aktivera tvåstegsverifiering.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Avslöjade lösenord hittades" }, - "exposedPasswordsFoundDesc": { - "message": "Vi hittade $COUNT$ objekt i ditt valv med lösenord som har äventyrats i kända dataintrång. Du bör ändra dessa till att använda nya lösenord.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Svaga lösenord hittades" }, - "weakPasswordsFoundDesc": { - "message": "Vi hittade $COUNT$ objekt i ditt valv med lösenord som inte är starka. Du bör ändra dessa till att använda starkare lösenord.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Återanvända lösenord hittades" }, - "reusedPasswordsFoundDesc": { - "message": "Vi hittade $COUNT$ lösenord som återanvänds i ditt valv. Du bör ändra dessa till unika lösenord.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "Du kan bara tilldela samlingar som du hanterar." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/te/messages.json b/apps/web/src/locales/te/messages.json index 13fa5f60c0..2d7c32595c 100644 --- a/apps/web/src/locales/te/messages.json +++ b/apps/web/src/locales/te/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Unsecured websites found" }, - "unsecuredWebsitesFoundDesc": { - "message": "We found $COUNT$ items in your vault with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Logins without two-step login found" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Exposed passwords found" }, - "exposedPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault that have passwords that were exposed in known data breaches. You should change them to use a new password.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Weak passwords found" }, - "weakPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault with passwords that are not strong. You should update them to use stronger passwords.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Reused passwords found" }, - "reusedPasswordsFoundDesc": { - "message": "We found $COUNT$ passwords that are being reused in your vault. You should change them to a unique value.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/th/messages.json b/apps/web/src/locales/th/messages.json index 929c91e74b..091660dd83 100644 --- a/apps/web/src/locales/th/messages.json +++ b/apps/web/src/locales/th/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Unsecured websites found" }, - "unsecuredWebsitesFoundDesc": { - "message": "We found $COUNT$ items in your vault with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Logins without two-step login found" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Exposed passwords found" }, - "exposedPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault that have passwords that were exposed in known data breaches. You should change them to use a new password.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Weak passwords found" }, - "weakPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault with passwords that are not strong. You should update them to use stronger passwords.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Reused passwords found" }, - "reusedPasswordsFoundDesc": { - "message": "We found $COUNT$ passwords that are being reused in your vault. You should change them to a unique value.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/tr/messages.json b/apps/web/src/locales/tr/messages.json index 1087bc89bb..b439f60538 100644 --- a/apps/web/src/locales/tr/messages.json +++ b/apps/web/src/locales/tr/messages.json @@ -657,7 +657,7 @@ "message": "Geçiş anahtarı başarıyla oluşturuldu." }, "customPasskeyNameInfo": { - "message": "Tanımlamanıza yardımcı olması için Passkey'inize bir isim verin." + "message": "Sonradan tanıyabilmeniz için geçiş anahtarınıza bir isim verin." }, "useForVaultEncryption": { "message": "Kasa şifrelemesinde kullan" @@ -678,7 +678,7 @@ "message": "Şifreleme için kullanılır" }, "loginWithPasskeyEnabled": { - "message": "Passkey ile giriş yap" + "message": "Geçiş anahtarıyla giriş açık" }, "passkeySaved": { "message": "$NAME$ kaydedildi", @@ -696,10 +696,10 @@ "message": "Geçiş anahtarını kaldır" }, "removePasskeyInfo": { - "message": "Tüm Passkey'ler kaldırılırsa, ana parolanız olmadan yeni cihazlara giriş yapamazsınız." + "message": "Tüm geçiş anahtarları kaldırılırsa ana parolanız olmadan yeni cihazlara giriş yapamazsınız." }, "passkeyLimitReachedInfo": { - "message": "Passkey sınırına ulaşıldı. Başka bir Passkey eklemek için bir Passley'i kaldırın." + "message": "Geçiş anahtarı sınırına ulaşıldı. Yeni bir geçiş anahtarı eklemek için mevcut bir geçiş anahtarını kaldırın." }, "tryAgain": { "message": "Yeniden dene" @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Güvensiz web siteleri bulundu" }, - "unsecuredWebsitesFoundDesc": { - "message": "Kasanızda güvenli olmayan URI'ye sahip $COUNT$ kayıt bulduk. Web sitesi izin veriyorsa URI şemasını https:// olarak değiştirmelisiniz.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "İki aşamalı girişi olmayan hesaplar bulundu" }, - "inactive2faFoundDesc": { - "message": "Kasanızda iki aşamalı giriş kullanmıyor olabilecek $COUNT$ web sitesi bulduk (2fa.directory’ye göre). Bu hesapları daha iyi korumak için iki aşamalı girişi etkinleştirmelisiniz.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Açığa çıkmış parolalar bulundu" }, - "exposedPasswordsFoundDesc": { - "message": "Kasanızda, bilinen veri ihlallerine maruz kalmış parolalara sahip $COUNT$ kayıt bulundu. Bu parolaları değiştirmelisiniz.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Zayıf parolalar pulundu" }, - "weakPasswordsFoundDesc": { - "message": "Kasanızda zayıf parolalara sahip $COUNT$ kayıt bulduk. Bunları güncelleyip daha güçlü parolalar kullanmalısınız.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Yeniden kullanılmış parolalar bulundu" }, - "reusedPasswordsFoundDesc": { - "message": "Kasanızda tekrar kullanılmakta olan $COUNT$ parola bulduk. Onları benzersiz parolalarla değiştirmelisiniz.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -2323,7 +2343,7 @@ "message": "Müşteri hizmetleriyle iletişime geçin" }, "contactSupportShort": { - "message": "Destek Ekibiyle İletişime Geç" + "message": "Destek ekibiyle iletişime geç" }, "updatedPaymentMethod": { "message": "Ödeme yöntemi güncellendi." @@ -2427,7 +2447,7 @@ "message": "İşletmeler ve diğer ekipler için." }, "planNameTeamsStarter": { - "message": "Başlangıç Ekipleri" + "message": "Teams Starter" }, "planNameEnterprise": { "message": "Kurumsal" @@ -3678,7 +3698,7 @@ "message": "Kasa zaman aşımı eyleminin ne zaman gerçekleştirileceğini seçin." }, "vaultTimeoutLogoutDesc": { - "message": "Kasanızın oturumundan ne zaman çıkış yapılacağını seçin." + "message": "Kasanızdan ne zaman çıkış yapılacağını seçin." }, "oneMinute": { "message": "1 dakika" @@ -4782,7 +4802,7 @@ "message": "Ana parolaları olan mevcut hesaplar, yöneticilerin hesaplarını kurtarabilmesi için üyelerin kendi kendilerine kaydolmalarını gerektirecektir. Otomatik kayıt, yeni üyeler için hesap kurtarmayı açacaktır." }, "accountRecoverySingleOrgRequirementDesc": { - "message": "The single organization Enterprise policy must be turned on before activating this policy." + "message": "Bu ilkeyi etkinleştirmeden önce tek kuruluş kurumsal ilkesi etkinleştirilmelidir." }, "resetPasswordPolicyAutoEnroll": { "message": "Otomatik eklenme" @@ -4957,7 +4977,7 @@ "message": "Mevcut kuruluşu ekle" }, "addNewOrganization": { - "message": "Add new organization" + "message": "Yeni kuruluş ekle" }, "myProvider": { "message": "Sağlayıcım" @@ -5796,7 +5816,7 @@ "message": "Cihaz doğrulamasını aç" }, "deviceVerificationDesc": { - "message": "Verification codes are sent to your email address when logging in from an unrecognized device" + "message": "Tanınmayan bir cihazdan oturum açarken e-posta adresinize doğrulama kodları gönderilir" }, "updatedDeviceVerification": { "message": "Cihaz doğrulaması güncellendi" @@ -5940,13 +5960,13 @@ } }, "launchDuoAndFollowStepsToFinishLoggingIn": { - "message": "DUO'yu başlatın ve oturum açmayı tamamlamak için adımları izleyin." + "message": "Duo'yu başlatın ve oturum açmayı tamamlamak için adımları izleyin." }, "duoRequiredByOrgForAccount": { - "message": "Hesabınız için DUO iki adımlı giriş gereklidir." + "message": "Hesabınız için Duo iki adımlı giriş gereklidir." }, "launchDuo": { - "message": "DUO'yu başlat" + "message": "Duo'yu başlat" }, "turnOn": { "message": "Aç" @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Kullanıcıları bu gruba ekleyerek koleksiyonlara erişim izni verin." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Mevcut ve gelecekteki tüm koleksiyonlara erişim izni verin." }, @@ -6647,16 +6670,16 @@ } }, "verificationRequiredForActionSetPinToContinue": { - "message": "Verification required for this action. Set a PIN to continue." + "message": "Bu işlem için doğrulama gerekiyor. Devam etmek için bir PIN belirleyin." }, "setPin": { - "message": "Set PIN" + "message": "PIN belirle" }, "verifyWithBiometrics": { - "message": "Verify with biometrics" + "message": "Biyometri ile doğrula" }, "awaitingConfirmation": { - "message": "Awaiting confirmation" + "message": "Onay bekleniyor" }, "couldNotCompleteBiometrics": { "message": "Biyometri işlemi tamamlanamadı." @@ -6671,10 +6694,10 @@ "message": "PIN kullan" }, "useBiometrics": { - "message": "Use biometrics" + "message": "Biyometri kullan" }, "enterVerificationCodeSentToEmail": { - "message": "Enter the verification code that was sent to your email." + "message": "E-posta adresinize gönderilen doğrulama kodunu girin." }, "resendCode": { "message": "Kodu yeniden gönder" @@ -7150,7 +7173,7 @@ } }, "verificationRequired": { - "message": "Verification required", + "message": "Doğrulama gerekli", "description": "Default title for the user verification dialog." }, "recoverAccount": { @@ -7517,11 +7540,11 @@ "message": "Hizmet hesabı erişimi güncellendi" }, "commonImportFormats": { - "message": "Common formats", + "message": "Sık kullanılan biçimler", "description": "Label indicating the most common import formats" }, "maintainYourSubscription": { - "message": "$ORG$ aboneliğinizi sürdürmek için, ", + "message": "$ORG$ aboneliğinizi sürdürmek için ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To maintain your subscription for $ORG$, add a payment method.'", "placeholders": { "org": { @@ -7531,7 +7554,7 @@ } }, "addAPaymentMethod": { - "message": "bir ödeme yöntemi ekle", + "message": "bir ödeme yöntemi ekleyin", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To maintain your subscription for $ORG$, add a payment method.'" }, "collectionEnhancementsDesc": { @@ -7554,7 +7577,7 @@ "message": "E-posta adresinize bir onay e-postası gönderdik:" }, "confirmCollectionEnhancementsDialogTitle": { - "message": "Bu eylem geri alınamaz" + "message": "Bu işlem geri alınamaz" }, "confirmCollectionEnhancementsDialogContent": { "message": "Bu özelliğin etkinleştirilmesi, yönetici rolünün kullanımdan kaldırılmasına ve onun yerine Yönetebilir izninin getirilmesine neden olur. Bu birkaç dakika sürecektir. Tamamlanana kadar herhangi bir organizasyon değişikliği yapmayın. Devam etmek istediğinizden emin misiniz?" @@ -7580,11 +7603,11 @@ "description": "An option for the offboarding survey shown when a user cancels their subscription." }, "tooDifficultToUse": { - "message": "Kullanılması zor", + "message": "Kullanması zor", "description": "An option for the offboarding survey shown when a user cancels their subscription." }, "notUsingEnough": { - "message": "Yeterince kullanılmama", + "message": "Fazla kullanmıyorum", "description": "An option for the offboarding survey shown when a user cancels their subscription." }, "tooExpensive": { @@ -7607,7 +7630,7 @@ "message": "Sağlayıcı Portalı" }, "success": { - "message": "Success" + "message": "Başarılı" }, "viewCollection": { "message": "View collection" @@ -7662,10 +7685,10 @@ "message": "Assigned" }, "used": { - "message": "Used" + "message": "Kullanılan" }, "remaining": { - "message": "Remaining" + "message": "Kalan" }, "unlinkOrganization": { "message": "Unlink organization" @@ -7930,7 +7953,7 @@ "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." }, "deleteProviderName": { - "message": "Cannot delete $ID$", + "message": "$ID$ silinemedi", "placeholders": { "id": { "content": "$1", @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/uk/messages.json b/apps/web/src/locales/uk/messages.json index e7b9722c9d..9dcb10f101 100644 --- a/apps/web/src/locales/uk/messages.json +++ b/apps/web/src/locales/uk/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Знайдено незахищені вебсайти" }, - "unsecuredWebsitesFoundDesc": { - "message": "Ми знайшли $COUNT$ записів у вашому сховищі з незахищеними URL-адресами. Вам слід змінити їхні URL-схеми на https://, якщо вони це дозволяють.", + "unsecuredWebsitesFoundReportDesc": { + "message": "Ми знайшли $COUNT$ записів у вашому $VAULT$ з незахищеними URL-адресами. Вам слід змінити їхні URL-схеми на https://, якщо вони це дозволяють.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Знайдено записи без двоетапної перевірки" }, - "inactive2faFoundDesc": { - "message": "Ми знайшли $COUNT$ вебсайтів у вашому сховищі, що можуть бути не налаштовані для двоетапної перевірки (за даними 2fa.directory). Для захисту цих облікових записів вам слід активувати двоетапну перевірку.", + "inactive2faFoundReportDesc": { + "message": "Ми знайшли $COUNT$ вебсайтів у вашому $VAULT$, що можуть бути не налаштовані для двоетапної перевірки (за даними 2fa.directory). Для захисту цих облікових записів вам слід активувати двоетапну перевірку.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Знайдено викриті паролі" }, - "exposedPasswordsFoundDesc": { - "message": "У вашому сховищі знайдено $COUNT$ записів з паролями, які було викрито у відомих витоках даних. Вам слід змінити їх з використанням нового пароля.", + "exposedPasswordsFoundReportDesc": { + "message": "У вашому $VAULT$ знайдено $COUNT$ записів з паролями, які було викрито у відомих витоках даних. Вам слід змінити їх з використанням нового пароля.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Знайдено ненадійні паролі" }, - "weakPasswordsFoundDesc": { - "message": "У вашому сховищі знайдено $COUNT$ записів з ненадійними паролями. Вам слід оновити їх з використанням надійніших паролів.", + "weakPasswordsFoundReportDesc": { + "message": "У вашому $VAULT$ знайдено $COUNT$ записів з ненадійними паролями. Вам слід оновити їх з використанням надійніших паролів.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Знайдено повторювані паролі" }, - "reusedPasswordsFoundDesc": { - "message": "У вашому сховищі знайдено $COUNT$ паролів з повторним використанням. Вам слід змінити їх на унікальні.", + "reusedPasswordsFoundReportDesc": { + "message": "У вашому $VAULT$ знайдено $COUNT$ паролів з повторним використанням. Вам слід змінити їх на унікальні.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Надати доступ до збірок, додавши їх до цієї групи." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "Ви можете призначити лише збірки, якими керуєте." + }, "accessAllCollectionsDesc": { "message": "Надати доступ до всіх наявних і майбутніх збірок." }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Вибрати елемент збірки" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Керування рахунками на порталі провайдера" } } diff --git a/apps/web/src/locales/vi/messages.json b/apps/web/src/locales/vi/messages.json index 81fd3c2820..43718cd347 100644 --- a/apps/web/src/locales/vi/messages.json +++ b/apps/web/src/locales/vi/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Tìm thấy trang web không an toàn" }, - "unsecuredWebsitesFoundDesc": { - "message": "We found $COUNT$ items in your vault with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Logins without two-step login found" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Phát hiện mật khẩu bị rò rĩ" }, - "exposedPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault that have passwords that were exposed in known data breaches. You should change them to use a new password.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Phát hiện mật khẩu yếu" }, - "weakPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault with passwords that are not strong. You should update them to use stronger passwords.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Phát hiện mật khẩu bị trùng" }, - "reusedPasswordsFoundDesc": { - "message": "We found $COUNT$ passwords that are being reused in your vault. You should change them to a unique value.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/zh_CN/messages.json b/apps/web/src/locales/zh_CN/messages.json index 292041624c..0631291823 100644 --- a/apps/web/src/locales/zh_CN/messages.json +++ b/apps/web/src/locales/zh_CN/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "发现不安全的网站" }, - "unsecuredWebsitesFoundDesc": { - "message": "我们在您的密码库中发现了 $COUNT$ 个项目带有不安全的 URI。如果网站允许,您应该将他们更改为 https://。", + "unsecuredWebsitesFoundReportDesc": { + "message": "我们在您的 $VAULT$ 中发现了 $COUNT$ 个带不安全 URI 的项目。如果网站允许,您应将其 URI 方案更改为 https://。", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "发现未启用两步登录的登录项目" }, - "inactive2faFoundDesc": { - "message": "我们在您的密码库中发现 $COUNT$ 个网站可能没有配置两步登录(根据 2fa.directory)。为了进一步保护这些账户,您应该设置两步登录。", + "inactive2faFoundReportDesc": { + "message": "我们在您的 $VAULT$ 中发现了 $COUNT$ 个网站可能未配置两步登录(通过 2fa.directory)。为进一步保护这些账户,您应设置两步登录。", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "发现暴露的密码" }, - "exposedPasswordsFoundDesc": { - "message": "我们在您的密码库中发现了 $COUNT$ 个项目的密码在已知数据泄露事件中被暴露。您应该将它们更改为新密码。", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "发现弱密码" }, - "weakPasswordsFoundDesc": { - "message": "我们在您的密码库中发现了 $COUNT$ 个弱密码项目。您应该将它们改为更强的密码。", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "发现重复使用的密码" }, - "reusedPasswordsFoundDesc": { - "message": "我们在您的密码库中发现了 $COUNT$ 个被重复使用的密码。您应该将它们更改为唯一值。", + "reusedPasswordsFoundReportDesc": { + "message": "我们在您的 $VAULT$ 中发现了 $COUNT$ 个被重复使用的密码。您应将其更改为唯一值。", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "通过将集合添加到此群组来授予对集合的访问权限。" }, + "editGroupCollectionsRestrictionsDesc": { + "message": "您只能分配您管理的集合。" + }, "accessAllCollectionsDesc": { "message": "授予对所有当前和未来的集合的访问权限。" }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "选择集合项目" + }, + "manageBillingFromProviderPortalMessage": { + "message": "在供应商门户中管理账单" } } diff --git a/apps/web/src/locales/zh_TW/messages.json b/apps/web/src/locales/zh_TW/messages.json index 42e47a6e31..5e4d8f5d48 100644 --- a/apps/web/src/locales/zh_TW/messages.json +++ b/apps/web/src/locales/zh_TW/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "發現不安全的網站" }, - "unsecuredWebsitesFoundDesc": { - "message": "我們在您的密碼庫中找到了 $COUNT$ 個使用不安全 URI 的項目。若網站允許,您應變更其網址配置為 https://。", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "發現未啟用 2FA 的登入" }, - "inactive2faFoundDesc": { - "message": "我們在您的密碼庫中找到 $COUNT$ 個可能未設定兩步驟登入的網站(依據 twofactorauth.org)。若要進一步保護這些帳戶,您應啟用兩步驟登入。", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "發現暴露的密碼" }, - "exposedPasswordsFoundDesc": { - "message": "我們在您的密碼庫中找到了 $COUNT$ 個項目的密碼在已知資料外洩事件中被暴露。您應將它們變更為新密碼。", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "發現弱式密碼" }, - "weakPasswordsFoundDesc": { - "message": "我們在您的密碼庫中找到了 $COUNT$ 個使用弱式密碼的項目。您應該將它們變更為更強的密碼。", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "發現重複使用的密碼" }, - "reusedPasswordsFoundDesc": { - "message": "我們在您的密碼庫中找到了 $COUNT$ 組密碼重複使用。您應該將它們變更為不同的密碼。", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "透過將他們添加到此群組,授予對集合的存取權限。" }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "授予對所有目前和未來的集合的存取權限。" }, @@ -8049,5 +8072,8 @@ }, "collectionItemSelect": { "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } From ed236df24b93509aa8e50b7b7a4804cf95959c6e Mon Sep 17 00:00:00 2001 From: Anas <abarghoud@gmail.com> Date: Fri, 3 May 2024 15:44:57 +0200 Subject: [PATCH 350/351] fix(8560): refreshing reports pages displays empty pages (#8700) --- .../tools/exposed-passwords-report.component.ts | 3 +++ .../tools/inactive-two-factor-report.component.ts | 3 +++ .../tools/reused-passwords-report.component.ts | 11 ++++++++++- .../tools/unsecured-websites-report.component.ts | 11 ++++++++++- .../tools/weak-passwords-report.component.ts | 3 +++ .../tools/reports/pages/cipher-report.component.ts | 3 +++ .../pages/exposed-passwords-report.component.spec.ts | 11 +++++++++++ .../pages/exposed-passwords-report.component.ts | 11 ++++++++++- .../inactive-two-factor-report.component.spec.ts | 11 +++++++++++ .../pages/inactive-two-factor-report.component.ts | 11 ++++++++++- .../pages/reused-passwords-report.component.spec.ts | 11 +++++++++++ .../pages/reused-passwords-report.component.ts | 11 ++++++++++- .../pages/unsecured-websites-report.component.spec.ts | 11 +++++++++++ .../pages/unsecured-websites-report.component.ts | 11 ++++++++++- .../pages/weak-passwords-report.component.spec.ts | 11 +++++++++++ .../reports/pages/weak-passwords-report.component.ts | 11 ++++++++++- 16 files changed, 137 insertions(+), 7 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/tools/exposed-passwords-report.component.ts b/apps/web/src/app/admin-console/organizations/tools/exposed-passwords-report.component.ts index d354459ee9..cab6189c45 100644 --- a/apps/web/src/app/admin-console/organizations/tools/exposed-passwords-report.component.ts +++ b/apps/web/src/app/admin-console/organizations/tools/exposed-passwords-report.component.ts @@ -6,6 +6,7 @@ import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { PasswordRepromptService } from "@bitwarden/vault"; @@ -29,6 +30,7 @@ export class ExposedPasswordsReportComponent extends BaseExposedPasswordsReportC private route: ActivatedRoute, passwordRepromptService: PasswordRepromptService, i18nService: I18nService, + syncService: SyncService, ) { super( cipherService, @@ -37,6 +39,7 @@ export class ExposedPasswordsReportComponent extends BaseExposedPasswordsReportC modalService, passwordRepromptService, i18nService, + syncService, ); } diff --git a/apps/web/src/app/admin-console/organizations/tools/inactive-two-factor-report.component.ts b/apps/web/src/app/admin-console/organizations/tools/inactive-two-factor-report.component.ts index 67d4e963b0..abfbd45f38 100644 --- a/apps/web/src/app/admin-console/organizations/tools/inactive-two-factor-report.component.ts +++ b/apps/web/src/app/admin-console/organizations/tools/inactive-two-factor-report.component.ts @@ -6,6 +6,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { PasswordRepromptService } from "@bitwarden/vault"; @@ -26,6 +27,7 @@ export class InactiveTwoFactorReportComponent extends BaseInactiveTwoFactorRepor passwordRepromptService: PasswordRepromptService, organizationService: OrganizationService, i18nService: I18nService, + syncService: SyncService, ) { super( cipherService, @@ -34,6 +36,7 @@ export class InactiveTwoFactorReportComponent extends BaseInactiveTwoFactorRepor logService, passwordRepromptService, i18nService, + syncService, ); } diff --git a/apps/web/src/app/admin-console/organizations/tools/reused-passwords-report.component.ts b/apps/web/src/app/admin-console/organizations/tools/reused-passwords-report.component.ts index c8ceb023af..76d783b666 100644 --- a/apps/web/src/app/admin-console/organizations/tools/reused-passwords-report.component.ts +++ b/apps/web/src/app/admin-console/organizations/tools/reused-passwords-report.component.ts @@ -5,6 +5,7 @@ import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { PasswordRepromptService } from "@bitwarden/vault"; @@ -27,8 +28,16 @@ export class ReusedPasswordsReportComponent extends BaseReusedPasswordsReportCom organizationService: OrganizationService, passwordRepromptService: PasswordRepromptService, i18nService: I18nService, + syncService: SyncService, ) { - super(cipherService, organizationService, modalService, passwordRepromptService, i18nService); + super( + cipherService, + organizationService, + modalService, + passwordRepromptService, + i18nService, + syncService, + ); } async ngOnInit() { diff --git a/apps/web/src/app/admin-console/organizations/tools/unsecured-websites-report.component.ts b/apps/web/src/app/admin-console/organizations/tools/unsecured-websites-report.component.ts index 2a905b3665..7f6f08fb96 100644 --- a/apps/web/src/app/admin-console/organizations/tools/unsecured-websites-report.component.ts +++ b/apps/web/src/app/admin-console/organizations/tools/unsecured-websites-report.component.ts @@ -5,6 +5,7 @@ import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { PasswordRepromptService } from "@bitwarden/vault"; @@ -24,8 +25,16 @@ export class UnsecuredWebsitesReportComponent extends BaseUnsecuredWebsitesRepor organizationService: OrganizationService, passwordRepromptService: PasswordRepromptService, i18nService: I18nService, + syncService: SyncService, ) { - super(cipherService, organizationService, modalService, passwordRepromptService, i18nService); + super( + cipherService, + organizationService, + modalService, + passwordRepromptService, + i18nService, + syncService, + ); } async ngOnInit() { diff --git a/apps/web/src/app/admin-console/organizations/tools/weak-passwords-report.component.ts b/apps/web/src/app/admin-console/organizations/tools/weak-passwords-report.component.ts index 8820e596e3..0ac2129478 100644 --- a/apps/web/src/app/admin-console/organizations/tools/weak-passwords-report.component.ts +++ b/apps/web/src/app/admin-console/organizations/tools/weak-passwords-report.component.ts @@ -6,6 +6,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { PasswordRepromptService } from "@bitwarden/vault"; @@ -29,6 +30,7 @@ export class WeakPasswordsReportComponent extends BaseWeakPasswordsReportCompone organizationService: OrganizationService, passwordRepromptService: PasswordRepromptService, i18nService: I18nService, + syncService: SyncService, ) { super( cipherService, @@ -37,6 +39,7 @@ export class WeakPasswordsReportComponent extends BaseWeakPasswordsReportCompone modalService, passwordRepromptService, i18nService, + syncService, ); } diff --git a/apps/web/src/app/tools/reports/pages/cipher-report.component.ts b/apps/web/src/app/tools/reports/pages/cipher-report.component.ts index 041307122b..4e63dd5cc9 100644 --- a/apps/web/src/app/tools/reports/pages/cipher-report.component.ts +++ b/apps/web/src/app/tools/reports/pages/cipher-report.component.ts @@ -6,6 +6,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { PasswordRepromptService } from "@bitwarden/vault"; @@ -40,6 +41,7 @@ export class CipherReportComponent implements OnDestroy { protected passwordRepromptService: PasswordRepromptService, protected organizationService: OrganizationService, protected i18nService: I18nService, + private syncService: SyncService, ) { this.organizations$ = this.organizationService.organizations$; this.organizations$.pipe(takeUntil(this.destroyed$)).subscribe((orgs) => { @@ -106,6 +108,7 @@ export class CipherReportComponent implements OnDestroy { async load() { this.loading = true; + await this.syncService.fullSync(false); // when a user fixes an item in a report we want to persist the filter they had // if they fix the last item of that filter we will go back to the "All" filter if (this.currentFilterStatus) { diff --git a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.spec.ts index 7b73ad8305..07dc218bd6 100644 --- a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.spec.ts +++ b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.spec.ts @@ -9,6 +9,7 @@ import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { PasswordRepromptService } from "@bitwarden/vault"; import { ExposedPasswordsReportComponent } from "./exposed-passwords-report.component"; @@ -19,8 +20,10 @@ describe("ExposedPasswordsReportComponent", () => { let fixture: ComponentFixture<ExposedPasswordsReportComponent>; let auditService: MockProxy<AuditService>; let organizationService: MockProxy<OrganizationService>; + let syncServiceMock: MockProxy<SyncService>; beforeEach(() => { + syncServiceMock = mock<SyncService>(); auditService = mock<AuditService>(); organizationService = mock<OrganizationService>(); organizationService.organizations$ = of([]); @@ -49,6 +52,10 @@ describe("ExposedPasswordsReportComponent", () => { provide: PasswordRepromptService, useValue: mock<PasswordRepromptService>(), }, + { + provide: SyncService, + useValue: syncServiceMock, + }, { provide: I18nService, useValue: mock<I18nService>(), @@ -82,4 +89,8 @@ describe("ExposedPasswordsReportComponent", () => { expect(component.ciphers[1].id).toEqual(expectedIdTwo); expect(component.ciphers[1].edit).toEqual(true); }); + + it("should call fullSync method of syncService", () => { + expect(syncServiceMock.fullSync).toHaveBeenCalledWith(false); + }); }); diff --git a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.ts b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.ts index 39414487d7..cabc7bdfa1 100644 --- a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.ts +++ b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.ts @@ -5,6 +5,7 @@ import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { PasswordRepromptService } from "@bitwarden/vault"; @@ -26,8 +27,16 @@ export class ExposedPasswordsReportComponent extends CipherReportComponent imple modalService: ModalService, passwordRepromptService: PasswordRepromptService, i18nService: I18nService, + syncService: SyncService, ) { - super(cipherService, modalService, passwordRepromptService, organizationService, i18nService); + super( + cipherService, + modalService, + passwordRepromptService, + organizationService, + i18nService, + syncService, + ); } async ngOnInit() { diff --git a/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.spec.ts index 528f6306e0..80760eb5de 100644 --- a/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.spec.ts +++ b/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.spec.ts @@ -9,6 +9,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { PasswordRepromptService } from "@bitwarden/vault"; import { InactiveTwoFactorReportComponent } from "./inactive-two-factor-report.component"; @@ -18,10 +19,12 @@ describe("InactiveTwoFactorReportComponent", () => { let component: InactiveTwoFactorReportComponent; let fixture: ComponentFixture<InactiveTwoFactorReportComponent>; let organizationService: MockProxy<OrganizationService>; + let syncServiceMock: MockProxy<SyncService>; beforeEach(() => { organizationService = mock<OrganizationService>(); organizationService.organizations$ = of([]); + syncServiceMock = mock<SyncService>(); // 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 TestBed.configureTestingModule({ @@ -47,6 +50,10 @@ describe("InactiveTwoFactorReportComponent", () => { provide: PasswordRepromptService, useValue: mock<PasswordRepromptService>(), }, + { + provide: SyncService, + useValue: syncServiceMock, + }, { provide: I18nService, useValue: mock<I18nService>(), @@ -87,4 +94,8 @@ describe("InactiveTwoFactorReportComponent", () => { expect(component.ciphers[1].id).toEqual(expectedIdTwo); expect(component.ciphers[1].edit).toEqual(true); }); + + it("should call fullSync method of syncService", () => { + expect(syncServiceMock.fullSync).toHaveBeenCalledWith(false); + }); }); diff --git a/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.ts b/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.ts index 956607c8fb..5cfe2cd1a9 100644 --- a/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.ts +++ b/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.ts @@ -6,6 +6,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { PasswordRepromptService } from "@bitwarden/vault"; @@ -28,8 +29,16 @@ export class InactiveTwoFactorReportComponent extends CipherReportComponent impl private logService: LogService, passwordRepromptService: PasswordRepromptService, i18nService: I18nService, + syncService: SyncService, ) { - super(cipherService, modalService, passwordRepromptService, organizationService, i18nService); + super( + cipherService, + modalService, + passwordRepromptService, + organizationService, + i18nService, + syncService, + ); } async ngOnInit() { diff --git a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.spec.ts index 29e20c11af..9d16bbb1c6 100644 --- a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.spec.ts +++ b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.spec.ts @@ -8,6 +8,7 @@ import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { PasswordRepromptService } from "@bitwarden/vault"; import { cipherData } from "./reports-ciphers.mock"; @@ -17,10 +18,12 @@ describe("ReusedPasswordsReportComponent", () => { let component: ReusedPasswordsReportComponent; let fixture: ComponentFixture<ReusedPasswordsReportComponent>; let organizationService: MockProxy<OrganizationService>; + let syncServiceMock: MockProxy<SyncService>; beforeEach(() => { organizationService = mock<OrganizationService>(); organizationService.organizations$ = of([]); + syncServiceMock = mock<SyncService>(); // 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 TestBed.configureTestingModule({ @@ -42,6 +45,10 @@ describe("ReusedPasswordsReportComponent", () => { provide: PasswordRepromptService, useValue: mock<PasswordRepromptService>(), }, + { + provide: SyncService, + useValue: syncServiceMock, + }, { provide: I18nService, useValue: mock<I18nService>(), @@ -73,4 +80,8 @@ describe("ReusedPasswordsReportComponent", () => { expect(component.ciphers[1].id).toEqual(expectedIdTwo); expect(component.ciphers[1].edit).toEqual(true); }); + + it("should call fullSync method of syncService", () => { + expect(syncServiceMock.fullSync).toHaveBeenCalledWith(false); + }); }); diff --git a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.ts b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.ts index cbc2ea11b5..70cb2ed69b 100644 --- a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.ts +++ b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.ts @@ -4,6 +4,7 @@ import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { PasswordRepromptService } from "@bitwarden/vault"; @@ -24,8 +25,16 @@ export class ReusedPasswordsReportComponent extends CipherReportComponent implem modalService: ModalService, passwordRepromptService: PasswordRepromptService, i18nService: I18nService, + syncService: SyncService, ) { - super(cipherService, modalService, passwordRepromptService, organizationService, i18nService); + super( + cipherService, + modalService, + passwordRepromptService, + organizationService, + i18nService, + syncService, + ); } async ngOnInit() { diff --git a/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.spec.ts index 3b7c6d350f..e616d1f21e 100644 --- a/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.spec.ts +++ b/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.spec.ts @@ -8,6 +8,7 @@ import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { PasswordRepromptService } from "@bitwarden/vault"; import { cipherData } from "./reports-ciphers.mock"; @@ -17,10 +18,12 @@ describe("UnsecuredWebsitesReportComponent", () => { let component: UnsecuredWebsitesReportComponent; let fixture: ComponentFixture<UnsecuredWebsitesReportComponent>; let organizationService: MockProxy<OrganizationService>; + let syncServiceMock: MockProxy<SyncService>; beforeEach(() => { organizationService = mock<OrganizationService>(); organizationService.organizations$ = of([]); + syncServiceMock = mock<SyncService>(); // 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 TestBed.configureTestingModule({ @@ -42,6 +45,10 @@ describe("UnsecuredWebsitesReportComponent", () => { provide: PasswordRepromptService, useValue: mock<PasswordRepromptService>(), }, + { + provide: SyncService, + useValue: syncServiceMock, + }, { provide: I18nService, useValue: mock<I18nService>(), @@ -73,4 +80,8 @@ describe("UnsecuredWebsitesReportComponent", () => { expect(component.ciphers[1].id).toEqual(expectedIdTwo); expect(component.ciphers[1].edit).toEqual(true); }); + + it("should call fullSync method of syncService", () => { + expect(syncServiceMock.fullSync).toHaveBeenCalledWith(false); + }); }); diff --git a/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.ts b/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.ts index 769eb058cd..0a8023c303 100644 --- a/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.ts +++ b/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.ts @@ -4,6 +4,7 @@ import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { CipherType } from "@bitwarden/common/vault/enums"; import { PasswordRepromptService } from "@bitwarden/vault"; @@ -22,8 +23,16 @@ export class UnsecuredWebsitesReportComponent extends CipherReportComponent impl modalService: ModalService, passwordRepromptService: PasswordRepromptService, i18nService: I18nService, + syncService: SyncService, ) { - super(cipherService, modalService, passwordRepromptService, organizationService, i18nService); + super( + cipherService, + modalService, + passwordRepromptService, + organizationService, + i18nService, + syncService, + ); } async ngOnInit() { diff --git a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.spec.ts index dbc367b108..bcace60ac0 100644 --- a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.spec.ts +++ b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.spec.ts @@ -9,6 +9,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { PasswordRepromptService } from "@bitwarden/vault"; import { cipherData } from "./reports-ciphers.mock"; @@ -19,8 +20,10 @@ describe("WeakPasswordsReportComponent", () => { let fixture: ComponentFixture<WeakPasswordsReportComponent>; let passwordStrengthService: MockProxy<PasswordStrengthServiceAbstraction>; let organizationService: MockProxy<OrganizationService>; + let syncServiceMock: MockProxy<SyncService>; beforeEach(() => { + syncServiceMock = mock<SyncService>(); passwordStrengthService = mock<PasswordStrengthServiceAbstraction>(); organizationService = mock<OrganizationService>(); organizationService.organizations$ = of([]); @@ -49,6 +52,10 @@ describe("WeakPasswordsReportComponent", () => { provide: PasswordRepromptService, useValue: mock<PasswordRepromptService>(), }, + { + provide: SyncService, + useValue: syncServiceMock, + }, { provide: I18nService, useValue: mock<I18nService>(), @@ -85,4 +92,8 @@ describe("WeakPasswordsReportComponent", () => { expect(component.ciphers[1].id).toEqual(expectedIdTwo); expect(component.ciphers[1].edit).toEqual(true); }); + + it("should call fullSync method of syncService", () => { + expect(syncServiceMock.fullSync).toHaveBeenCalledWith(false); + }); }); diff --git a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.ts b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.ts index 4d179b58f3..f33e0626ab 100644 --- a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.ts +++ b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.ts @@ -6,6 +6,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { Utils } from "@bitwarden/common/platform/misc/utils"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { BadgeVariant } from "@bitwarden/components"; @@ -31,8 +32,16 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen modalService: ModalService, passwordRepromptService: PasswordRepromptService, i18nService: I18nService, + syncService: SyncService, ) { - super(cipherService, modalService, passwordRepromptService, organizationService, i18nService); + super( + cipherService, + modalService, + passwordRepromptService, + organizationService, + i18nService, + syncService, + ); } async ngOnInit() { From debfe914c24d39bec799d2d7111eec7e2a43fe34 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 3 May 2024 10:47:20 -0400 Subject: [PATCH 351/351] [deps] Platform (CL): Update tailwindcss to v3.4.3 (#8736) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 10 +++++----- package.json | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index ba0119ca63..b7372ce27b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -172,7 +172,7 @@ "sass-loader": "13.3.3", "storybook": "7.6.17", "style-loader": "3.3.4", - "tailwindcss": "3.4.1", + "tailwindcss": "3.4.3", "ts-jest": "29.1.2", "ts-loader": "9.5.1", "tsconfig-paths-webpack-plugin": "4.1.0", @@ -35401,9 +35401,9 @@ "dev": true }, "node_modules/tailwindcss": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz", - "integrity": "sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.3.tgz", + "integrity": "sha512-U7sxQk/n397Bmx4JHbJx/iSOOv5G+II3f1kpLpY2QeUv5DcPdcTsYLlusZfq1NthHS1c1cZoyFmmkex1rzke0A==", "dev": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", @@ -35414,7 +35414,7 @@ "fast-glob": "^3.3.0", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", - "jiti": "^1.19.1", + "jiti": "^1.21.0", "lilconfig": "^2.1.0", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", diff --git a/package.json b/package.json index c73ae492f5..5aca97a6f6 100644 --- a/package.json +++ b/package.json @@ -133,7 +133,7 @@ "sass-loader": "13.3.3", "storybook": "7.6.17", "style-loader": "3.3.4", - "tailwindcss": "3.4.1", + "tailwindcss": "3.4.3", "ts-jest": "29.1.2", "ts-loader": "9.5.1", "tsconfig-paths-webpack-plugin": "4.1.0",