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"] } 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/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/_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/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 7d3471e598..a64ee2b8a0 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"; @@ -111,7 +113,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 +227,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 +248,8 @@ export default class MainBackground { secureStorageService: AbstractStorageService; memoryStorageService: AbstractMemoryStorageService; memoryStorageForStateProviders: AbstractMemoryStorageService & ObservableStorageService; + largeObjectMemoryStorageForStateProviders: AbstractMemoryStorageService & + ObservableStorageService; i18nService: I18nServiceAbstraction; platformUtilsService: PlatformUtilsServiceAbstraction; logService: LogServiceAbstraction; @@ -402,34 +406,57 @@ 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("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) - ? mv3MemoryStorageCreator("stateProviders") - : new BackgroundMemoryStorageService(); + ? 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() // 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 +493,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, @@ -788,7 +813,6 @@ export default class MainBackground { this.avatarService, logoutCallback, this.billingAccountProfileStateService, - this.tokenService, ); this.eventUploadService = new EventUploadService( this.apiService, @@ -1081,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) { 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 cdd0869fc5..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.", @@ -59,7 +59,6 @@ "clipboardRead", "clipboardWrite", "idle", - "alarms", "scripting", "offscreen" ], 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/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("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/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(key: string): Promise { + return this.get(key); + } } 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; - let keyGenerationService: MockProxy; - let localStorageService: MockProxy; - let sessionStorageService: MockProxy; - let logService: MockProxy; let platformUtilsService: MockProxy; - - let cache: Record; - 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; - const mockEnc = (input: string) => Promise.resolve(new EncString("ENCRYPTED" + input)); + let logService: MockProxy; 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(); + localStorage = new FakeStorageService(); encryptService = mock(); - keyGenerationService = mock(); - localStorageService = mock(); - sessionStorageService = mock(); + platformUtilsService = mock(); + logService = mock(); 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 = new Set([]); + private cache: Record = {}; private updatesSubject = new Subject(); - private commandName = `localBackedSessionStorage_${this.partitionName}`; - private encKey = `localEncryptionKey_${this.partitionName}`; - private sessionKey = `session_${this.partitionName}`; - private cachedSession: Record = {}; - private _ports: Set = new Set([]); - private knownNullishCacheKeys: Set = 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>, + 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(key: string, options?: MemoryStorageOptions): Promise { - 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(key: string, options?: MemoryStorageOptions): Promise { - 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); } - void this.save(key, value); + this.cache[key] = value; return value as T; } @@ -114,7 +91,7 @@ export class LocalBackedSessionStorageService async save(key: string, obj: T): Promise { // 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(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 { - 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(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> { - if (Object.keys(this.cachedSession).length > 0) { - return this.cachedSession; - } - - this.cachedSession = {}; - const local = await this.localStorage.get(this.sessionKey); + private async getLocalSessionValue(encKey: SymmetricCryptoKey, key: string): Promise { + const local = await this.localStorage.get(this.sessionStorageKey(key)); if (local == null) { - return this.cachedSession; + return null; } - if (devFlagEnabled("storeSessionDecrypted")) { - return local as any as Record; + 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, key: SymmetricCryptoKey) { - if (devFlagEnabled("storeSessionDecrypted")) { - await this.setDecryptedLocalSession(session); - } else { - await this.setEncryptedLocalSession(session, key); - } - } - - @devFlag("storeSessionDecrypted") - async setDecryptedLocalSession(session: Record): Promise { - // 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, 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 { - let storedKey = await this.sessionStorage.get(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 { - if (input == null) { - await this.sessionStorage.remove(this.encKey); - } else { - await this.sessionStorage.save(this.encKey, input); - } - } - - private compareValues(value1: T, value2: T): boolean { - if (value1 == null && value2 == null) { - return true; + private async updateLocalSessionValue(key: string, value: unknown): Promise { + 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) { - 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(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/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, deriveDefinition: DeriveDefinition, dependencies: TDeps, + storageLocation: [string, AbstractStorageService & ObservableStorageService], ): DerivedState { + 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, deriveDefinition: DeriveDefinition, 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(); + const portName = "testPort"; beforeEach(() => { mockPorts(); parentState$ = new Subject(); 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( _parentState$: Observable, deriveDefinition: DeriveDefinition, _dependencies: TDeps, + storageLocation: [string, AbstractStorageService & ObservableStorageService], ): DerivedState { - 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; let memoryStorage: FakeStorageService; + const portName = "testPort"; const ngZone = mock(); 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 implements DerivedState { constructor( private deriveDefinition: DeriveDefinition, private memoryStorage: AbstractStorageService & ObservableStorageService, + private portName: string, private ngZone: NgZone, ) { this.storageKey = deriveDefinition.storageKey; @@ -88,7 +89,7 @@ export class ForegroundDerivedState implements DerivedState { 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, + ): [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( + "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/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 @@ Bitwarden Passwort-Manager - At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. + Zu Hause, am Arbeitsplatz oder unterwegs schützt Bitwarden einfach alle deine Passwörter, Passkeys und vertraulichen Informationen. - Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! + 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! - At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. + Zu Hause, am Arbeitsplatz oder unterwegs schützt Bitwarden einfach alle deine Passwörter, Passkeys und vertraulichen Informationen. Synchronisiere und greife auf deinen Tresor von unterschiedlichen Geräten aus zu 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 @@ Bitwarden Password Manager - At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. + 집에서도, 직장에서도, 이동 중에도 Bitwarden은 비밀번호, 패스키, 민감 정보를 쉽게 보호합니다. 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 - At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. + 집에서도, 직장에서도, 이동 중에도 Bitwarden은 비밀번호, 패스키, 민감 정보를 쉽게 보호합니다. 여러 기기에서 보관함에 접근하고 동기화할 수 있습니다. 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 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden Password Manager + Menedżer Haseł Bitwarden - At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. + W domu, w pracy, lub w ruchu, Bitwarden z łatwością zabezpiecza wszystkie Twoje hasła, passkeys i poufne informacje. - Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! + 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! - At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. + W domu, w pracy, lub w ruchu, Bitwarden z łatwością zabezpiecza wszystkie Twoje hasła, passkeys i poufne informacje. Synchronizacja i dostęp do sejfu z różnych urządzeń 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 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden Password Manager + Gerenciador de Senhas Bitwarden - At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. + Em casa, no trabalho, ou em qualquer lugar, o Bitwarden protege facilmente todas as suas senhas, senhas e informações confidenciais. 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 - At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. + Em casa, no trabalho, ou em qualquer lugar, o Bitwarden protege facilmente todas as suas senhas, senhas e informações confidenciais. Sincronize e acesse o seu cofre através de múltiplos dispositivos 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 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden Password Manager + Bitwarden 密码管理器 - At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. + 无论是在家里、工作中还是在外出时,Bitwarden 都可以轻松地保护您的所有密码、通行密钥和敏感信息。 - Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! + 被 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! - +Bitwarden 保护的不仅仅是密码 +Bitwarden 的端对端加密凭据管理解决方案使组织能够保护所有内容,包括开发人员机密和通行密钥体验。访问 Bitwarden.com 了解更多关于Bitwarden Secrets Manager 和 Bitwarden Passwordless.dev 的信息! - At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. + 无论是在家里、工作中还是在外出时,Bitwarden 都可以轻松地保护您的所有密码、通行密钥和敏感信息。 从多台设备同步和访问密码库 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/cli/src/bw.ts b/apps/cli/src/bw.ts index c3c4042adf..58329128b8 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, @@ -633,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/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", 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/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": "疑難排解" }, diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 0ed3524f59..821a85f075 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/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. (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/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 }} -
-
- - -
-
+ + + {{ "turnOn" | i18n }} + 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, + }); + } } } 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": "选择集合项目" } } 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 @@ }} -
-
- - -
-
+ + + {{ "turnOn" | i18n }} + diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 9d311d34af..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({ @@ -1047,7 +1046,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: DerivedStateProvider, useClass: DefaultDerivedStateProvider, - deps: [OBSERVABLE_MEMORY_STORAGE], + deps: [StorageServiceProvider], }), safeProvider({ provide: StateProvider, 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/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, dependencies: TDeps, ): DerivedState { - let result = this.states.get(deriveDefinition.buildCacheKey()) as DerivedState; + let result = this.states.get(deriveDefinition.buildCacheKey("memory")) as DerivedState; 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/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/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>; - postEventsCollect: (request: EventRequest[]) => Promise; + + /** + * 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; deleteSsoUser: (organizationId: string) => Promise; getSsoUserIdentifier: () => Promise; 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; - - /** Gets the active or passed in user's security stamp */ - getSecurityStamp: (userId?: UserId) => Promise; - - /** Sets the security stamp for the active or passed in user */ - setSecurityStamp: (securityStamp: string, userId?: UserId) => Promise; } 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 { - 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 { - 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, 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( clearOn: [], // Manually handled }, ); - -export const SECURITY_STAMP_MEMORY = new UserKeyDefinition(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 { * Sets the user's Pin, encrypted by the user key */ setProtectedPin: (value: string, options?: StorageOptions) => Promise; + getSecurityStamp: (options?: StorageOptions) => Promise; + setSecurityStamp: (value: string, options?: StorageOptions) => Promise; getUserId: (options?: StorageOptions) => Promise; getVaultTimeout: (options?: StorageOptions) => Promise; setVaultTimeout: (value: number, options?: StorageOptions) => Promise; 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>; + let lazy: Lazy>; + + 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; + + beforeEach(() => { + syncFactory.mockReturnValue(42); + lazy = new Lazy(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 { + 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; + } +} 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 { + 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) { 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 { + return ( + await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) + )?.tokens?.securityStamp; + } + + async setSecurityStamp(value: string, options?: StorageOptions): Promise { + 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 { return ( await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) 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> = {}; - constructor(protected memoryStorage: AbstractStorageService & ObservableStorageService) {} + constructor(protected storageServiceProvider: StorageServiceProvider) {} get( parentState$: Observable, deriveDefinition: DeriveDefinition, dependencies: TDeps, ): DerivedState { - 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; } - 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, deriveDefinition: DeriveDefinition, dependencies: TDeps, + storageLocation: [string, AbstractStorageService & ObservableStorageService], ): DerivedState { return new DefaultDerivedState( 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", +}); 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 { - const authHeader = await this.getActiveBearerToken(); + async postEventsCollect(request: EventRequest[], userId?: UserId): Promise { + 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. 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, private billingAccountProfileStateService: BillingAccountProfileStateService, - private tokenService: TokenService, ) {} async getLastSync(): Promise { @@ -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( 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; +} 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",