1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-12-05 09:14:28 +01:00

Merge branch 'main' into feat/engines

This commit is contained in:
Kazuya Fujimori 2025-08-08 22:16:48 +09:00 committed by GitHub
commit d20beaa1fc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
689 changed files with 11794 additions and 5331 deletions

6
.github/CODEOWNERS vendored
View File

@ -96,6 +96,12 @@ libs/logging @bitwarden/team-platform-dev
libs/storage-test-utils @bitwarden/team-platform-dev
libs/messaging @bitwarden/team-platform-dev
libs/messaging-internal @bitwarden/team-platform-dev
libs/serialization @bitwarden/team-platform-dev
libs/guid @bitwarden/team-platform-dev
libs/client-type @bitwarden/team-platform-dev
libs/core-test-utils @bitwarden/team-platform-dev
libs/state @bitwarden/team-platform-dev
libs/state-test-utils @bitwarden/team-platform-dev
# Web utils used across app and connectors
apps/web/src/utils/ @bitwarden/team-platform-dev
# Web core and shared files

View File

@ -71,6 +71,7 @@ jobs:
- name: Get Node Version
id: retrieve-node-version
working-directory: ./
run: |
NODE_NVMRC=$(cat .nvmrc)
NODE_VERSION=${NODE_NVMRC/v/''}
@ -104,7 +105,7 @@ jobs:
env:
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
_WIN_PKG_FETCH_VERSION: 20.11.1
_WIN_PKG_FETCH_VERSION: 22.15.1
_WIN_PKG_VERSION: 3.5
permissions:
contents: read
@ -283,7 +284,7 @@ jobs:
env:
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
_WIN_PKG_FETCH_VERSION: 20.11.1
_WIN_PKG_FETCH_VERSION: 22.15.1
_WIN_PKG_VERSION: 3.5
steps:
- name: Check out repo

View File

@ -1,6 +1,6 @@
{
"name": "@bitwarden/browser",
"version": "2025.7.0",
"version": "2025.7.1",
"scripts": {
"build": "npm run build:chrome",
"build:chrome": "cross-env BROWSER=chrome MANIFEST_VERSION=3 NODE_OPTIONS=\"--max-old-space-size=8192\" webpack",

View File

@ -3657,25 +3657,6 @@
"thisRequestIsNoLongerValid": {
"message": "This request is no longer valid."
},
"areYouTryingToAccessYourAccount": {
"message": "Are you trying to access your account?"
},
"logInConfirmedForEmailOnDevice": {
"message": "Login confirmed for $EMAIL$ on $DEVICE$",
"placeholders": {
"email": {
"content": "$1",
"example": "name@example.com"
},
"device": {
"content": "$2",
"example": "iOS"
}
}
},
"youDeniedALogInAttemptFromAnotherDevice": {
"message": "You denied a login attempt from another device. If this really was you, try to log in with the device again."
},
"loginRequestHasAlreadyExpired": {
"message": "Login request has already expired."
},
@ -5592,5 +5573,11 @@
"wasmNotSupported": {
"message": "WebAssembly is not supported on your browser or is not enabled. WebAssembly is required to use the Bitwarden app.",
"description": "'WebAssembly' is a technical term and should not be translated."
},
"showMore": {
"message": "Show more"
},
"showLess": {
"message": "Show less"
}
}

View File

@ -548,7 +548,7 @@
"message": "वॉल्ट खोजे"
},
"resetSearch": {
"message": "Reset search"
"message": "खोज रीसेट करें"
},
"edit": {
"message": "संपादन करें"

View File

@ -338,7 +338,7 @@
"message": "Przejść do bitwarden.com?"
},
"bitwardenForBusiness": {
"message": "Bitwarden dla biznesu"
"message": "Bitwarden dla firm"
},
"bitwardenAuthenticator": {
"message": "Bitwarden Authenticator"
@ -347,16 +347,16 @@
"message": "Bitwarden Authenticator umożliwia przechowywanie kluczy uwierzytelniających i generowanie kodów TOTP dla weryfikacji dwustopniowej. Dowiedz się wiecej na bitwarden.com"
},
"bitwardenSecretsManager": {
"message": "Menedżer sekretów Bitwarden"
"message": "Bitwarden Secrets Manager"
},
"continueToSecretsManagerPageDesc": {
"message": "Bezpiecznie przechowuj, zarządzaj i udostępniaj sekrety programistów z Menedżerem sekretów Bitwarden. Dowiedz się więcej na stronie bitwarden.com."
"message": "Bezpiecznie przechowuj, zarządzaj i udostępniaj sekrety deweloperów za pomocą usługi Bitwarden Secrets Manager. Dowiedz się więcej na stronie bitwarden.com."
},
"passwordlessDotDev": {
"message": "Passwordless.dev"
},
"continueToPasswordlessDotDevPageDesc": {
"message": "Twórz przyjemne i bezpieczne doświadczenia z logowaniem wolne od tradycyjnych haseł za pomocą Passwordless.dev. Dowiedz się więcej na stronie bitwarden.com."
"message": "Loguj się szybko i bezpiecznie bez tradycyjnych haseł za pomocą usługi Passwordless.dev. Dowiedz się więcej na stronie bitwarden.com."
},
"freeBitwardenFamilies": {
"message": "Darmowy plan rodzinny"
@ -541,14 +541,14 @@
"description": "Label for the avoid ambiguous characters checkbox."
},
"generatorPolicyInEffect": {
"message": "Wymagania polityki przedsiębiorstwa zostały użyte do ustawienia opcji generatora.",
"message": "Zasady organizacji zostały zastosowane do opcji generatora.",
"description": "Indicates that a policy limits the credential generator screen."
},
"searchVault": {
"message": "Szukaj w sejfie"
},
"resetSearch": {
"message": "Reset search"
"message": "Zresetuj wyszukiwanie"
},
"edit": {
"message": "Edytuj"
@ -650,7 +650,7 @@
"message": "Blokowanie sejfu"
},
"otherOptions": {
"message": "Pozostałe opcje"
"message": "Inne opcje"
},
"rateExtension": {
"message": "Oceń rozszerzenie"
@ -805,7 +805,7 @@
"message": "Zalogowano!"
},
"youSuccessfullyLoggedIn": {
"message": "Zalogowałeś się pomyślnie"
"message": "Zalogowano"
},
"youMayCloseThisWindow": {
"message": "Możesz zamknąć to okno"
@ -833,7 +833,7 @@
}
},
"autofillError": {
"message": "Nie można zastosować autouzupełnienia na tej stronie. Skopiuj i wklej informacje ręcznie."
"message": "Nie można uzupełnić elementu na tej stronie internetowej. Skopiuj i wklej informacje ręcznie."
},
"totpCaptureError": {
"message": "Nie można zeskanować kodu QR z obecnej strony"
@ -1028,7 +1028,7 @@
"message": "Proponuj dodanie elementu, jeśli nie ma go w sejfie. Dotyczy wszystkich zalogowanych kont."
},
"showCardsInVaultViewV2": {
"message": "Pokazuj zawsze karty w sugestiach autouzupełniania"
"message": "Zawsze pokazuj karty w sugestiach autouzupełniania"
},
"showCardsCurrentTab": {
"message": "Pokaż karty na stronie głównej"
@ -1037,7 +1037,7 @@
"message": "Wyświetla karty na głównej karcie sejfu."
},
"showIdentitiesInVaultViewV2": {
"message": "Pokazuj zawsze tożsamości w sugestiach autouzupełniania"
"message": "Zawsze pokazuj tożsamości w sugestiach autouzupełniania"
},
"showIdentitiesCurrentTab": {
"message": "Pokaż tożsamości na stronie głównej"
@ -1066,7 +1066,7 @@
"message": "Zapisz"
},
"notificationViewAria": {
"message": "Wyświetl $ITEMNAME$, otworzy się w nowym oknie",
"message": "Pokaż $ITEMNAME$, otwiera się w nowym oknie",
"placeholders": {
"itemName": {
"content": "$1"
@ -1177,7 +1177,7 @@
"description": "Detailed error message shown when saving login details fails."
},
"changePasswordWarning": {
"message": "Po zmianie hasła musisz się zalogować przy użyciu nowego hasła. Aktywne sesje na innych urządzeniach zostaną wylogowane w ciągu jednej godziny."
"message": "Po zmianie hasła zaloguj się za pomocą nowego hasła. Aktywne sesje na innych urządzeniach zostaną wylogowane w ciągu jednej godziny."
},
"accountRecoveryUpdateMasterPasswordSubtitle": {
"message": "Zmień hasło główne, aby zakończyć odzyskiwanie konta."
@ -1204,7 +1204,7 @@
"message": "Zaktualizuj"
},
"notificationUnlockDesc": {
"message": "Odblokuj swój sejf Bitwarden, aby ukończyć żądanie autouzupełniania."
"message": "Odblokuj sejf Bitwarden, aby uzupełnić dane."
},
"notificationUnlock": {
"message": "Odblokuj"
@ -1273,7 +1273,7 @@
"message": "Rodzaj eksportu"
},
"accountRestricted": {
"message": "Konto ograniczone"
"message": "Konto zostało ograniczone"
},
"filePasswordAndConfirmFilePasswordDoNotMatch": {
"message": "Hasła pliku nie pasują do siebie."
@ -1305,7 +1305,7 @@
"message": "Udostępnione"
},
"bitwardenForBusinessPageDesc": {
"message": "Bitwarden dla biznesu pozwala na udostępnianie zawartości sejfu innym użytkownikom za pośrednictwem organizacji. Dowiedz się więcej na stronie bitwarden.com."
"message": "Bitwarden dla firm pozwala na udostępnianie zawartości sejfu innym użytkownikom za pośrednictwem organizacji. Dowiedz się więcej na stronie bitwarden.com."
},
"moveToOrganization": {
"message": "Przenieś do organizacji"
@ -1459,7 +1459,7 @@
"message": "Automatycznie kopiuje kod TOTP do schowka podczas autouzupełniania."
},
"enableAutoBiometricsPrompt": {
"message": "Poproś o dane biometryczne przy uruchomieniu"
"message": "Wymagaj odblokowania biometrią po uruchomieniu przeglądarki"
},
"premiumRequired": {
"message": "Konto premium jest wymagane"
@ -1983,7 +1983,7 @@
"message": "Kolekcje"
},
"nCollections": {
"message": "Kolekcje ($COUNT$)",
"message": "W $COUNT$ kolekcjach",
"placeholders": {
"count": {
"content": "$1",
@ -2069,7 +2069,7 @@
"description": "Default URI match detection for autofill."
},
"toggleOptions": {
"message": "Zmień opcje"
"message": "Przełącz opcje"
},
"toggleCurrentUris": {
"message": "Przełącz obecny URI",
@ -2099,7 +2099,7 @@
"message": "Brak zawartości do pokazania"
},
"nothingGeneratedRecently": {
"message": "Nic nie zostało wygenerowane przez ciebie w ostatnim czasie"
"message": "Nic nie zostało wygenerowane w ostatnim czasie"
},
"remove": {
"message": "Usuń"
@ -2150,7 +2150,7 @@
"message": "Hasło główne jest słabe"
},
"weakMasterPasswordDesc": {
"message": "Wybrane przez Ciebie hasło główne jest słabe. Powinieneś użyć silniejszego hasła (lub frazy), aby właściwie chronić swoje konto Bitwarden. Czy na pewno chcesz użyć tego hasła głównego?"
"message": "Użyj silniejszego hasła, aby odpowiednio chronić konto Bitwarden. Czy na pewno chcesz użyć tego hasła głównego?"
},
"pin": {
"message": "Kod PIN",
@ -2190,7 +2190,7 @@
"message": "Oczekiwanie na potwierdzenie z aplikacji desktopowej"
},
"awaitDesktopDesc": {
"message": "Włącz dane biometryczne w aplikacji desktopowej Bitwarden, aby włączyć tę samą funkcję w przeglądarce."
"message": "Włącz najpierw biometrię w aplikacji desktopowej Bitwarden, aby skonfigurować dane biometryczne w przeglądarce."
},
"lockWithMasterPassOnRestart": {
"message": "Zablokuj hasłem głównym po uruchomieniu przeglądarki"
@ -2226,7 +2226,7 @@
"message": "Użyj tej nazwy użytkownika"
},
"securePasswordGenerated": {
"message": "Wygenerowane bezpieczne hasło! Nie zapomnij również zaktualizować hasła na stronie."
"message": "Bezpieczne hasło zostało wygenerowane! Nie zapomnij zaktualizować hasła na stronie internetowej."
},
"useGeneratorHelpTextPartOne": {
"message": "Użyj generatora",
@ -2371,7 +2371,7 @@
"message": "Anuluj subskrypcję"
},
"atAnyTime": {
"message": "w każdej chwili."
"message": "w dowolnym momencie."
},
"byContinuingYouAgreeToThe": {
"message": "Kontynuując, akceptujesz"
@ -2410,7 +2410,7 @@
"message": "Weryfikacja synchronizacji z aplikacją desktopową"
},
"desktopIntegrationVerificationText": {
"message": "Zweryfikuj aplikację desktopową z odciskiem klucza: "
"message": "Zweryfikuj aplikację desktopową z identyfikatorem: "
},
"desktopIntegrationDisabledTitle": {
"message": "Połączenie z przeglądarką jest wyłączone"
@ -2452,7 +2452,7 @@
"message": "Biometria jest wyłączona"
},
"biometricsNotEnabledDesc": {
"message": "Aby włączyć dane biometryczne w przeglądarce, musisz włączyć tę samą funkcję w ustawianiach aplikacji desktopowej."
"message": "Aby skonfigurować dane biometryczne w przeglądarce, włącz najpierw biometrię w aplikacji desktopowej."
},
"biometricsNotSupportedTitle": {
"message": "Biometria nie jest obsługiwana"
@ -2622,7 +2622,7 @@
"message": "Zmień zagrożone hasła szybciej"
},
"changeAtRiskPasswordsFasterDesc": {
"message": "Zaktualizuj swoje ustawienia, aby szybko autouzupełniać hasła i generować nowe"
"message": "Zaktualizuj ustawienia, aby szybko uzupełniać hasła i generować nowe."
},
"reviewAtRiskLogins": {
"message": "Sprawdź zagrożone dane logowania"
@ -3431,7 +3431,7 @@
"message": "Unikalny identyfikator konta"
},
"fingerprintMatchInfo": {
"message": "Upewnij się, że sejf jest odblokowany, a identyfikator konta pasuje do drugiego urządzenia."
"message": "Upewnij się, że sejf jest odblokowany, a identyfikator pasuje do drugiego urządzenia."
},
"resendNotification": {
"message": "Wyślij ponownie powiadomienie"
@ -3449,7 +3449,7 @@
"message": "aplikacji internetowej"
},
"notificationSentDevicePart2": {
"message": "Upewnij się, że fraza odcisku palca zgadza się z tą poniżej, zanim zatwierdzisz."
"message": "Upewnij się, że identyfikator jest zgodny."
},
"aNotificationWasSentToYourDevice": {
"message": "Powiadomienie zostało wysłane na urządzenie"
@ -3486,7 +3486,7 @@
"message": "Urządzenie"
},
"loginStatus": {
"message": "Status zalogowania"
"message": "Status logowania"
},
"masterPasswordChanged": {
"message": "Hasło główne zostało zapisane"
@ -3537,7 +3537,7 @@
}
},
"autofillSelectInfoWithoutCommand": {
"message": "Wybierz element z tego ekranu lub zobacz inne opcje w ustawieniach."
"message": "Wybierz element lub zobacz inne opcje w ustawieniach."
},
"gotIt": {
"message": "Ok"
@ -3582,7 +3582,7 @@
"message": "Otwiera w nowym oknie"
},
"rememberThisDeviceToMakeFutureLoginsSeamless": {
"message": "Zapamiętaj to urządzenie, aby przyszłe logowania były bezproblemowe"
"message": "Zapamiętaj urządzenie, aby przyszłe logowania były bezproblemowe"
},
"manageDevices": {
"message": "Zarządzaj urządzeniami"
@ -3808,7 +3808,7 @@
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendsBodyNoItems": {
"message": "Udostępniaj pliki i teksty każdemu, na dowolnej platformie. Informacje będę szyfrowane end-to-end, zapewniając poufność.",
"message": "Udostępniaj pliki i teksty każdemu na dowolnej platformie. Informacje będę szyfrowane end-to-end, zapewniając poufność.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"inputRequired": {
@ -3946,7 +3946,7 @@
"message": "Przełącz nawigację boczną"
},
"skipToContent": {
"message": "Przejdź do treści"
"message": "Przejdź do zawartości"
},
"bitwardenOverlayButton": {
"message": "Przycisk menu autouzupełniania Bitwarden",
@ -4751,7 +4751,7 @@
"message": "Pobierz Bitwarden"
},
"downloadBitwardenOnAllDevices": {
"message": "Pobierz Bitwarden na wszystkie urządzenia"
"message": "Bitwarden na inne urządzenia"
},
"getTheMobileApp": {
"message": "Pobierz aplikację mobilną"
@ -4763,7 +4763,7 @@
"message": "Pobierz aplikację desktopową"
},
"getTheDesktopAppDesc": {
"message": "Uzyskaj dostęp do sejfu bez przeglądarki, a następnie ustaw odblokowanie biometryczne, aby przyspieszyć odblokowanie zarówno w aplikacji desktopowej, jak i w rozszerzeniu przeglądarki."
"message": "Uzyskaj dostęp do sejfu bez przeglądarki. Skonfiguruj biometrię, aby przyśpieszyć odblokowywanie aplikacji."
},
"downloadFromBitwardenNow": {
"message": "Pobierz z bitwarden.com"
@ -4889,7 +4889,7 @@
"message": "Karta wygasła"
},
"cardExpiredMessage": {
"message": "Jeśli ją wznowiłeś, zaktualizuj informacje o karcie"
"message": "Jeśli karta została odnowiona, zaktualizuj informacje o niej"
},
"cardDetails": {
"message": "Szczegóły karty"
@ -5078,7 +5078,7 @@
"message": "Przypisano kolekcje"
},
"nothingSelected": {
"message": "Nie zaznaczyłeś żadnych elementów."
"message": "Nie zaznaczono żadnych elementów."
},
"itemsMovedToOrg": {
"message": "Elementy zostały przeniesione do organizacji $ORGNAME$",
@ -5143,7 +5143,7 @@
"message": "Domyślny systemu"
},
"enterprisePolicyRequirementsApplied": {
"message": "Zastosowano wymagania zasady organizacji"
"message": "Wymagania zasady organizacji zostały zastosowane"
},
"sshPrivateKey": {
"message": "Klucz prywatny"
@ -5170,7 +5170,7 @@
"message": "RSA 4096-bit"
},
"retry": {
"message": "Powtórz"
"message": "Spróbuj ponownie"
},
"vaultCustomTimeoutMinimum": {
"message": "Minimalny niestandardowy czas to 1 minuta."
@ -5458,7 +5458,7 @@
"message": "Zaktualizuj aplikację desktopową"
},
"updateDesktopAppOrDisableFingerprintDialogMessage": {
"message": "Aby używać odblokowywania biometrycznego, zaktualizuj aplikację na komputerze lub wyłącz odblokowywanie odciskiem palca w ustawieniach aplikacji na komputerze."
"message": "Aby używać biometrii, zaktualizuj aplikację desktopową."
},
"changeAtRiskPassword": {
"message": "Zmień zagrożone hasło"
@ -5587,7 +5587,7 @@
"description": "Aria label for the body content of the generator nudge"
},
"noPermissionsViewPage": {
"message": "Nie masz uprawnień do przeglądania tej strony. Spróbuj zalogować się na inne konto."
"message": "Nie masz uprawnień do przeglądania tej strony. Zaloguj się na inne konto."
},
"wasmNotSupported": {
"message": "WebAssembly nie jest obsługiwany w przeglądarce lub jest wyłączony. WebAssembly jest wymagany do korzystania z aplikacji Bitwarden.",

View File

@ -548,7 +548,7 @@
"message": "Претражи сеф"
},
"resetSearch": {
"message": "Reset search"
"message": "Ресетовати претрагу"
},
"edit": {
"message": "Уреди"
@ -1833,7 +1833,7 @@
"message": "Сигурносни код"
},
"cardNumber": {
"message": "card number"
"message": "број картице"
},
"ex": {
"message": "нпр."
@ -3467,7 +3467,7 @@
"message": "Захтев је послат"
},
"loginRequestApprovedForEmailOnDevice": {
"message": "Login request approved for $EMAIL$ on $DEVICE$",
"message": "Захтев за пријаву одобрен за $EMAIL$ на $DEVICE$",
"placeholders": {
"email": {
"content": "$1",
@ -3480,13 +3480,13 @@
}
},
"youDeniedLoginAttemptFromAnotherDevice": {
"message": "You denied a login attempt from another device. If this was you, try to log in with the device again."
"message": "Одбили сте покушај пријаве са другог уређаја. Ако сте то били ви, покушајте поново да се пријавите помоћу уређаја."
},
"device": {
"message": "Device"
"message": "Уређај"
},
"loginStatus": {
"message": "Login status"
"message": "Статус пријаве"
},
"masterPasswordChanged": {
"message": "Главна лозинка сачувана"
@ -3585,28 +3585,28 @@
"message": "Запамтити овај уређај да би будуће пријаве биле беспрекорне"
},
"manageDevices": {
"message": "Manage devices"
"message": "Управљање уређајима"
},
"currentSession": {
"message": "Current session"
"message": "Тренутна сесија"
},
"mobile": {
"message": "Mobile",
"message": "Мобилни",
"description": "Mobile app"
},
"extension": {
"message": "Extension",
"message": "Додатак",
"description": "Browser extension/addon"
},
"desktop": {
"message": "Desktop",
"message": "Десктоп",
"description": "Desktop app"
},
"webVault": {
"message": "Web vault"
"message": "Интернет Сеф"
},
"webApp": {
"message": "Web app"
"message": "Веб апликација"
},
"cli": {
"message": "CLI"
@ -3616,22 +3616,22 @@
"description": "Software Development Kit"
},
"requestPending": {
"message": "Request pending"
"message": "Захтев је на чекању"
},
"firstLogin": {
"message": "First login"
"message": "Прва пријава"
},
"trusted": {
"message": "Trusted"
"message": "Поуздан"
},
"needsApproval": {
"message": "Needs approval"
"message": "Потребно је одобрење"
},
"devices": {
"message": "Devices"
"message": "Уређаји"
},
"accessAttemptBy": {
"message": "Access attempt by $EMAIL$",
"message": "Покушај приступа са $EMAIL$",
"placeholders": {
"email": {
"content": "$1",
@ -3640,28 +3640,28 @@
}
},
"confirmAccess": {
"message": "Confirm access"
"message": "Потврди приступ"
},
"denyAccess": {
"message": "Deny access"
"message": "Одбиј приступ"
},
"time": {
"message": "Time"
"message": "Време"
},
"deviceType": {
"message": "Device Type"
"message": "Тип уређаја"
},
"loginRequest": {
"message": "Login request"
"message": "Захтев за пријаву"
},
"thisRequestIsNoLongerValid": {
"message": "This request is no longer valid."
"message": "Овај захтев више није важећи."
},
"areYouTryingToAccessYourAccount": {
"message": "Are you trying to access your account?"
"message": "Да ли покушавате да приступите вашем налогу?"
},
"logInConfirmedForEmailOnDevice": {
"message": "Login confirmed for $EMAIL$ on $DEVICE$",
"message": "Пријава потврђена за $EMAIL$ на $DEVICE$",
"placeholders": {
"email": {
"content": "$1",
@ -3674,16 +3674,16 @@
}
},
"youDeniedALogInAttemptFromAnotherDevice": {
"message": "You denied a login attempt from another device. If this really was you, try to log in with the device again."
"message": "Одбили сте покушај пријаве са другог уређаја. Ако сте то заиста били ви, покушајте поново да се пријавите помоћу уређаја."
},
"loginRequestHasAlreadyExpired": {
"message": "Login request has already expired."
"message": "Захтев за пријаву је већ истекао."
},
"justNow": {
"message": "Just now"
"message": "Управо сада"
},
"requestedXMinutesAgo": {
"message": "Requested $MINUTES$ minutes ago",
"message": "Затражено пре $MINUTES$ минута",
"placeholders": {
"minutes": {
"content": "$1",
@ -4601,7 +4601,7 @@
}
},
"copyFieldCipherName": {
"message": "Copy $FIELD$, $CIPHERNAME$",
"message": "Копирај $FIELD$, $CIPHERNAME$",
"description": "Title for a button that copies a field value to the clipboard.",
"placeholders": {
"field": {

View File

@ -462,13 +462,13 @@
"message": "Parola oluştur"
},
"generatePassphrase": {
"message": "Parola üret"
"message": "Parola cümlesi üret"
},
"passwordGenerated": {
"message": "Parola üretildi"
},
"passphraseGenerated": {
"message": "Parola ifadesi oluşturuldu"
"message": "Parola cümlesi üretildi"
},
"usernameGenerated": {
"message": "Kullanıcı adı üretildi"
@ -572,7 +572,7 @@
"message": "Kimlik doğrulama sırrı"
},
"passphrase": {
"message": "Uzun söz"
"message": "Parola cümlesi"
},
"favorite": {
"message": "Favori"
@ -2220,7 +2220,7 @@
"message": "Bu parolayı kullan"
},
"useThisPassphrase": {
"message": "Bu parola ifadesini kullanın"
"message": "Bu parola cümlesini kullan"
},
"useThisUsername": {
"message": "Bu kullanıcı adını kullan"
@ -3183,7 +3183,7 @@
}
},
"passphraseNumWordsRecommendationHint": {
"message": "Güçlü bir parola ifadesi oluşturmak için $RECOMMENDED$ veya daha fazla kelime kullanın.",
"message": " Güçlü bir parola cümlesi üretmek için en az $RECOMMENDED$ kelime kullanın.",
"description": "Appended to `spinboxBoundariesHint` to recommend a number of words to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).",
"placeholders": {
"recommended": {

View File

@ -548,7 +548,7 @@
"message": "Пошук"
},
"resetSearch": {
"message": "Reset search"
"message": "Скинути пошук"
},
"edit": {
"message": "Змінити"
@ -1833,7 +1833,7 @@
"message": "Код безпеки"
},
"cardNumber": {
"message": "card number"
"message": "номер картки"
},
"ex": {
"message": "зразок"
@ -3467,7 +3467,7 @@
"message": "Запит надіслано"
},
"loginRequestApprovedForEmailOnDevice": {
"message": "Login request approved for $EMAIL$ on $DEVICE$",
"message": "Запит входу підтверджено для $EMAIL$ на $DEVICE$",
"placeholders": {
"email": {
"content": "$1",
@ -3480,13 +3480,13 @@
}
},
"youDeniedLoginAttemptFromAnotherDevice": {
"message": "You denied a login attempt from another device. If this was you, try to log in with the device again."
"message": "Ви відхилили спробу входу з іншого пристрою. Якщо це були ви, спробуйте ввійти з пристроєм знову."
},
"device": {
"message": "Device"
"message": "Пристрій"
},
"loginStatus": {
"message": "Login status"
"message": "Стан входу в систему"
},
"masterPasswordChanged": {
"message": "Головний пароль збережено"
@ -3585,28 +3585,28 @@
"message": "Запам'ятайте цей пристрій, щоб спростити майбутні входи в систему"
},
"manageDevices": {
"message": "Manage devices"
"message": "Керувати пристроями"
},
"currentSession": {
"message": "Current session"
"message": "Поточний сеанс"
},
"mobile": {
"message": "Mobile",
"message": "Мобільний",
"description": "Mobile app"
},
"extension": {
"message": "Extension",
"message": "Розширення",
"description": "Browser extension/addon"
},
"desktop": {
"message": "Desktop",
"message": "Комп'ютер",
"description": "Desktop app"
},
"webVault": {
"message": "Web vault"
"message": "Вебсховище"
},
"webApp": {
"message": "Web app"
"message": "Вебпрограма"
},
"cli": {
"message": "CLI"
@ -3616,22 +3616,22 @@
"description": "Software Development Kit"
},
"requestPending": {
"message": "Request pending"
"message": "Запит в очікуванні"
},
"firstLogin": {
"message": "First login"
"message": "Перший вхід"
},
"trusted": {
"message": "Trusted"
"message": "Надійний"
},
"needsApproval": {
"message": "Needs approval"
"message": "Потребує підтвердження"
},
"devices": {
"message": "Devices"
"message": "Пристрої"
},
"accessAttemptBy": {
"message": "Access attempt by $EMAIL$",
"message": "Спроба доступу з $EMAIL$",
"placeholders": {
"email": {
"content": "$1",
@ -3640,28 +3640,28 @@
}
},
"confirmAccess": {
"message": "Confirm access"
"message": "Підтвердити доступ"
},
"denyAccess": {
"message": "Deny access"
"message": "Заборонити доступ"
},
"time": {
"message": "Time"
"message": "Час"
},
"deviceType": {
"message": "Device Type"
"message": "Тип пристрою"
},
"loginRequest": {
"message": "Login request"
"message": "Запит входу"
},
"thisRequestIsNoLongerValid": {
"message": "This request is no longer valid."
"message": "Цей запит більше недійсний."
},
"areYouTryingToAccessYourAccount": {
"message": "Are you trying to access your account?"
"message": "Ви намагаєтесь отримати доступ до свого облікового запису?"
},
"logInConfirmedForEmailOnDevice": {
"message": "Login confirmed for $EMAIL$ on $DEVICE$",
"message": "Підтверджено вхід для $EMAIL$ на $DEVICE$",
"placeholders": {
"email": {
"content": "$1",
@ -3674,16 +3674,16 @@
}
},
"youDeniedALogInAttemptFromAnotherDevice": {
"message": "You denied a login attempt from another device. If this really was you, try to log in with the device again."
"message": "Ви відхилили спробу входу з іншого пристрою. Якщо це були дійсно ви, спробуйте ввійти з пристроєм знову."
},
"loginRequestHasAlreadyExpired": {
"message": "Login request has already expired."
"message": "Термін дії запиту на вхід завершився."
},
"justNow": {
"message": "Just now"
"message": "Щойно"
},
"requestedXMinutesAgo": {
"message": "Requested $MINUTES$ minutes ago",
"message": "Запитано $MINUTES$ хвилин тому",
"placeholders": {
"minutes": {
"content": "$1",
@ -4601,7 +4601,7 @@
}
},
"copyFieldCipherName": {
"message": "Copy $FIELD$, $CIPHERNAME$",
"message": "Копіювати $FIELD$, $CIPHERNAME$",
"description": "Title for a button that copies a field value to the clipboard.",
"placeholders": {
"field": {

View File

@ -1,4 +1,4 @@
<div class="tw-mr-[5px] tw-mt-1">
<div class="tw-me-2 tw-mt-1">
<button
*ngIf="currentAccount$ | async as currentAccount; else defaultButton"
type="button"

View File

@ -5,7 +5,6 @@ import { mock } from "jest-mock-extended";
import { firstValueFrom, of } from "rxjs";
import { CollectionService } from "@bitwarden/admin-console/common";
import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
@ -13,6 +12,7 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
import {
VaultTimeoutSettingsService,
VaultTimeoutService,

View File

@ -25,7 +25,6 @@ import { JslibModule } from "@bitwarden/angular/jslib.module";
import { NudgesService, NudgeType } from "@bitwarden/angular/vault";
import { SpotlightComponent } from "@bitwarden/angular/vault/components/spotlight/spotlight.component";
import { FingerprintDialogComponent, VaultTimeoutInputComponent } from "@bitwarden/auth/angular";
import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { getFirstPolicy } from "@bitwarden/common/admin-console/services/policy/default-policy.service";
@ -33,6 +32,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
import {
VaultTimeout,
VaultTimeoutAction,

View File

@ -1,9 +1,7 @@
import { NeverDomains } from "@bitwarden/common/models/domain/domain-service";
import { ServerConfig } from "@bitwarden/common/platform/abstractions/config/server-config";
import { UserId } from "@bitwarden/common/types/guid";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { SecurityTask } from "@bitwarden/common/vault/tasks";
import { CollectionView } from "../../content/components/common-types";
import { NotificationQueueMessageTypes } from "../../enums/notification-queue-message-type.enum";
@ -20,7 +18,7 @@ interface NotificationQueueMessage {
interface AddChangePasswordQueueMessage extends NotificationQueueMessage {
type: "change";
cipherId: string;
cipherId: CipherView["id"];
newPassword: string;
}
@ -60,23 +58,10 @@ type LockedVaultPendingNotificationsData = {
target: string;
};
type AtRiskPasswordNotificationsData = {
activeUserId: UserId;
cipher: CipherView;
securityTask: SecurityTask;
uri: string;
};
type AdjustNotificationBarMessageData = {
height: number;
};
type ChangePasswordMessageData = {
currentPassword: string;
newPassword: string;
url: string;
};
type AddLoginMessageData = {
username: string;
password: string;
@ -92,10 +77,7 @@ type NotificationBackgroundExtensionMessage = {
command: string;
data?: Partial<LockedVaultPendingNotificationsData> &
Partial<AdjustNotificationBarMessageData> &
Partial<ChangePasswordMessageData> &
Partial<UnlockVaultMessageData> &
Partial<AtRiskPasswordNotificationsData>;
login?: AddLoginMessageData;
Partial<UnlockVaultMessageData>;
folder?: string;
edit?: boolean;
details?: AutofillPageDetails;
@ -121,18 +103,6 @@ type NotificationBackgroundExtensionMessageHandlers = {
bgCloseNotificationBar: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<void>;
bgOpenAtRiskPasswords: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<void>;
bgAdjustNotificationBar: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<void>;
bgTriggerAddLoginNotification: ({
message,
sender,
}: BackgroundOnMessageHandlerParams) => Promise<boolean>;
bgTriggerChangedPasswordNotification: ({
message,
sender,
}: BackgroundOnMessageHandlerParams) => Promise<boolean>;
bgTriggerAtRiskPasswordNotification: ({
message,
sender,
}: BackgroundOnMessageHandlerParams) => Promise<boolean>;
bgRemoveTabFromNotificationQueue: ({ sender }: BackgroundSenderParam) => void;
bgSaveCipher: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
bgOpenAddEditVaultItemPopout: ({
@ -162,7 +132,6 @@ export {
NotificationQueueMessageItem,
LockedVaultPendingNotificationsData,
AdjustNotificationBarMessageData,
ChangePasswordMessageData,
UnlockVaultMessageData,
AddLoginMessageData,
NotificationBackgroundExtensionMessage,

View File

@ -1,3 +1,6 @@
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { SecurityTask } from "@bitwarden/common/vault/tasks";
import AutofillPageDetails from "../../models/autofill-page-details";
export type NotificationTypeData = {
@ -8,6 +11,12 @@ export type NotificationTypeData = {
launchTimestamp?: number;
};
export type LoginSecurityTaskInfo = {
securityTask: SecurityTask;
cipher: CipherView;
uri: ModifyLoginCipherFormData["uri"];
};
export type WebsiteOriginsWithFields = Map<chrome.tabs.Tab["id"], Set<string>>;
export type ActiveFormSubmissionRequests = Set<chrome.webRequest.ResourceRequest["requestId"]>;
@ -19,19 +28,12 @@ export type ModifyLoginCipherFormData = {
newPassword: string;
};
export type ModifyLoginCipherFormDataForTab = Map<
chrome.tabs.Tab["id"],
{ uri: string; username: string; password: string; newPassword: string }
>;
export type ModifyLoginCipherFormDataForTab = Map<chrome.tabs.Tab["id"], ModifyLoginCipherFormData>;
export type OverlayNotificationsExtensionMessage = {
command: string;
uri?: string;
username?: string;
password?: string;
newPassword?: string;
details?: AutofillPageDetails;
};
} & ModifyLoginCipherFormData;
type OverlayNotificationsMessageParams = { message: OverlayNotificationsExtensionMessage };
type OverlayNotificationSenderParams = { sender: chrome.runtime.MessageSender };

View File

@ -245,6 +245,7 @@ export type OverlayBackgroundExtensionMessageHandlers = {
editedCipher: () => void;
deletedCipher: () => void;
bgSaveCipher: () => void;
updateOverlayCiphers: () => void;
fido2AbortRequest: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
};

View File

@ -3,17 +3,18 @@ import { BehaviorSubject, firstValueFrom, of } from "rxjs";
import { CollectionService } from "@bitwarden/admin-console/common";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { DefaultPolicyService } from "@bitwarden/common/admin-console/services/policy/default-policy.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
import { ExtensionCommand } from "@bitwarden/common/autofill/constants";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { UserNotificationSettingsService } from "@bitwarden/common/autofill/services/user-notification-settings.service";
import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { ThemeTypes } from "@bitwarden/common/platform/enums";
import { SelfHostedEnvironment } from "@bitwarden/common/platform/services/default-environment.service";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
import { UserId } from "@bitwarden/common/types/guid";
@ -38,6 +39,7 @@ import {
LockedVaultPendingNotificationsData,
NotificationBackgroundExtensionMessage,
} from "./abstractions/notification.background";
import { ModifyLoginCipherFormData } from "./abstractions/overlay-notifications.background";
import NotificationBackground from "./notification.background";
jest.mock("rxjs", () => {
@ -58,13 +60,21 @@ describe("NotificationBackground", () => {
const collectionService = mock<CollectionService>();
let activeAccountStatusMock$: BehaviorSubject<AuthenticationStatus>;
let authService: MockProxy<AuthService>;
const policyService = mock<DefaultPolicyService>();
const policyAppliesToUser$ = new BehaviorSubject<boolean>(true);
const policyService = mock<PolicyService>({
policyAppliesToUser$: jest.fn().mockReturnValue(policyAppliesToUser$),
});
const folderService = mock<FolderService>();
const userNotificationSettingsService = mock<UserNotificationSettingsService>();
const enableChangedPasswordPromptMock$ = new BehaviorSubject(true);
const userNotificationSettingsService = mock<UserNotificationSettingsServiceAbstraction>();
userNotificationSettingsService.enableChangedPasswordPrompt$ = enableChangedPasswordPromptMock$;
const domainSettingsService = mock<DomainSettingsService>();
const environmentService = mock<EnvironmentService>();
const logService = mock<LogService>();
const selectedThemeMock$ = new BehaviorSubject(ThemeTypes.Light);
const themeStateService = mock<ThemeStateService>();
themeStateService.selectedTheme$ = selectedThemeMock$;
const configService = mock<ConfigService>();
const accountService = mock<AccountService>();
const organizationService = mock<OrganizationService>();
@ -164,7 +174,7 @@ describe("NotificationBackground", () => {
});
});
describe("notification bar extension message handlers", () => {
describe("notification bar extension message handlers and triggers", () => {
beforeEach(() => {
notificationBackground.init();
});
@ -283,7 +293,12 @@ describe("NotificationBackground", () => {
let pushAddLoginToQueueSpy: jest.SpyInstance;
let pushChangePasswordToQueueSpy: jest.SpyInstance;
let getAllDecryptedForUrlSpy: jest.SpyInstance;
const mockModifyLoginCipherFormData: ModifyLoginCipherFormData = {
username: "test",
password: "password",
uri: "https://example.com",
newPassword: null,
};
beforeEach(() => {
tab = createChromeTabMock();
sender = mock<chrome.runtime.MessageSender>({ tab });
@ -304,43 +319,34 @@ describe("NotificationBackground", () => {
});
it("skips attempting to add the login if the user is logged out", async () => {
const message: NotificationBackgroundExtensionMessage = {
command: "bgTriggerAddLoginNotification",
login: { username: "test", password: "password", url: "https://example.com" },
};
const data: ModifyLoginCipherFormData = mockModifyLoginCipherFormData;
activeAccountStatusMock$.next(AuthenticationStatus.LoggedOut);
sendMockExtensionMessage(message, sender);
await flushPromises();
await notificationBackground.triggerAddLoginNotification(data, tab);
expect(getEnableAddedLoginPromptSpy).not.toHaveBeenCalled();
expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled();
});
it("skips attempting to add the login if the login data does not contain a valid url", async () => {
const message: NotificationBackgroundExtensionMessage = {
command: "bgTriggerAddLoginNotification",
login: { username: "test", password: "password", url: "" },
const data: ModifyLoginCipherFormData = {
...mockModifyLoginCipherFormData,
uri: "",
};
activeAccountStatusMock$.next(AuthenticationStatus.Locked);
sendMockExtensionMessage(message, sender);
await flushPromises();
await notificationBackground.triggerAddLoginNotification(data, tab);
expect(getEnableAddedLoginPromptSpy).not.toHaveBeenCalled();
expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled();
});
it("skips attempting to add the login if the user with a locked vault has disabled the login notification", async () => {
const message: NotificationBackgroundExtensionMessage = {
command: "bgTriggerAddLoginNotification",
login: { username: "test", password: "password", url: "https://example.com" },
};
const data: ModifyLoginCipherFormData = mockModifyLoginCipherFormData;
activeAccountStatusMock$.next(AuthenticationStatus.Locked);
getEnableAddedLoginPromptSpy.mockReturnValueOnce(false);
sendMockExtensionMessage(message, sender);
await flushPromises();
await notificationBackground.triggerAddLoginNotification(data, tab);
expect(getEnableAddedLoginPromptSpy).toHaveBeenCalled();
expect(getAllDecryptedForUrlSpy).not.toHaveBeenCalled();
@ -349,16 +355,12 @@ describe("NotificationBackground", () => {
});
it("skips attempting to add the login if the user with an unlocked vault has disabled the login notification", async () => {
const message: NotificationBackgroundExtensionMessage = {
command: "bgTriggerAddLoginNotification",
login: { username: "test", password: "password", url: "https://example.com" },
};
const data: ModifyLoginCipherFormData = mockModifyLoginCipherFormData;
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
getEnableAddedLoginPromptSpy.mockReturnValueOnce(false);
getAllDecryptedForUrlSpy.mockResolvedValueOnce([]);
sendMockExtensionMessage(message, sender);
await flushPromises();
await notificationBackground.triggerAddLoginNotification(data, tab);
expect(getEnableAddedLoginPromptSpy).toHaveBeenCalled();
expect(getAllDecryptedForUrlSpy).toHaveBeenCalled();
@ -367,10 +369,7 @@ describe("NotificationBackground", () => {
});
it("skips attempting to change the password for an existing login if the user has disabled changing the password notification", async () => {
const message: NotificationBackgroundExtensionMessage = {
command: "bgTriggerAddLoginNotification",
login: { username: "test", password: "password", url: "https://example.com" },
};
const data: ModifyLoginCipherFormData = mockModifyLoginCipherFormData;
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
getEnableAddedLoginPromptSpy.mockReturnValueOnce(true);
getEnableChangedPasswordPromptSpy.mockReturnValueOnce(false);
@ -378,8 +377,7 @@ describe("NotificationBackground", () => {
mock<CipherView>({ login: { username: "test", password: "oldPassword" } }),
]);
sendMockExtensionMessage(message, sender);
await flushPromises();
await notificationBackground.triggerAddLoginNotification(data, tab);
expect(getEnableAddedLoginPromptSpy).toHaveBeenCalled();
expect(getAllDecryptedForUrlSpy).toHaveBeenCalled();
@ -389,18 +387,14 @@ describe("NotificationBackground", () => {
});
it("skips attempting to change the password for an existing login if the password has not changed", async () => {
const message: NotificationBackgroundExtensionMessage = {
command: "bgTriggerAddLoginNotification",
login: { username: "test", password: "password", url: "https://example.com" },
};
const data: ModifyLoginCipherFormData = mockModifyLoginCipherFormData;
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
getEnableAddedLoginPromptSpy.mockReturnValueOnce(true);
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
mock<CipherView>({ login: { username: "test", password: "password" } }),
]);
sendMockExtensionMessage(message, sender);
await flushPromises();
await notificationBackground.triggerAddLoginNotification(data, tab);
expect(getEnableAddedLoginPromptSpy).toHaveBeenCalled();
expect(getAllDecryptedForUrlSpy).toHaveBeenCalled();
@ -409,48 +403,55 @@ describe("NotificationBackground", () => {
});
it("adds the login to the queue if the user has a locked account", async () => {
const login = { username: "test", password: "password", url: "https://example.com" };
const message: NotificationBackgroundExtensionMessage = {
command: "bgTriggerAddLoginNotification",
login,
};
const data: ModifyLoginCipherFormData = mockModifyLoginCipherFormData;
activeAccountStatusMock$.next(AuthenticationStatus.Locked);
getEnableAddedLoginPromptSpy.mockReturnValueOnce(true);
sendMockExtensionMessage(message, sender);
await flushPromises();
await notificationBackground.triggerAddLoginNotification(data, tab);
expect(pushAddLoginToQueueSpy).toHaveBeenCalledWith("example.com", login, sender.tab, true);
expect(pushAddLoginToQueueSpy).toHaveBeenCalledWith(
"example.com",
{
url: data.uri,
username: data.username,
password: data.password,
},
sender.tab,
true,
);
});
it("adds the login to the queue if the user has an unlocked account and the login is new", async () => {
const login = {
username: undefined,
password: "password",
url: "https://example.com",
} as any;
const message: NotificationBackgroundExtensionMessage = {
command: "bgTriggerAddLoginNotification",
login,
const data: ModifyLoginCipherFormData = {
...mockModifyLoginCipherFormData,
username: null,
};
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
getEnableAddedLoginPromptSpy.mockReturnValueOnce(true);
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
mock<CipherView>({ login: { username: "anotherTestUsername", password: "password" } }),
]);
sendMockExtensionMessage(message, sender);
await flushPromises();
await notificationBackground.triggerAddLoginNotification(data, tab);
expect(pushAddLoginToQueueSpy).toHaveBeenCalledWith("example.com", login, sender.tab);
expect(pushAddLoginToQueueSpy).toHaveBeenCalledWith(
"example.com",
{
url: data.uri,
username: data.username,
password: data.password,
},
sender.tab,
);
});
it("adds a change password message to the queue if the user has changed an existing cipher's password", async () => {
const login = { username: "tEsT", password: "password", url: "https://example.com" };
const message: NotificationBackgroundExtensionMessage = {
command: "bgTriggerAddLoginNotification",
login,
const data: ModifyLoginCipherFormData = {
...mockModifyLoginCipherFormData,
username: "tEsT",
};
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
getEnableAddedLoginPromptSpy.mockResolvedValueOnce(true);
getEnableChangedPasswordPromptSpy.mockResolvedValueOnce(true);
@ -461,13 +462,12 @@ describe("NotificationBackground", () => {
}),
]);
sendMockExtensionMessage(message, sender);
await flushPromises();
await notificationBackground.triggerAddLoginNotification(data, tab);
expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith(
"cipher-id",
"example.com",
login.password,
data.password,
sender.tab,
);
});
@ -478,6 +478,12 @@ describe("NotificationBackground", () => {
let sender: chrome.runtime.MessageSender;
let pushChangePasswordToQueueSpy: jest.SpyInstance;
let getAllDecryptedForUrlSpy: jest.SpyInstance;
const mockModifyLoginCipherFormData: ModifyLoginCipherFormData = {
username: null,
uri: null,
password: "currentPassword",
newPassword: "newPassword",
};
beforeEach(() => {
tab = createChromeTabMock();
@ -490,69 +496,51 @@ describe("NotificationBackground", () => {
});
it("skips attempting to add the change password message to the queue if the passed url is not valid", async () => {
const message: NotificationBackgroundExtensionMessage = {
command: "bgTriggerChangedPasswordNotification",
data: { newPassword: "newPassword", currentPassword: "currentPassword", url: "" },
};
const data: ModifyLoginCipherFormData = mockModifyLoginCipherFormData;
sendMockExtensionMessage(message);
await flushPromises();
await notificationBackground.triggerChangedPasswordNotification(data, tab);
expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled();
});
it("adds a change password message to the queue if the user does not have an unlocked account", async () => {
const message: NotificationBackgroundExtensionMessage = {
command: "bgTriggerChangedPasswordNotification",
data: {
newPassword: "newPassword",
currentPassword: "currentPassword",
url: "https://example.com",
},
const data: ModifyLoginCipherFormData = {
...mockModifyLoginCipherFormData,
uri: "https://example.com",
};
activeAccountStatusMock$.next(AuthenticationStatus.Locked);
sendMockExtensionMessage(message, sender);
await flushPromises();
await notificationBackground.triggerChangedPasswordNotification(data, tab);
expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith(
null,
"example.com",
message.data?.newPassword,
data?.newPassword,
sender.tab,
true,
);
});
it("skips adding a change password message to the queue if the multiple ciphers exist for the passed URL and the current password is not found within the list of ciphers", async () => {
const message: NotificationBackgroundExtensionMessage = {
command: "bgTriggerChangedPasswordNotification",
data: {
newPassword: "newPassword",
currentPassword: "currentPassword",
url: "https://example.com",
},
const data: ModifyLoginCipherFormData = {
...mockModifyLoginCipherFormData,
uri: "https://example.com",
};
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
mock<CipherView>({ login: { username: "test", password: "password" } }),
]);
sendMockExtensionMessage(message, sender);
await flushPromises();
await notificationBackground.triggerChangedPasswordNotification(data, tab);
expect(getAllDecryptedForUrlSpy).toHaveBeenCalled();
expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled();
});
it("skips adding a change password message if more than one existing cipher is found with a matching password ", async () => {
const message: NotificationBackgroundExtensionMessage = {
command: "bgTriggerChangedPasswordNotification",
data: {
newPassword: "newPassword",
currentPassword: "currentPassword",
url: "https://example.com",
},
const data: ModifyLoginCipherFormData = {
...mockModifyLoginCipherFormData,
uri: "https://example.com",
};
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
@ -560,21 +548,16 @@ describe("NotificationBackground", () => {
mock<CipherView>({ login: { username: "test2", password: "password" } }),
]);
sendMockExtensionMessage(message, sender);
await flushPromises();
await notificationBackground.triggerChangedPasswordNotification(data, tab);
expect(getAllDecryptedForUrlSpy).toHaveBeenCalled();
expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled();
});
it("adds a change password message to the queue if a single cipher matches the passed current password", async () => {
const message: NotificationBackgroundExtensionMessage = {
command: "bgTriggerChangedPasswordNotification",
data: {
newPassword: "newPassword",
currentPassword: "currentPassword",
url: "https://example.com",
},
const data: ModifyLoginCipherFormData = {
...mockModifyLoginCipherFormData,
uri: "https://example.com",
};
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
@ -584,24 +567,20 @@ describe("NotificationBackground", () => {
}),
]);
sendMockExtensionMessage(message, sender);
await flushPromises();
await notificationBackground.triggerChangedPasswordNotification(data, tab);
expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith(
"cipher-id",
"example.com",
message.data?.newPassword,
data?.newPassword,
sender.tab,
);
});
it("skips adding a change password message if no current password is passed in the message and more than one cipher is found for a url", async () => {
const message: NotificationBackgroundExtensionMessage = {
command: "bgTriggerChangedPasswordNotification",
data: {
newPassword: "newPassword",
url: "https://example.com",
},
const data: ModifyLoginCipherFormData = {
...mockModifyLoginCipherFormData,
uri: "https://example.com",
};
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
@ -609,20 +588,17 @@ describe("NotificationBackground", () => {
mock<CipherView>({ login: { username: "test2", password: "password" } }),
]);
sendMockExtensionMessage(message, sender);
await flushPromises();
await notificationBackground.triggerChangedPasswordNotification(data, tab);
expect(getAllDecryptedForUrlSpy).toHaveBeenCalled();
expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled();
});
it("adds a change password message to the queue if no current password is passed with the message, but a single cipher is matched for the uri", async () => {
const message: NotificationBackgroundExtensionMessage = {
command: "bgTriggerChangedPasswordNotification",
data: {
newPassword: "newPassword",
url: "https://example.com",
},
const data: ModifyLoginCipherFormData = {
...mockModifyLoginCipherFormData,
uri: "https://example.com",
password: null,
};
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
@ -632,13 +608,12 @@ describe("NotificationBackground", () => {
}),
]);
sendMockExtensionMessage(message, sender);
await flushPromises();
await notificationBackground.triggerChangedPasswordNotification(data, tab);
expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith(
"cipher-id",
"example.com",
message.data?.newPassword,
data?.newPassword,
sender.tab,
);
});

View File

@ -41,7 +41,7 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
import { TaskService } from "@bitwarden/common/vault/tasks";
import { SecurityTaskType } from "@bitwarden/common/vault/tasks/enums";
import { SecurityTaskStatus, SecurityTaskType } from "@bitwarden/common/vault/tasks/enums";
import { SecurityTask } from "@bitwarden/common/vault/tasks/models/security-task";
// FIXME (PM-22628): Popup imports are forbidden in background
@ -68,14 +68,17 @@ import {
AddChangePasswordQueueMessage,
AddLoginQueueMessage,
AddUnlockVaultQueueMessage,
ChangePasswordMessageData,
AddLoginMessageData,
NotificationQueueMessageItem,
LockedVaultPendingNotificationsData,
NotificationBackgroundExtensionMessage,
NotificationBackgroundExtensionMessageHandlers,
} from "./abstractions/notification.background";
import { NotificationTypeData } from "./abstractions/overlay-notifications.background";
import {
LoginSecurityTaskInfo,
ModifyLoginCipherFormData,
NotificationTypeData,
} from "./abstractions/overlay-notifications.background";
import { OverlayBackgroundExtensionMessage } from "./abstractions/overlay.background";
export default class NotificationBackground {
@ -91,12 +94,6 @@ export default class NotificationBackground {
private readonly extensionMessageHandlers: NotificationBackgroundExtensionMessageHandlers = {
bgAdjustNotificationBar: ({ message, sender }) =>
this.handleAdjustNotificationBarMessage(message, sender),
bgTriggerAddLoginNotification: ({ message, sender }) =>
this.triggerAddLoginNotification(message, sender),
bgTriggerChangedPasswordNotification: ({ message, sender }) =>
this.triggerChangedPasswordNotification(message, sender),
bgTriggerAtRiskPasswordNotification: ({ message, sender }) =>
this.triggerAtRiskPasswordNotification(message, sender),
bgCloseNotificationBar: ({ message, sender }) =>
this.handleCloseNotificationBarMessage(message, sender),
bgOpenAtRiskPasswords: ({ message, sender }) =>
@ -286,6 +283,62 @@ export default class NotificationBackground {
};
}
/**
* If there is a security task for this cipher at login, return the task, cipher view, and uri.
*
* @param modifyLoginData - The modified login form data
* @param activeUserId - The currently logged in user ID
*/
private async getSecurityTaskAndCipherForLoginData(
modifyLoginData: ModifyLoginCipherFormData,
activeUserId: UserId,
): Promise<LoginSecurityTaskInfo | null> {
const tasks: SecurityTask[] = await this.getSecurityTasks(activeUserId);
if (!tasks?.length) {
return null;
}
const urlCiphers: CipherView[] = await this.cipherService.getAllDecryptedForUrl(
modifyLoginData.uri,
activeUserId,
);
if (!urlCiphers?.length) {
return null;
}
const securityTaskForLogin = urlCiphers.reduce(
(taskInfo: LoginSecurityTaskInfo | null, cipher: CipherView) => {
if (
// exit early if info was found already
taskInfo ||
// exit early if the cipher was deleted
cipher.deletedDate ||
// exit early if the entered login info doesn't match an existing cipher
modifyLoginData.username !== cipher.login.username ||
modifyLoginData.password !== cipher.login.password
) {
return taskInfo;
}
// Find the first security task for the cipherId belonging to the entered login
const cipherSecurityTask = tasks.find(
({ cipherId, status }) =>
cipher.id === cipherId && // match security task cipher id to url cipher id
status === SecurityTaskStatus.Pending, // security task has not been completed
);
if (cipherSecurityTask) {
return { securityTask: cipherSecurityTask, cipher, uri: modifyLoginData.uri };
}
return taskInfo;
},
null,
);
return securityTaskForLogin;
}
/**
* Gets the active user server config from the config service.
*/
@ -302,6 +355,10 @@ export default class NotificationBackground {
return flagValue;
}
/**
* Gets the current authentication status of the user.
* @returns Promise<AuthenticationStatus> - The current authentication status of the user.
*/
private async getAuthStatus() {
return await firstValueFrom(this.authService.activeAccountStatus$);
}
@ -400,11 +457,32 @@ export default class NotificationBackground {
* @param sender - The contextual sender of the message
*/
async triggerAtRiskPasswordNotification(
message: NotificationBackgroundExtensionMessage,
sender: chrome.runtime.MessageSender,
data: ModifyLoginCipherFormData,
tab: chrome.tabs.Tab,
): Promise<boolean> {
const { activeUserId, securityTask, cipher } = message.data;
const domain = Utils.getDomain(sender.tab.url);
const flag = await this.getNotificationFlag();
if (!flag) {
return false;
}
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(getOptionalUserId),
);
if (!activeUserId) {
return false;
}
const loginSecurityTaskInfo = await this.getSecurityTaskAndCipherForLoginData(
data,
activeUserId,
);
if (!loginSecurityTaskInfo) {
return false;
}
const { securityTask, cipher } = loginSecurityTaskInfo;
const domain = Utils.getDomain(tab.url);
const passwordChangeUri =
await new TemporaryNotificationChangeLoginService().getChangePasswordUrl(cipher);
@ -418,7 +496,7 @@ export default class NotificationBackground {
.pipe(getOrganizationById(securityTask.organizationId)),
);
this.removeTabFromNotificationQueue(sender.tab);
this.removeTabFromNotificationQueue(tab);
const launchTimestamp = new Date().getTime();
const queueMessage: NotificationQueueMessageItem = {
domain,
@ -426,12 +504,12 @@ export default class NotificationBackground {
type: NotificationQueueMessageType.AtRiskPassword,
passwordChangeUri,
organizationName: organization.name,
tab: sender.tab,
tab: tab,
launchTimestamp,
expires: new Date(launchTimestamp + NOTIFICATION_BAR_LIFESPAN_MS),
};
this.notificationQueue.push(queueMessage);
await this.checkNotificationQueue(sender.tab);
await this.checkNotificationQueue(tab);
return true;
}
@ -444,17 +522,22 @@ export default class NotificationBackground {
* @param sender - The contextual sender of the message
*/
async triggerAddLoginNotification(
message: NotificationBackgroundExtensionMessage,
sender: chrome.runtime.MessageSender,
data: ModifyLoginCipherFormData,
tab: chrome.tabs.Tab,
): Promise<boolean> {
const login = {
url: data.uri,
username: data.username,
password: data.password || data.newPassword,
};
const authStatus = await this.getAuthStatus();
if (authStatus === AuthenticationStatus.LoggedOut) {
return false;
}
const loginInfo = message.login;
const normalizedUsername = loginInfo.username ? loginInfo.username.toLowerCase() : "";
const loginDomain = Utils.getDomain(loginInfo.url);
const normalizedUsername = login.username ? login.username.toLowerCase() : "";
const loginDomain = Utils.getDomain(login.url);
if (loginDomain == null) {
return false;
}
@ -463,7 +546,7 @@ export default class NotificationBackground {
if (authStatus === AuthenticationStatus.Locked) {
if (addLoginIsEnabled) {
await this.pushAddLoginToQueue(loginDomain, loginInfo, sender.tab, true);
await this.pushAddLoginToQueue(loginDomain, login, tab, true);
}
return false;
@ -476,12 +559,12 @@ export default class NotificationBackground {
return false;
}
const ciphers = await this.cipherService.getAllDecryptedForUrl(loginInfo.url, activeUserId);
const ciphers = await this.cipherService.getAllDecryptedForUrl(login.url, activeUserId);
const usernameMatches = ciphers.filter(
(c) => c.login.username != null && c.login.username.toLowerCase() === normalizedUsername,
);
if (addLoginIsEnabled && usernameMatches.length === 0) {
await this.pushAddLoginToQueue(loginDomain, loginInfo, sender.tab);
await this.pushAddLoginToQueue(loginDomain, login, tab);
return true;
}
@ -490,14 +573,9 @@ export default class NotificationBackground {
if (
changePasswordIsEnabled &&
usernameMatches.length === 1 &&
usernameMatches[0].login.password !== loginInfo.password
usernameMatches[0].login.password !== login.password
) {
await this.pushChangePasswordToQueue(
usernameMatches[0].id,
loginDomain,
loginInfo.password,
sender.tab,
);
await this.pushChangePasswordToQueue(usernameMatches[0].id, loginDomain, login.password, tab);
return true;
}
return false;
@ -535,23 +613,22 @@ export default class NotificationBackground {
* @param sender - The contextual sender of the message
*/
async triggerChangedPasswordNotification(
message: NotificationBackgroundExtensionMessage,
sender: chrome.runtime.MessageSender,
) {
const changeData = message.data as ChangePasswordMessageData;
data: ModifyLoginCipherFormData,
tab: chrome.tabs.Tab,
): Promise<boolean> {
const changeData = {
url: data.uri,
currentPassword: data.password,
newPassword: data.newPassword,
};
const loginDomain = Utils.getDomain(changeData.url);
if (loginDomain == null) {
return false;
}
if ((await this.getAuthStatus()) < AuthenticationStatus.Unlocked) {
await this.pushChangePasswordToQueue(
null,
loginDomain,
changeData.newPassword,
sender.tab,
true,
);
await this.pushChangePasswordToQueue(null, loginDomain, changeData.newPassword, tab, true);
return true;
}
@ -575,7 +652,7 @@ export default class NotificationBackground {
id = ciphers[0].id;
}
if (id != null) {
await this.pushChangePasswordToQueue(id, loginDomain, changeData.newPassword, sender.tab);
await this.pushChangePasswordToQueue(id, loginDomain, changeData.newPassword, tab);
return true;
}
return false;

View File

@ -1,12 +1,9 @@
import { mock, MockProxy } from "jest-mock-extended";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { CLEAR_NOTIFICATION_LOGIN_DATA_DURATION } from "@bitwarden/common/autofill/constants";
import { ServerConfig } from "@bitwarden/common/platform/abstractions/config/server-config";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { EnvironmentServerConfigData } from "@bitwarden/common/platform/models/data/server-config.data";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { TaskService } from "@bitwarden/common/vault/tasks";
import { BrowserApi } from "../../platform/browser/browser-api";
import AutofillField from "../models/autofill-field";
@ -27,9 +24,6 @@ import { OverlayNotificationsBackground } from "./overlay-notifications.backgrou
describe("OverlayNotificationsBackground", () => {
let logService: MockProxy<LogService>;
let notificationBackground: NotificationBackground;
let taskService: TaskService;
let accountService: AccountService;
let cipherService: CipherService;
let getEnableChangedPasswordPromptSpy: jest.SpyInstance;
let getEnableAddedLoginPromptSpy: jest.SpyInstance;
let overlayNotificationsBackground: OverlayNotificationsBackground;
@ -38,9 +32,6 @@ describe("OverlayNotificationsBackground", () => {
jest.useFakeTimers();
logService = mock<LogService>();
notificationBackground = mock<NotificationBackground>();
taskService = mock<TaskService>();
accountService = mock<AccountService>();
cipherService = mock<CipherService>();
getEnableChangedPasswordPromptSpy = jest
.spyOn(notificationBackground, "getEnableChangedPasswordPrompt")
.mockResolvedValue(true);
@ -50,9 +41,6 @@ describe("OverlayNotificationsBackground", () => {
overlayNotificationsBackground = new OverlayNotificationsBackground(
logService,
notificationBackground,
taskService,
accountService,
cipherService,
);
await overlayNotificationsBackground.init();
});

View File

@ -1,17 +1,12 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { firstValueFrom, Subject, switchMap, timer } from "rxjs";
import { Subject, switchMap, timer } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getOptionalUserId } from "@bitwarden/common/auth/services/account.service";
import { CLEAR_NOTIFICATION_LOGIN_DATA_DURATION } from "@bitwarden/common/autofill/constants";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { SecurityTask, SecurityTaskStatus, TaskService } from "@bitwarden/common/vault/tasks";
import { BrowserApi } from "../../platform/browser/browser-api";
import { NotificationType, NotificationTypes } from "../notification/abstractions/notification-bar";
import { generateDomainMatchPatterns, isInvalidResponseStatusCode } from "../utils";
import {
@ -25,12 +20,6 @@ import {
} from "./abstractions/overlay-notifications.background";
import NotificationBackground from "./notification.background";
type LoginSecurityTaskInfo = {
securityTask: SecurityTask;
cipher: CipherView;
uri: ModifyLoginCipherFormData["uri"];
};
export class OverlayNotificationsBackground implements OverlayNotificationsBackgroundInterface {
private websiteOriginsWithFields: WebsiteOriginsWithFields = new Map();
private activeFormSubmissionRequests: ActiveFormSubmissionRequests = new Set();
@ -39,6 +28,8 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
private notificationFallbackTimeout: number | NodeJS.Timeout | null;
private readonly formSubmissionRequestMethods: Set<string> = new Set(["POST", "PUT", "PATCH"]);
private readonly extensionMessageHandlers: OverlayNotificationsExtensionMessageHandlers = {
generatedPasswordFilled: ({ message, sender }) =>
this.storeModifiedLoginFormData(message, sender),
formFieldSubmitted: ({ message, sender }) => this.storeModifiedLoginFormData(message, sender),
collectPageDetailsResponse: ({ message, sender }) =>
this.handleCollectPageDetailsResponse(message, sender),
@ -47,9 +38,6 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
constructor(
private logService: LogService,
private notificationBackground: NotificationBackground,
private taskService: TaskService,
private accountService: AccountService,
private cipherService: CipherService,
) {}
/**
@ -274,8 +262,8 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
const modifyLoginData = this.modifyLoginCipherFormData.get(tabId);
return (
!modifyLoginData ||
!this.shouldAttemptAddLoginNotification(modifyLoginData) ||
!this.shouldAttemptChangedPasswordNotification(modifyLoginData)
!this.shouldAttemptNotification(modifyLoginData, NotificationTypes.Add) ||
!this.shouldAttemptNotification(modifyLoginData, NotificationTypes.Change)
);
};
@ -381,7 +369,7 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
return;
}
await this.triggerNotificationInit(requestId, modifyLoginData, tab);
await this.processNotifications(requestId, modifyLoginData, tab);
};
/**
@ -401,171 +389,86 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
const handleWebNavigationOnCompleted = async () => {
chrome.webNavigation.onCompleted.removeListener(handleWebNavigationOnCompleted);
const tab = await BrowserApi.getTab(tabId);
await this.triggerNotificationInit(requestId, modifyLoginData, tab);
await this.processNotifications(requestId, modifyLoginData, tab);
};
chrome.webNavigation.onCompleted.addListener(handleWebNavigationOnCompleted);
};
/**
* Initializes the add login or change password notification based on the modified login form data
* and the tab details. This will trigger the notification to be displayed to the user.
* This method attempts to trigger the add login, change password, or at-risk password notifications
* based on the modified login data and the tab details.
*
* @param requestId - The details of the web response
* @param modifyLoginData - The modified login form data
* @param tab - The tab details
*/
private triggerNotificationInit = async (
private processNotifications = async (
requestId: chrome.webRequest.ResourceRequest["requestId"],
modifyLoginData: ModifyLoginCipherFormData,
tab: chrome.tabs.Tab,
config: { skippable: NotificationType[] } = { skippable: [] },
) => {
let result: string;
if (this.shouldAttemptChangedPasswordNotification(modifyLoginData)) {
// These notifications are temporarily setup as "messages" to the notification background.
// This will be structured differently in a future refactor.
const success = await this.notificationBackground.triggerChangedPasswordNotification(
{
command: "bgChangedPassword",
data: {
url: modifyLoginData.uri,
currentPassword: modifyLoginData.password,
newPassword: modifyLoginData.newPassword,
},
},
{ tab },
);
if (!success) {
result = "Unqualified changedPassword notification attempt.";
}
}
if (this.shouldAttemptAddLoginNotification(modifyLoginData)) {
const success = await this.notificationBackground.triggerAddLoginNotification(
{
command: "bgTriggerAddLoginNotification",
login: {
url: modifyLoginData.uri,
username: modifyLoginData.username,
password: modifyLoginData.password || modifyLoginData.newPassword,
},
},
{ tab },
);
if (!success) {
result = "Unqualified addLogin notification attempt.";
}
}
const shouldGetTasks =
(await this.notificationBackground.getNotificationFlag()) && !modifyLoginData.newPassword;
if (shouldGetTasks) {
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(getOptionalUserId),
);
if (activeUserId) {
const loginSecurityTaskInfo = await this.getSecurityTaskAndCipherForLoginData(
modifyLoginData,
activeUserId,
);
if (loginSecurityTaskInfo) {
await this.notificationBackground.triggerAtRiskPasswordNotification(
{
command: "bgTriggerAtRiskPasswordNotification",
data: {
activeUserId,
cipher: loginSecurityTaskInfo.cipher,
securityTask: loginSecurityTaskInfo.securityTask,
},
},
{ tab },
);
} else {
result = "Unqualified atRiskPassword notification attempt.";
}
}
}
this.clearCompletedWebRequest(requestId, tab);
return result;
};
/**
* Determines if the change password notification should be triggered.
*
* @param modifyLoginData - The modified login form data
*/
private shouldAttemptChangedPasswordNotification = (
modifyLoginData: ModifyLoginCipherFormData,
) => {
return modifyLoginData?.newPassword && !modifyLoginData.username;
};
/**
* Determines if the add login notification should be triggered.
*
* @param modifyLoginData - The modified login form data
*/
private shouldAttemptAddLoginNotification = (modifyLoginData: ModifyLoginCipherFormData) => {
return modifyLoginData?.username && (modifyLoginData.password || modifyLoginData.newPassword);
};
/**
* If there is a security task for this cipher at login, return the task, cipher view, and uri.
*
* @param modifyLoginData - The modified login form data
* @param activeUserId - The currently logged in user ID
*/
private async getSecurityTaskAndCipherForLoginData(
modifyLoginData: ModifyLoginCipherFormData,
activeUserId: UserId,
): Promise<LoginSecurityTaskInfo | null> {
const tasks: SecurityTask[] = await this.notificationBackground.getSecurityTasks(activeUserId);
if (!tasks?.length) {
return null;
}
const urlCiphers: CipherView[] = await this.cipherService.getAllDecryptedForUrl(
modifyLoginData.uri,
activeUserId,
);
if (!urlCiphers?.length) {
return null;
}
const securityTaskForLogin = urlCiphers.reduce(
(taskInfo: LoginSecurityTaskInfo | null, cipher: CipherView) => {
if (
// exit early if info was found already
taskInfo ||
// exit early if the cipher was deleted
cipher.deletedDate ||
// exit early if the entered login info doesn't match an existing cipher
modifyLoginData.username !== cipher.login.username ||
modifyLoginData.password !== cipher.login.password
) {
return taskInfo;
}
// Find the first security task for the cipherId belonging to the entered login
const cipherSecurityTask = tasks.find(
({ cipherId, status }) =>
cipher.id === cipherId && // match security task cipher id to url cipher id
status === SecurityTaskStatus.Pending, // security task has not been completed
);
if (cipherSecurityTask) {
return { securityTask: cipherSecurityTask, cipher, uri: modifyLoginData.uri };
}
return taskInfo;
const notificationCandidates = [
{
type: NotificationTypes.Change,
trigger: this.notificationBackground.triggerChangedPasswordNotification,
},
null,
{
type: NotificationTypes.Add,
trigger: this.notificationBackground.triggerAddLoginNotification,
},
{
type: NotificationTypes.AtRiskPassword,
trigger: this.notificationBackground.triggerAtRiskPasswordNotification,
},
].filter(
(candidate) =>
this.shouldAttemptNotification(modifyLoginData, candidate.type) ||
config.skippable.includes(candidate.type),
);
return securityTaskForLogin;
}
const results: string[] = [];
for (const { trigger, type } of notificationCandidates) {
const success = await trigger.bind(this.notificationBackground)(modifyLoginData, tab);
if (success) {
results.push(`Success: ${type}`);
break;
} else {
results.push(`Unqualified ${type} notification attempt.`);
}
}
this.clearCompletedWebRequest(requestId, tab.id);
return results.join(" ");
};
/**
* Determines if the add login notification should be attempted based on the modified login form data.
* @param modifyLoginData modified login form data
* @param notificationType The type of notification to be triggered
* @returns true if the notification should be attempted, false otherwise
*/
private shouldAttemptNotification = (
modifyLoginData: ModifyLoginCipherFormData,
notificationType: NotificationType,
): boolean => {
switch (notificationType) {
case NotificationTypes.Change:
return modifyLoginData?.newPassword && !modifyLoginData.username;
case NotificationTypes.Add:
return (
modifyLoginData?.username && !!(modifyLoginData.password || modifyLoginData.newPassword)
);
case NotificationTypes.AtRiskPassword:
return !modifyLoginData.newPassword;
case NotificationTypes.Unlock:
// Unlock notifications are handled separately and do not require form data
return false;
default:
this.logService.error(`Unknown notification type: ${notificationType}`);
return false;
}
};
/**
* Clears the completed web request and removes the modified login form data for the tab.
@ -575,11 +478,11 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
*/
private clearCompletedWebRequest = (
requestId: chrome.webRequest.ResourceRequest["requestId"],
tab: chrome.tabs.Tab,
tabId: chrome.tabs.Tab["id"],
) => {
this.activeFormSubmissionRequests.delete(requestId);
this.modifyLoginCipherFormData.delete(tab.id);
this.websiteOriginsWithFields.delete(tab.id);
this.modifyLoginCipherFormData.delete(tabId);
this.websiteOriginsWithFields.delete(tabId);
this.setupWebRequestsListeners();
};

View File

@ -49,7 +49,6 @@ import {
MAX_SUB_FRAME_DEPTH,
RedirectFocusDirection,
} from "../enums/autofill-overlay.enum";
import { InlineMenuFormFieldData } from "../services/abstractions/autofill-overlay-content.service";
import { AutofillService } from "../services/abstractions/autofill.service";
import { InlineMenuFieldQualificationService } from "../services/inline-menu-field-qualification.service";
import {
@ -71,6 +70,7 @@ import {
triggerWebRequestOnCompletedEvent,
} from "../spec/testing-utils";
import { ModifyLoginCipherFormData } from "./abstractions/overlay-notifications.background";
import {
FocusedFieldData,
InlineMenuPosition,
@ -2076,7 +2076,7 @@ describe("OverlayBackground", () => {
const tab = createChromeTabMock({ id: 2 });
const sender = mock<chrome.runtime.MessageSender>({ tab, frameId: 100 });
let focusedFieldData: FocusedFieldData;
let formData: InlineMenuFormFieldData;
let formData: ModifyLoginCipherFormData;
beforeEach(async () => {
await initOverlayElementPorts();
@ -3651,6 +3651,18 @@ describe("OverlayBackground", () => {
});
});
it("sends a message to the tab to store modify login change when a password is generated", async () => {
jest.useFakeTimers();
sendPortMessage(listMessageConnectorSpy, { command: "fillGeneratedPassword", portKey });
await flushPromises();
jest.advanceTimersByTime(400);
await flushPromises();
expect(tabsSendMessageSpy.mock.lastCall[1].command).toBe("generatedPasswordModifyLogin");
});
it("filters the page details to only include the new password fields before filling", async () => {
sendPortMessage(listMessageConnectorSpy, { command: "fillGeneratedPassword", portKey });
await flushPromises();
@ -3663,31 +3675,6 @@ describe("OverlayBackground", () => {
allowTotpAutofill: false,
});
});
it("opens the inline menu for fields that fill a generated password", async () => {
jest.useFakeTimers();
const formData = {
uri: "https://example.com",
username: "username",
password: "password",
newPassword: "newPassword",
};
tabsSendMessageSpy.mockImplementation((_tab, message) => {
if (message.command === "getInlineMenuFormFieldData") {
return Promise.resolve(formData);
}
return Promise.resolve();
});
const openInlineMenuSpy = jest.spyOn(overlayBackground as any, "openInlineMenu");
sendPortMessage(listMessageConnectorSpy, { command: "fillGeneratedPassword", portKey });
await flushPromises();
jest.advanceTimersByTime(400);
await flushPromises();
expect(openInlineMenuSpy).toHaveBeenCalled();
});
});
});

View File

@ -69,7 +69,6 @@ import {
MAX_SUB_FRAME_DEPTH,
} from "../enums/autofill-overlay.enum";
import AutofillField from "../models/autofill-field";
import { InlineMenuFormFieldData } from "../services/abstractions/autofill-overlay-content.service";
import { AutofillService, PageDetail } from "../services/abstractions/autofill.service";
import { InlineMenuFieldQualificationService } from "../services/abstractions/inline-menu-field-qualifications.service";
import {
@ -82,6 +81,7 @@ import {
} from "../utils";
import { LockedVaultPendingNotificationsData } from "./abstractions/notification.background";
import { ModifyLoginCipherFormData } from "./abstractions/overlay-notifications.background";
import {
BuildCipherDataParams,
CloseInlineMenuMessage,
@ -191,6 +191,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
editedCipher: () => this.updateOverlayCiphers(),
deletedCipher: () => this.updateOverlayCiphers(),
bgSaveCipher: () => this.updateOverlayCiphers(),
updateOverlayCiphers: () => this.updateOverlayCiphers(),
fido2AbortRequest: ({ sender }) => this.abortFido2ActiveRequest(sender.tab.id),
};
private readonly inlineMenuButtonPortMessageHandlers: InlineMenuButtonPortMessageHandlers = {
@ -1812,7 +1813,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
/**
* Triggers a fill of the generated password into the current tab. Will trigger
* a focus of the last focused field after filling the password.
* a focus of the last focused field after filling the password.
*
* @param port - The port of the sender
*/
@ -1856,10 +1857,16 @@ export class OverlayBackground implements OverlayBackgroundInterface {
});
globalThis.setTimeout(async () => {
if (await this.shouldShowSaveLoginInlineMenuList(port.sender.tab)) {
await this.openInlineMenu(port.sender, true);
}
}, 300);
await BrowserApi.tabSendMessage(
port.sender.tab,
{
command: "generatedPasswordModifyLogin",
},
{
frameId: this.focusedFieldData.frameId || 0,
},
);
}, 150);
}
/**
@ -1890,7 +1897,9 @@ export class OverlayBackground implements OverlayBackgroundInterface {
*
* @param tab - The tab to get the form field data from
*/
private async getInlineMenuFormFieldData(tab: chrome.tabs.Tab): Promise<InlineMenuFormFieldData> {
private async getInlineMenuFormFieldData(
tab: chrome.tabs.Tab,
): Promise<ModifyLoginCipherFormData> {
return await BrowserApi.tabSendMessage(
tab,
{

View File

@ -20,10 +20,8 @@ export default class TabsBackground {
return;
}
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.updateCurrentTabData();
this.setupTabEventListeners();
void this.updateCurrentTabData();
void this.setupTabEventListeners();
}
/**

View File

@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { EVENTS } from "@bitwarden/common/autofill/constants";
import AutofillPageDetails from "../models/autofill-page-details";
@ -122,7 +120,7 @@ class AutofillInit implements AutofillInitInterface {
* @param {AutofillExtensionMessage} message
*/
private async fillForm({ fillScript, pageDetailsUrl }: AutofillExtensionMessage) {
if ((document.defaultView || window).location.href !== pageDetailsUrl) {
if ((document.defaultView || window).location.href !== pageDetailsUrl || !fillScript) {
return;
}
@ -177,7 +175,7 @@ class AutofillInit implements AutofillInitInterface {
message: AutofillExtensionMessage,
sender: chrome.runtime.MessageSender,
sendResponse: (response?: any) => void,
): boolean => {
): boolean | null => {
const command: string = message.command;
const handler: CallableFunction | undefined = this.getExtensionMessageHandler(command);
if (!handler) {

View File

@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { setupExtensionDisconnectAction } from "../utils";
if (document.readyState === "loading") {
@ -9,7 +7,7 @@ if (document.readyState === "loading") {
}
function loadAutofiller() {
let pageHref: string = null;
let pageHref: null | string = null;
let filledThisHref = false;
let delayFillTimeout: number;
let doFillInterval: number | NodeJS.Timeout;
@ -51,9 +49,7 @@ function loadAutofiller() {
sender: "autofiller",
};
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
chrome.runtime.sendMessage(msg);
void chrome.runtime.sendMessage(msg);
}
}

View File

@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { AutofillInlineMenuContentService } from "../overlay/inline-menu/content/autofill-inline-menu-content.service";
import { AutofillOverlayContentService } from "../services/autofill-overlay-content.service";
import DomElementVisibilityService from "../services/dom-element-visibility.service";
@ -11,7 +9,7 @@ import AutofillInit from "./autofill-init";
(function (windowContext) {
if (!windowContext.bitwardenAutofillInit) {
let inlineMenuContentService: AutofillInlineMenuContentService;
let inlineMenuContentService: undefined | AutofillInlineMenuContentService;
if (globalThis.self === globalThis.top) {
inlineMenuContentService = new AutofillInlineMenuContentService();
}

View File

@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { OverlayNotificationsContentService } from "../overlay/notifications/content/overlay-notifications-content.service";
import { AutofillOverlayContentService } from "../services/autofill-overlay-content.service";
import DomElementVisibilityService from "../services/dom-element-visibility.service";
@ -20,7 +18,7 @@ import AutofillInit from "./autofill-init";
inlineMenuFieldQualificationService,
);
let overlayNotificationsContentService: OverlayNotificationsContentService;
let overlayNotificationsContentService: undefined | OverlayNotificationsContentService;
if (globalThis.self === globalThis.top) {
overlayNotificationsContentService = new OverlayNotificationsContentService();
}
@ -29,7 +27,7 @@ import AutofillInit from "./autofill-init";
domQueryService,
domElementVisibilityService,
autofillOverlayContentService,
null,
undefined,
overlayNotificationsContentService,
);
setupAutofillInitDisconnectAction(windowContext);

View File

@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { AutofillInlineMenuContentService } from "../overlay/inline-menu/content/autofill-inline-menu-content.service";
import { OverlayNotificationsContentService } from "../overlay/notifications/content/overlay-notifications-content.service";
import { AutofillOverlayContentService } from "../services/autofill-overlay-content.service";
@ -12,8 +10,8 @@ import AutofillInit from "./autofill-init";
(function (windowContext) {
if (!windowContext.bitwardenAutofillInit) {
let inlineMenuContentService: AutofillInlineMenuContentService;
let overlayNotificationsContentService: OverlayNotificationsContentService;
let inlineMenuContentService: undefined | AutofillInlineMenuContentService;
let overlayNotificationsContentService: undefined | OverlayNotificationsContentService;
if (globalThis.self === globalThis.top) {
inlineMenuContentService = new AutofillInlineMenuContentService();
overlayNotificationsContentService = new OverlayNotificationsContentService();

View File

@ -1,9 +1,16 @@
import path, { dirname, join } from "path";
import { createRequire } from "module";
import { dirname, join, resolve } from "path";
import { fileURLToPath } from "url";
import type { StorybookConfig } from "@storybook/web-components-webpack5";
import remarkGfm from "remark-gfm";
import TsconfigPathsPlugin from "tsconfig-paths-webpack-plugin";
const currentFile = fileURLToPath(import.meta.url);
const currentDirectory = dirname(currentFile);
const require = createRequire(import.meta.url);
const getAbsolutePath = (value: string): string =>
dirname(require.resolve(join(value, "package.json")));
@ -43,7 +50,7 @@ const config: StorybookConfig = {
if (config.resolve) {
config.resolve.plugins = [
new TsconfigPathsPlugin({
configFile: path.resolve(__dirname, "../../../../../tsconfig.json"),
configFile: resolve(currentDirectory, "../../../../../tsconfig.json"),
}),
] as any;
}

View File

@ -8,6 +8,7 @@ import { Spinner } from "../icons";
export type ActionButtonProps = {
buttonText: string | TemplateResult;
dataTestId?: string;
disabled?: boolean;
isLoading?: boolean;
theme: Theme;
@ -17,6 +18,7 @@ export type ActionButtonProps = {
export function ActionButton({
buttonText,
dataTestId,
disabled = false,
isLoading = false,
theme,
@ -32,6 +34,7 @@ export function ActionButton({
return html`
<button
class=${actionButtonStyles({ disabled, fullWidth, isLoading, theme })}
data-testid="${dataTestId}"
title=${buttonText}
type="button"
@click=${handleButtonClick}

View File

@ -25,7 +25,6 @@ It is designed with accessibility and responsive design in mind.
## Installation and Setup
1. Ensure you have the necessary dependencies installed:
- `lit`: Used to render the component.
- `@emotion/css`: Used for styling the component.

View File

@ -25,7 +25,6 @@ handling, and a disabled state. The component is optimized for accessibility and
## Installation and Setup
1. Ensure you have the necessary dependencies installed:
- `lit`: Used to render the component.
- `@emotion/css`: Used for styling the component.

View File

@ -22,7 +22,6 @@ a close icon for visual clarity. The component is designed to be intuitive and a
## Installation and Setup
1. Ensure you have the necessary dependencies installed:
- `lit`: Used to render the component.
- `@emotion/css`: Used for styling the component.

View File

@ -25,7 +25,6 @@ or settings where inline editing is required.
## Installation and Setup
1. Ensure you have the necessary dependencies installed:
- `lit`: Used to render the component.
- `@emotion/css`: Used for styling the component.

View File

@ -2,6 +2,8 @@ import { Meta, StoryObj } from "@storybook/web-components";
import { ThemeTypes } from "@bitwarden/common/platform/enums";
import { NotificationTypes } from "../../../../../notification/abstractions/notification-bar";
import { getNotificationTestId } from "../../../../../notification/bar";
import {
AtRiskNotification,
AtRiskNotificationProps,
@ -30,8 +32,10 @@ export default {
},
} as Meta<AtRiskNotificationProps>;
const Template = (args: AtRiskNotificationProps) => AtRiskNotification({ ...args });
const Template = (args: AtRiskNotificationProps) => {
const notificationTestId = getNotificationTestId(NotificationTypes.AtRiskPassword);
return AtRiskNotification({ ...args, notificationTestId });
};
export const Default: StoryObj<AtRiskNotificationProps> = {
render: Template,
};

View File

@ -18,11 +18,13 @@ export type AtRiskNotificationProps = NotificationBarIframeInitData & {
handleCloseNotification: (e: Event) => void;
} & {
i18n: I18n;
notificationTestId: string;
};
export function AtRiskNotification({
handleCloseNotification,
i18n,
notificationTestId,
theme = ThemeTypes.Light,
params,
}: AtRiskNotificationProps) {
@ -33,7 +35,7 @@ export function AtRiskNotification({
);
return html`
<div class=${atRiskNotificationContainerStyles(theme)}>
<div data-testid="${notificationTestId}" class=${atRiskNotificationContainerStyles(theme)}>
${NotificationHeader({
handleCloseNotification,
i18n,

View File

@ -26,6 +26,7 @@ export function AtRiskNotificationFooter({
open(passwordChangeUri, "_blank");
},
buttonText: AdditionalTasksButtonContent({ buttonText: i18n.changePassword, theme }),
dataTestId: "change-password-button",
theme,
fullWidth: false,
})}

View File

@ -1,5 +1,3 @@
(function () {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
chrome.runtime.sendMessage({ command: "triggerAutofillScriptInjection" });
void chrome.runtime.sendMessage({ command: "triggerAutofillScriptInjection" });
})();

View File

@ -200,7 +200,7 @@ export function getNotificationTestId(
[NotificationTypes.Unlock]: "unlock-notification-bar",
[NotificationTypes.Add]: "save-notification-bar",
[NotificationTypes.Change]: "update-notification-bar",
[NotificationTypes.AtRiskPassword]: "at-risk-password-notification-bar",
[NotificationTypes.AtRiskPassword]: "at-risk-notification-bar",
}[notificationType];
}
@ -287,6 +287,7 @@ async function initNotificationBar(message: NotificationBarWindowMessage) {
type: notificationBarIframeInitData.type as NotificationType,
theme: resolvedTheme,
i18n,
notificationTestId,
params: initData.params,
handleCloseNotification,
}),

View File

@ -1,3 +1,4 @@
import { ModifyLoginCipherFormData } from "../../background/abstractions/overlay-notifications.background";
import { SubFrameOffsetData } from "../../background/abstractions/overlay.background";
import { AutofillExtensionMessageParam } from "../../content/abstractions/autofill-init";
import AutofillField from "../../models/autofill-field";
@ -8,13 +9,6 @@ export type SubFrameDataFromWindowMessage = SubFrameOffsetData & {
subFrameDepth: number;
};
export type InlineMenuFormFieldData = {
uri: string;
username: string;
password: string;
newPassword: string;
};
export type AutofillOverlayContentExtensionMessageHandlers = {
[key: string]: CallableFunction;
addNewVaultItemFromOverlay: ({ message }: AutofillExtensionMessageParam) => void;
@ -32,7 +26,7 @@ export type AutofillOverlayContentExtensionMessageHandlers = {
destroyAutofillInlineMenuListeners: () => void;
getInlineMenuFormFieldData: ({
message,
}: AutofillExtensionMessageParam) => Promise<InlineMenuFormFieldData>;
}: AutofillExtensionMessageParam) => Promise<ModifyLoginCipherFormData>;
};
export interface AutofillOverlayContentService {

View File

@ -50,6 +50,15 @@ export class AutoFillConstants {
static readonly SearchFieldNames: string[] = ["search", "query", "find", "go"];
static readonly NewEmailFieldKeywords: string[] = [
"new-email",
"newemail",
"new email",
"neue e-mail",
];
static readonly NewsletterFormNames: string[] = ["newsletter"];
static readonly FieldIgnoreList: string[] = ["captcha", "findanything", "forgot"];
static readonly PasswordFieldExcludeList: string[] = [

View File

@ -3,6 +3,7 @@ import { mock, MockProxy } from "jest-mock-extended";
import { EVENTS } from "@bitwarden/common/autofill/constants";
import { CipherType } from "@bitwarden/common/vault/enums";
import { ModifyLoginCipherFormData } from "../background/abstractions/overlay-notifications.background";
import AutofillInit from "../content/autofill-init";
import {
AutofillOverlayElement,
@ -1750,6 +1751,29 @@ describe("AutofillOverlayContentService", () => {
});
describe("extension onMessage handlers", () => {
describe("generatedPasswordModifyLogin", () => {
it("relays a message regarding password generation to store modified login data", async () => {
const formFieldData: ModifyLoginCipherFormData = {
newPassword: "newPassword",
password: "password",
uri: "http://localhost/",
username: "username",
};
jest
.spyOn(autofillOverlayContentService as any, "getFormFieldData")
.mockResolvedValue(formFieldData);
sendMockExtensionMessage({
command: "generatedPasswordModifyLogin",
});
await flushPromises();
const resolvedValue = await sendExtensionMessageSpy.mock.calls[0][1];
expect(resolvedValue).toEqual(formFieldData);
});
});
describe("addNewVaultItemFromOverlay message handler", () => {
it("skips sending the message if the overlay list is not visible", async () => {
jest

View File

@ -12,6 +12,7 @@ import {
} from "@bitwarden/common/autofill/constants";
import { CipherType } from "@bitwarden/common/vault/enums";
import { ModifyLoginCipherFormData } from "../background/abstractions/overlay-notifications.background";
import {
FocusedFieldData,
NewCardCipherData,
@ -48,7 +49,6 @@ import {
import {
AutofillOverlayContentExtensionMessageHandlers,
AutofillOverlayContentService as AutofillOverlayContentServiceInterface,
InlineMenuFormFieldData,
SubFrameDataFromWindowMessage,
} from "./abstractions/autofill-overlay-content.service";
import { DomElementVisibilityService } from "./abstractions/dom-element-visibility.service";
@ -95,6 +95,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
destroyAutofillInlineMenuListeners: () => this.destroy(),
getInlineMenuFormFieldData: ({ message }) =>
this.handleGetInlineMenuFormFieldDataMessage(message),
generatedPasswordModifyLogin: () => this.sendGeneratedPasswordModifyLogin(),
};
private readonly loginFieldQualifiers: Record<string, CallableFunction> = {
[AutofillFieldQualifier.username]: this.inlineMenuFieldQualificationService.isUsernameField,
@ -235,6 +236,13 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
});
}
/**
* On password generation, send form field data i.e. modified login data
*/
sendGeneratedPasswordModifyLogin = async () => {
await this.sendExtensionMessage("generatedPasswordFilled", this.getFormFieldData());
};
/**
* Formats any found user filled fields for a login cipher and sends a message
* to the background script to add a new cipher.
@ -637,7 +645,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
/**
* Returns the form field data used for add login and change password notifications.
*/
private getFormFieldData = (): InlineMenuFormFieldData => {
private getFormFieldData = (): ModifyLoginCipherFormData => {
return {
uri: globalThis.document.URL,
username: this.userFilledFields["username"]?.value || "",

View File

@ -213,9 +213,7 @@ export default class AutofillService implements AutofillServiceInterface {
this.autofillScriptPortsSet.delete(port);
});
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.injectAutofillScriptsInAllTabs();
void this.injectAutofillScriptsInAllTabs();
}
/**
@ -470,9 +468,7 @@ export default class AutofillService implements AutofillServiceInterface {
await this.cipherService.updateLastUsedDate(options.cipher.id, activeAccount.id);
}
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
BrowserApi.tabSendMessage(
void BrowserApi.tabSendMessage(
tab,
{
command: options.autoSubmitLogin ? "triggerAutoSubmitLogin" : "fillForm",
@ -502,9 +498,10 @@ export default class AutofillService implements AutofillServiceInterface {
);
if (didAutofill) {
// 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.eventCollectionService.collect(EventType.Cipher_ClientAutofilled, options.cipher.id);
await this.eventCollectionService.collect(
EventType.Cipher_ClientAutofilled,
options.cipher.id,
);
if (totp !== null) {
return totp;
} else {

View File

@ -58,6 +58,8 @@ export class InlineMenuFieldQualificationService
"neue e-mail",
"pwdcheck",
];
private newEmailFieldKeywords = new Set(AutoFillConstants.NewEmailFieldKeywords);
private newsletterFormKeywords = new Set(AutoFillConstants.NewsletterFormNames);
private updatePasswordFieldKeywords = [
"update password",
"change password",
@ -152,6 +154,61 @@ export class InlineMenuFieldQualificationService
private totpFieldAutocompleteValue = "one-time-code";
private premiumEnabled = false;
/**
* Validates the provided field to indicate if the field is a new email field used for account creation/registration.
*
* @param field - The field to validate
*/
private isExplicitIdentityEmailField(field: AutofillField): boolean {
const matchFieldAttributeValues = [field.type, field.htmlName, field.htmlID, field.placeholder];
for (let attrIndex = 0; attrIndex < matchFieldAttributeValues.length; attrIndex++) {
if (!matchFieldAttributeValues[attrIndex]) {
continue;
}
for (let keywordIndex = 0; keywordIndex < matchFieldAttributeValues.length; keywordIndex++) {
if (this.newEmailFieldKeywords.has(matchFieldAttributeValues[attrIndex])) {
return true;
}
}
}
return false;
}
/**
* Validates the provided form to indicate if the form is related to newsletter registration.
*
* @param parentForm - The form to validate
*/
private isNewsletterForm(parentForm: any): boolean {
if (!parentForm) {
return false;
}
const matchFieldAttributeValues = [
parentForm.type,
parentForm.htmlName,
parentForm.htmlID,
parentForm.placeholder,
];
for (let attrIndex = 0; attrIndex < matchFieldAttributeValues.length; attrIndex++) {
const attrValue = matchFieldAttributeValues[attrIndex];
if (!attrValue || typeof attrValue !== "string") {
continue;
}
const attrValueLower = attrValue.toLowerCase();
for (const keyword of this.newsletterFormKeywords) {
if (attrValueLower.includes(keyword.toLowerCase())) {
return true;
}
}
}
return false;
}
constructor() {
void Promise.all([
sendExtensionMessage("getInlineMenuFieldQualificationFeatureFlag"),
@ -300,7 +357,11 @@ export class InlineMenuFieldQualificationService
return false;
}
return this.fieldContainsAutocompleteValues(field, this.identityAutocompleteValues);
return (
// Recognize explicit identity email fields (like id="new-email")
this.isFieldForIdentityEmail(field) ||
this.fieldContainsAutocompleteValues(field, this.identityAutocompleteValues)
);
}
/**
@ -397,6 +458,12 @@ export class InlineMenuFieldQualificationService
): boolean {
// If the provided field is set with an autocomplete of "username", we should assume that
// the page developer intends for this field to be interpreted as a username field.
// Exclude non-login email field from being treated as a login username field
if (this.isExplicitIdentityEmailField(field)) {
return false;
}
if (this.fieldContainsAutocompleteValues(field, this.loginUsernameAutocompleteValues)) {
const newPasswordFieldsInPageDetails = pageDetails.fields.filter(
(field) => field.viewable && this.isNewPasswordField(field),
@ -415,6 +482,10 @@ export class InlineMenuFieldQualificationService
const parentForm = pageDetails.forms[field.form];
const passwordFieldsInPageDetails = pageDetails.fields.filter(this.isCurrentPasswordField);
if (this.isNewsletterForm(parentForm)) {
return false;
}
// If the field is not structured within a form, we need to identify if the field is used in conjunction
// with a password field. If that's the case, then we should assume that it is a form field element.
if (!parentForm) {
@ -822,9 +893,14 @@ export class InlineMenuFieldQualificationService
* @param field - The field to validate
*/
isFieldForIdentityEmail = (field: AutofillField): boolean => {
if (this.isExplicitIdentityEmailField(field)) {
return true;
}
if (
this.fieldContainsAutocompleteValues(field, this.emailAutocompleteValue) ||
field.type === "email"
field.type === "email" ||
field.htmlName === "email"
) {
return true;
}

View File

@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { firstValueFrom } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
@ -15,7 +13,7 @@ const IdleInterval = 60 * 5; // 5 minutes
export default class IdleBackground {
private idle: typeof chrome.idle | typeof browser.idle | null;
private idleTimer: number | NodeJS.Timeout = null;
private idleTimer: null | number | NodeJS.Timeout = null;
private idleState = "active";
constructor(
@ -80,9 +78,8 @@ export default class IdleBackground {
globalThis.clearTimeout(this.idleTimer);
this.idleTimer = null;
}
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.idle.queryState(IdleInterval, (state: string) => {
void this.idle?.queryState(IdleInterval, (state: string) => {
if (state !== this.idleState) {
this.idleState = state;
handler(state);

View File

@ -14,8 +14,6 @@ import {
InternalUserDecryptionOptionsServiceAbstraction,
LoginEmailServiceAbstraction,
LogoutReason,
PinService,
PinServiceAbstraction,
UserDecryptionOptionsService,
} from "@bitwarden/auth/common";
import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service";
@ -82,6 +80,8 @@ import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarde
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/services/key-connector.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { MasterPasswordService } from "@bitwarden/common/key-management/master-password/services/master-password.service";
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
import { PinService } from "@bitwarden/common/key-management/pin/pin.service.implementation";
import { DefaultProcessReloadService } from "@bitwarden/common/key-management/services/default-process-reload.service";
import {
DefaultVaultTimeoutSettingsService,
@ -898,6 +898,7 @@ export default class MainBackground {
this.accountService,
this.logService,
this.cipherEncryptionService,
this.messagingService,
);
this.folderService = new FolderService(
this.keyService,
@ -1257,9 +1258,6 @@ export default class MainBackground {
this.overlayNotificationsBackground = new OverlayNotificationsBackground(
this.logService,
this.notificationBackground,
this.taskService,
this.accountService,
this.cipherService,
);
this.autoSubmitLoginBackground = new AutoSubmitLoginBackground(
@ -1374,6 +1372,7 @@ export default class MainBackground {
this.badgeService = new BadgeService(
this.stateProvider,
new DefaultBadgeBrowserApi(this.platformUtilsService),
this.logService,
);
this.authStatusBadgeUpdaterService = new AuthStatusBadgeUpdaterService(
this.badgeService,

View File

@ -48,7 +48,7 @@ export default class RuntimeBackground {
private platformUtilsService: BrowserPlatformUtilsService,
private notificationsService: NotificationsService,
private autofillSettingsService: AutofillSettingsServiceAbstraction,
private processReloadSerivce: ProcessReloadServiceAbstraction,
private processReloadService: ProcessReloadServiceAbstraction,
private environmentService: BrowserEnvironmentService,
private messagingService: MessagingService,
private logService: LogService,
@ -241,7 +241,7 @@ export default class RuntimeBackground {
await closeUnlockPopout();
}
this.processReloadSerivce.cancelProcessReload();
this.processReloadService.cancelProcessReload();
if (item) {
await BrowserApi.focusWindow(item.commandToRetry.sender.tab.windowId);

View File

@ -4,10 +4,8 @@ import { TestBed } from "@angular/core/testing";
import { mock, MockProxy } from "jest-mock-extended";
import { firstValueFrom, of } from "rxjs";
import {
PinServiceAbstraction,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
import { VaultTimeoutSettingsService } from "@bitwarden/common/key-management/vault-timeout";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { UserId } from "@bitwarden/common/types/guid";

View File

@ -3,10 +3,8 @@
import { inject } from "@angular/core";
import { combineLatest, defer, firstValueFrom, map, Observable } from "rxjs";
import {
PinServiceAbstraction,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
import { UserId } from "@bitwarden/common/types/guid";
import {
BiometricsService,

View File

@ -2,7 +2,7 @@
"manifest_version": 2,
"name": "__MSG_extName__",
"short_name": "Bitwarden",
"version": "2025.7.0",
"version": "2025.7.1",
"description": "__MSG_extDesc__",
"default_locale": "en",
"author": "Bitwarden Inc.",

View File

@ -3,7 +3,7 @@
"minimum_chrome_version": "102.0",
"name": "__MSG_extName__",
"short_name": "Bitwarden",
"version": "2025.7.0",
"version": "2025.7.1",
"description": "__MSG_extDesc__",
"default_locale": "en",
"author": "Bitwarden Inc.",

View File

@ -1,7 +1,10 @@
import { map, Observable } from "rxjs";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { BrowserApi } from "../browser/browser-api";
import { fromChromeEvent } from "../browser/from-chrome-event";
import { BadgeIcon, IconPaths } from "./icon";
@ -13,6 +16,8 @@ export interface RawBadgeState {
}
export interface BadgeBrowserApi {
activeTab$: Observable<chrome.tabs.TabActiveInfo | undefined>;
setState(state: RawBadgeState, tabId?: number): Promise<void>;
getTabs(): Promise<number[]>;
}
@ -21,6 +26,10 @@ export class DefaultBadgeBrowserApi implements BadgeBrowserApi {
private badgeAction = BrowserApi.getBrowserAction();
private sidebarAction = BrowserApi.getSidebarAction(self);
activeTab$ = fromChromeEvent(chrome.tabs.onActivated).pipe(
map(([tabActiveInfo]) => tabActiveInfo),
);
constructor(private platformUtilsService: PlatformUtilsService) {}
async setState(state: RawBadgeState, tabId?: number): Promise<void> {

View File

@ -1,5 +1,7 @@
import { mock, MockProxy } from "jest-mock-extended";
import { Subscription } from "rxjs";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { FakeAccountService, FakeStateProvider } from "@bitwarden/common/spec";
import { RawBadgeState } from "./badge-browser-api";
@ -13,6 +15,7 @@ import { MockBadgeBrowserApi } from "./test/mock-badge-browser-api";
describe("BadgeService", () => {
let badgeApi: MockBadgeBrowserApi;
let stateProvider: FakeStateProvider;
let logService!: MockProxy<LogService>;
let badgeService!: BadgeService;
let badgeServiceSubscription: Subscription;
@ -20,8 +23,9 @@ describe("BadgeService", () => {
beforeEach(() => {
badgeApi = new MockBadgeBrowserApi();
stateProvider = new FakeStateProvider(new FakeAccountService({}));
logService = mock<LogService>();
badgeService = new BadgeService(stateProvider, badgeApi);
badgeService = new BadgeService(stateProvider, badgeApi, logService);
});
afterEach(() => {
@ -34,14 +38,10 @@ describe("BadgeService", () => {
describe("given a single tab is open", () => {
beforeEach(() => {
badgeApi.tabs = [1];
badgeApi.setActiveTab(tabId);
badgeServiceSubscription = badgeService.startListening();
});
// This relies on the state provider to auto-emit
it("sets default values on startup", async () => {
expect(badgeApi.generalState).toEqual(DefaultBadgeState);
});
it("sets provided state when no other state has been set", async () => {
const state: BadgeState = {
text: "text",
@ -52,7 +52,6 @@ describe("BadgeService", () => {
await badgeService.setState("state-name", BadgeStatePriority.Default, state);
await new Promise((resolve) => setTimeout(resolve, 0));
expect(badgeApi.generalState).toEqual(state);
expect(badgeApi.specificStates[tabId]).toEqual(state);
});
@ -63,7 +62,6 @@ describe("BadgeService", () => {
await badgeService.setState("state-name", BadgeStatePriority.Default, state);
await new Promise((resolve) => setTimeout(resolve, 0));
expect(badgeApi.generalState).toEqual(DefaultBadgeState);
expect(badgeApi.specificStates[tabId]).toEqual(DefaultBadgeState);
});
@ -82,7 +80,6 @@ describe("BadgeService", () => {
backgroundColor: "#fff",
icon: BadgeIcon.Locked,
};
expect(badgeApi.generalState).toEqual(expectedState);
expect(badgeApi.specificStates[tabId]).toEqual(expectedState);
});
@ -105,7 +102,6 @@ describe("BadgeService", () => {
backgroundColor: "#aaa",
icon: BadgeIcon.Locked,
};
expect(badgeApi.generalState).toEqual(expectedState);
expect(badgeApi.specificStates[tabId]).toEqual(expectedState);
});
@ -126,7 +122,6 @@ describe("BadgeService", () => {
backgroundColor: "#fff",
icon: BadgeIcon.Locked,
};
expect(badgeApi.generalState).toEqual(expectedState);
expect(badgeApi.specificStates[tabId]).toEqual(expectedState);
});
@ -147,7 +142,6 @@ describe("BadgeService", () => {
await badgeService.clearState("state-3");
await new Promise((resolve) => setTimeout(resolve, 0));
expect(badgeApi.generalState).toEqual(DefaultBadgeState);
expect(badgeApi.specificStates[tabId]).toEqual(DefaultBadgeState);
});
@ -167,7 +161,6 @@ describe("BadgeService", () => {
backgroundColor: "#fff",
icon: DefaultBadgeState.icon,
};
expect(badgeApi.generalState).toEqual(expectedState);
expect(badgeApi.specificStates[tabId]).toEqual(expectedState);
});
@ -190,26 +183,20 @@ describe("BadgeService", () => {
backgroundColor: "#fff",
icon: BadgeIcon.Unlocked,
};
expect(badgeApi.generalState).toEqual(expectedState);
expect(badgeApi.specificStates[tabId]).toEqual(expectedState);
});
});
describe("given multiple tabs are open", () => {
const tabId = 1;
const tabIds = [1, 2, 3];
beforeEach(() => {
badgeApi.tabs = tabIds;
badgeApi.setActiveTab(tabId);
badgeServiceSubscription = badgeService.startListening();
});
it("sets default values for each tab on startup", async () => {
expect(badgeApi.generalState).toEqual(DefaultBadgeState);
for (const tabId of tabIds) {
expect(badgeApi.specificStates[tabId]).toEqual(DefaultBadgeState);
}
});
it("sets state for each tab when no other state has been set", async () => {
const state: BadgeState = {
text: "text",
@ -220,11 +207,10 @@ describe("BadgeService", () => {
await badgeService.setState("state-name", BadgeStatePriority.Default, state);
await new Promise((resolve) => setTimeout(resolve, 0));
expect(badgeApi.generalState).toEqual(state);
expect(badgeApi.specificStates).toEqual({
1: state,
2: state,
3: state,
2: undefined,
3: undefined,
});
});
});
@ -236,6 +222,7 @@ describe("BadgeService", () => {
beforeEach(() => {
badgeApi.tabs = [tabId];
badgeApi.setActiveTab(tabId);
badgeServiceSubscription = badgeService.startListening();
});
@ -249,7 +236,6 @@ describe("BadgeService", () => {
await badgeService.setState("state-name", BadgeStatePriority.Default, state, tabId);
await new Promise((resolve) => setTimeout(resolve, 0));
expect(badgeApi.generalState).toEqual(DefaultBadgeState);
expect(badgeApi.specificStates[tabId]).toEqual(state);
});
@ -260,7 +246,6 @@ describe("BadgeService", () => {
await badgeService.setState("state-name", BadgeStatePriority.Default, state, tabId);
await new Promise((resolve) => setTimeout(resolve, 0));
expect(badgeApi.generalState).toEqual(DefaultBadgeState);
expect(badgeApi.specificStates[tabId]).toEqual(DefaultBadgeState);
});
@ -279,11 +264,6 @@ describe("BadgeService", () => {
});
await new Promise((resolve) => setTimeout(resolve, 0));
expect(badgeApi.generalState).toEqual({
...DefaultBadgeState,
text: "text",
icon: BadgeIcon.Locked,
});
expect(badgeApi.specificStates[tabId]).toEqual({
text: "text",
backgroundColor: "#fff",
@ -316,7 +296,6 @@ describe("BadgeService", () => {
backgroundColor: "#fff",
icon: BadgeIcon.Locked,
};
expect(badgeApi.generalState).toEqual(DefaultBadgeState);
expect(badgeApi.specificStates[tabId]).toEqual(expectedState);
});
@ -354,7 +333,6 @@ describe("BadgeService", () => {
backgroundColor: "#aaa",
icon: BadgeIcon.Locked,
};
expect(badgeApi.generalState).toEqual(DefaultBadgeState);
expect(badgeApi.specificStates[tabId]).toEqual(expectedState);
});
@ -377,11 +355,6 @@ describe("BadgeService", () => {
});
await new Promise((resolve) => setTimeout(resolve, 0));
expect(badgeApi.generalState).toEqual({
text: "override",
backgroundColor: "#aaa",
icon: DefaultBadgeState.icon,
});
expect(badgeApi.specificStates[tabId]).toEqual({
text: "override",
backgroundColor: "#aaa",
@ -411,7 +384,6 @@ describe("BadgeService", () => {
await badgeService.clearState("state-2");
await new Promise((resolve) => setTimeout(resolve, 0));
expect(badgeApi.generalState).toEqual(DefaultBadgeState);
expect(badgeApi.specificStates[tabId]).toEqual({
text: "text",
backgroundColor: "#fff",
@ -451,7 +423,6 @@ describe("BadgeService", () => {
await badgeService.clearState("state-3");
await new Promise((resolve) => setTimeout(resolve, 0));
expect(badgeApi.generalState).toEqual(DefaultBadgeState);
expect(badgeApi.specificStates[tabId]).toEqual(DefaultBadgeState);
});
@ -476,7 +447,6 @@ describe("BadgeService", () => {
);
await new Promise((resolve) => setTimeout(resolve, 0));
expect(badgeApi.generalState).toEqual(DefaultBadgeState);
expect(badgeApi.specificStates[tabId]).toEqual({
text: "text",
backgroundColor: "#fff",
@ -513,7 +483,6 @@ describe("BadgeService", () => {
);
await new Promise((resolve) => setTimeout(resolve, 0));
expect(badgeApi.generalState).toEqual(DefaultBadgeState);
expect(badgeApi.specificStates[tabId]).toEqual({
text: "text",
backgroundColor: "#fff",
@ -523,14 +492,16 @@ describe("BadgeService", () => {
});
describe("given multiple tabs are open", () => {
const tabId = 1;
const tabIds = [1, 2, 3];
beforeEach(() => {
badgeApi.tabs = tabIds;
badgeApi.setActiveTab(tabId);
badgeServiceSubscription = badgeService.startListening();
});
it("sets tab-specific state for provided tab and general state for the others", async () => {
it("sets tab-specific state for provided tab", async () => {
const generalState: BadgeState = {
text: "general-text",
backgroundColor: "general-color",
@ -550,11 +521,10 @@ describe("BadgeService", () => {
);
await new Promise((resolve) => setTimeout(resolve, 0));
expect(badgeApi.generalState).toEqual(generalState);
expect(badgeApi.specificStates).toEqual({
[tabIds[0]]: { ...specificState, backgroundColor: "general-color" },
[tabIds[1]]: generalState,
[tabIds[2]]: generalState,
[tabIds[1]]: undefined,
[tabIds[2]]: undefined,
});
});
});

View File

@ -1,15 +1,15 @@
import {
defer,
combineLatest,
concatMap,
distinctUntilChanged,
filter,
map,
mergeMap,
pairwise,
startWith,
Subscription,
switchMap,
} from "rxjs";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import {
BADGE_MEMORY,
GlobalState,
@ -39,6 +39,7 @@ export class BadgeService {
constructor(
private stateProvider: StateProvider,
private badgeApi: BadgeBrowserApi,
private logService: LogService,
) {
this.states = this.stateProvider.getGlobal(BADGE_STATES);
}
@ -48,52 +49,47 @@ export class BadgeService {
* Without this the service will not be able to update the badge state.
*/
startListening(): Subscription {
const initialSetup$ = defer(async () => {
const openTabs = await this.badgeApi.getTabs();
await this.badgeApi.setState(DefaultBadgeState);
for (const tabId of openTabs) {
await this.badgeApi.setState(DefaultBadgeState, tabId);
}
});
return initialSetup$
.pipe(
switchMap(() => this.states.state$),
return combineLatest({
states: this.states.state$.pipe(
startWith({}),
distinctUntilChanged(),
map((states) => new Set(states ? Object.values(states) : [])),
pairwise(),
map(([previous, current]) => {
const [removed, added] = difference(previous, current);
return { states: current, removed, added };
return { all: current, removed, added };
}),
filter(({ removed, added }) => removed.size > 0 || added.size > 0),
mergeMap(async ({ states, removed, added }) => {
const changed = [...removed, ...added];
const changedTabIds = new Set(
changed.map((s) => s.tabId).filter((tabId) => tabId !== undefined),
);
const onlyTabSpecificStatesChanged = changed.every((s) => s.tabId != undefined);
if (onlyTabSpecificStatesChanged) {
// If only tab-specific states changed then we only need to update those specific tabs.
for (const tabId of changedTabIds) {
const newState = this.calculateState(states, tabId);
await this.badgeApi.setState(newState, tabId);
}
),
activeTab: this.badgeApi.activeTab$.pipe(startWith(undefined)),
})
.pipe(
concatMap(async ({ states, activeTab }) => {
const changed = [...states.removed, ...states.added];
// If the active tab wasn't changed, we don't need to update the badge.
if (!changed.some((s) => s.tabId === activeTab?.tabId || s.tabId === undefined)) {
return;
}
// If there are any general states that changed then we need to update all tabs.
const openTabs = await this.badgeApi.getTabs();
const generalState = this.calculateState(states);
await this.badgeApi.setState(generalState);
for (const tabId of openTabs) {
const newState = this.calculateState(states, tabId);
await this.badgeApi.setState(newState, tabId);
try {
const state = this.calculateState(states.all, activeTab?.tabId);
await this.badgeApi.setState(state, activeTab?.tabId);
} catch (error) {
// This usually happens when the user opens a popout because of how the browser treats it
// as a tab in the same window but then won't let you set the badge state for it.
this.logService.warning("Failed to set badge state", error);
}
}),
)
.subscribe();
.subscribe({
error: (err: unknown) => {
this.logService.error(
"Fatal error in badge service observable, badge will fail to update",
err,
);
},
});
}
/**

View File

@ -1,10 +1,22 @@
import { BehaviorSubject } from "rxjs";
import { BadgeBrowserApi, RawBadgeState } from "../badge-browser-api";
export class MockBadgeBrowserApi implements BadgeBrowserApi {
private _activeTab$ = new BehaviorSubject<chrome.tabs.TabActiveInfo | undefined>(undefined);
activeTab$ = this._activeTab$.asObservable();
specificStates: Record<number, RawBadgeState> = {};
generalState?: RawBadgeState;
tabs: number[] = [];
setActiveTab(tabId: number) {
this._activeTab$.next({
tabId,
windowId: 1,
});
}
setState(state: RawBadgeState, tabId?: number): Promise<void> {
if (tabId !== undefined) {
this.specificStates[tabId] = state;

View File

@ -1,16 +1,21 @@
<!--
end padding is less than start padding to prioritize visual alignment when icon buttons are used at the end of the end slot.
other elements used at the end of the end slot may need to add their own margin/padding to achieve visual alignment.
-->
<header
class="tw-p-3 bit-compact:tw-p-2 tw-pl-4 bit-compact:tw-pl-3 tw-transition-colors tw-duration-200 tw-border-0 tw-border-b tw-border-solid"
class="tw-py-3 bit-compact:tw-py-2 tw-pe-1 bit-compact:tw-pe-0.5 tw-transition-colors tw-duration-200 tw-border-0 tw-border-b tw-border-solid"
[ngClass]="{
'tw-bg-background-alt tw-border-transparent':
this.background === 'alt' && !pageContentScrolled(),
'tw-bg-background tw-border-secondary-300':
(this.background === 'alt' && pageContentScrolled()) || this.background === 'default',
'tw-ps-4 bit-compact:tw-ps-3': !showBackButton,
'tw-ps-1 bit-compact:tw-ps-0': showBackButton,
}"
>
<div class="tw-max-w-screen-sm tw-mx-auto tw-flex tw-justify-between tw-w-full">
<div class="tw-inline-flex tw-items-center tw-gap-2 tw-h-9">
<button
class="-tw-ml-1"
bitIconButton="bwi-angle-left"
type="button"
*ngIf="showBackButton"

View File

@ -117,7 +117,7 @@ class MockPopoutButtonComponent {}
@Component({
selector: "mock-current-account",
template: `
<button class="tw-bg-transparent tw-border-none" type="button">
<button class="tw-bg-transparent tw-border-none tw-p-0 tw-me-1" type="button">
<bit-avatar text="Ash Ketchum" size="small"></bit-avatar>
</button>
`,
@ -654,7 +654,7 @@ export const WithVirtualScrollChild: Story = {
<bit-section>
@defer (on immediate) {
<bit-item-group aria-label="Mock Vault Items">
<cdk-virtual-scroll-viewport itemSize="61" bitScrollLayout>
<cdk-virtual-scroll-viewport itemSize="59" bitScrollLayout>
<bit-item *cdkVirtualFor="let item of data; index as i">
<button type="button" bit-item-content>
<i

View File

@ -50,8 +50,8 @@ describe("LocalBackedSessionStorage", () => {
const result = await sut.get("test");
// FIXME: Remove when updating file. Eslint update
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
expect(encryptService.decryptString).toHaveBeenCalledWith(encrypted, sessionKey),
expect(result).toEqual("decrypted");
(expect(encryptService.decryptString).toHaveBeenCalledWith(encrypted, sessionKey),
expect(result).toEqual("decrypted"));
});
it("caches the decrypted value when one is stored in local storage", async () => {
@ -69,8 +69,8 @@ describe("LocalBackedSessionStorage", () => {
const result = await sut.get("test");
// FIXME: Remove when updating file. Eslint update
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
expect(encryptService.decryptString).toHaveBeenCalledWith(encrypted, sessionKey),
expect(result).toEqual("decrypted");
(expect(encryptService.decryptString).toHaveBeenCalledWith(encrypted, sessionKey),
expect(result).toEqual("decrypted"));
});
it("caches the decrypted value when one is stored in local storage", async () => {

View File

@ -33,7 +33,6 @@ import {
import {
LockService,
LoginEmailService,
PinServiceAbstraction,
SsoUrlService,
LogoutService,
} from "@bitwarden/auth/common";
@ -67,6 +66,7 @@ import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/a
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { WebCryptoFunctionService } from "@bitwarden/common/key-management/crypto/services/web-crypto-function.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
import {
VaultTimeoutService,
VaultTimeoutStringType,

View File

@ -7,7 +7,6 @@
objects = {
/* Begin PBXBuildFile section */
03100CAF291891F4008E14EF /* encrypt-worker.js in Resources */ = {isa = PBXBuildFile; fileRef = 03100CAE291891F4008E14EF /* encrypt-worker.js */; };
55BC93932CB4268A008CA4C6 /* assets in Resources */ = {isa = PBXBuildFile; fileRef = 55BC93922CB4268A008CA4C6 /* assets */; };
55E0374D2577FA6B00979016 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55E0374C2577FA6B00979016 /* AppDelegate.swift */; };
55E037502577FA6B00979016 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 55E0374E2577FA6B00979016 /* Main.storyboard */; };
@ -53,7 +52,6 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
03100CAE291891F4008E14EF /* encrypt-worker.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; name = "encrypt-worker.js"; path = "../../../build/encrypt-worker.js"; sourceTree = "<group>"; };
5508DD7926051B5900A85C58 /* libswiftAppKit.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libswiftAppKit.tbd; path = usr/lib/swift/libswiftAppKit.tbd; sourceTree = SDKROOT; };
55BC93922CB4268A008CA4C6 /* assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = assets; path = ../../../build/assets; sourceTree = "<group>"; };
55E037482577FA6B00979016 /* desktop.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = desktop.app; sourceTree = BUILT_PRODUCTS_DIR; };
@ -155,7 +153,6 @@
isa = PBXGroup;
children = (
55BC93922CB4268A008CA4C6 /* assets */,
03100CAE291891F4008E14EF /* encrypt-worker.js */,
55E037702577FA6F00979016 /* popup */,
55E037712577FA6F00979016 /* background.js */,
55E037722577FA6F00979016 /* images */,
@ -272,7 +269,6 @@
55E037802577FA6F00979016 /* background.html in Resources */,
55E0377A2577FA6F00979016 /* background.js in Resources */,
55E037792577FA6F00979016 /* popup in Resources */,
03100CAF291891F4008E14EF /* encrypt-worker.js in Resources */,
55BC93932CB4268A008CA4C6 /* assets in Resources */,
55E0377C2577FA6F00979016 /* notification in Resources */,
55E0377E2577FA6F00979016 /* vendor.js in Resources */,

View File

@ -10,7 +10,7 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs
import { ProductTierType } from "@bitwarden/common/billing/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { CipherId, UserId } from "@bitwarden/common/types/guid";
import { CipherId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@ -163,7 +163,7 @@ describe("OpenAttachmentsComponent", () => {
it("sets `cipherIsAPartOfFreeOrg` to true when the cipher is a part of a free organization", async () => {
cipherView.organizationId = "888-333-333";
org.productTierType = ProductTierType.Free;
org.id = cipherView.organizationId;
org.id = cipherView.organizationId as OrganizationId;
await component.ngOnInit();
@ -173,7 +173,7 @@ describe("OpenAttachmentsComponent", () => {
it("sets `cipherIsAPartOfFreeOrg` to false when the organization is not free", async () => {
cipherView.organizationId = "888-333-333";
org.productTierType = ProductTierType.Families;
org.id = cipherView.organizationId;
org.id = cipherView.organizationId as OrganizationId;
await component.ngOnInit();

View File

@ -121,7 +121,7 @@
<value>Bitwarden Lösenordshanterare</value>
</data>
<data name="Summary" xml:space="preserve">
<value>Hemma, på jobbet eller på resande fot säkrar Bitwarden enkelt alla dina lösenord, passkeys, och känslig information.</value>
<value>Hemma, på jobbet eller på resande fot säkrar Bitwarden enkelt alla dina lösenord, inloggningsnycklar och känslig information.</value>
</data>
<data name="Description" xml:space="preserve">
<value>Erkänd som den bästa lösenordshanteraren av PCMag, WIRED, The Verge, CNET, G2 och många fler!
@ -169,7 +169,7 @@ End-to-end krypterade lösningar för hantering av referenser från Bitwarden g
</value>
</data>
<data name="AssetTitle" xml:space="preserve">
<value>Hemma, på jobbet eller på resande fot säkrar Bitwarden enkelt alla dina lösenord, passkeys, och känslig information.</value>
<value>Hemma, på jobbet eller på resande fot säkrar Bitwarden enkelt alla dina lösenord, inloggningsnycklar och känslig information.</value>
</data>
<data name="ScreenshotSync" xml:space="preserve">
<value>Synkronisera och kom åt ditt valv från flera enheter</value>

View File

@ -1 +0,0 @@
v20

View File

@ -1,13 +1,20 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { firstValueFrom } from "rxjs";
import {
OrganizationUserApiService,
OrganizationUserConfirmRequest,
} from "@bitwarden/admin-console/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { OrgKey } from "@bitwarden/common/types/key";
import { KeyService } from "@bitwarden/key-management";
import { EncString } from "@bitwarden/sdk-internal";
import { Response } from "../../models/response";
@ -17,6 +24,8 @@ export class ConfirmCommand {
private keyService: KeyService,
private encryptService: EncryptService,
private organizationUserApiService: OrganizationUserApiService,
private configService: ConfigService,
private i18nService: I18nService,
) {}
async run(object: string, id: string, cmdOptions: Record<string, any>): Promise<Response> {
@ -60,6 +69,11 @@ export class ConfirmCommand {
const key = await this.encryptService.encapsulateKeyUnsigned(orgKey, publicKey);
const req = new OrganizationUserConfirmRequest();
req.key = key.encryptedString;
if (
await firstValueFrom(this.configService.getFeatureFlag$(FeatureFlag.CreateDefaultLocation))
) {
req.defaultUserCollectionName = await this.getEncryptedDefaultUserCollectionName(orgKey);
}
await this.organizationUserApiService.postOrganizationUserConfirm(
options.organizationId,
id,
@ -70,6 +84,12 @@ export class ConfirmCommand {
return Response.error(e);
}
}
private async getEncryptedDefaultUserCollectionName(orgKey: OrgKey): Promise<EncString> {
const defaultCollectionName = this.i18nService.t("myItems");
const encrypted = await this.encryptService.encryptString(defaultCollectionName, orgKey);
return encrypted.encryptedString;
}
}
class Options {

View File

@ -1,13 +1,14 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { CollectionExport } from "@bitwarden/common/models/export/collection.export";
import { OrganizationId } from "@bitwarden/common/types/guid";
import { SelectionReadOnly } from "../selection-read-only";
export class OrganizationCollectionRequest extends CollectionExport {
static template(): OrganizationCollectionRequest {
const req = new OrganizationCollectionRequest();
req.organizationId = "00000000-0000-0000-0000-000000000000";
req.organizationId = "00000000-0000-0000-0000-000000000000" as OrganizationId;
req.name = "Collection name";
req.externalId = null;
req.groups = [SelectionReadOnly.template(), SelectionReadOnly.template()];

View File

@ -170,7 +170,7 @@ export class EditCommand {
let folderView = await folder.decrypt();
folderView = FolderExport.toView(req, folderView);
const userKey = await this.keyService.getUserKeyWithLegacySupport(activeUserId);
const userKey = await this.keyService.getUserKey(activeUserId);
const encFolder = await this.folderService.encrypt(folderView, userKey);
try {
const folder = await this.folderApiService.save(encFolder, activeUserId);

View File

@ -215,5 +215,8 @@
},
"youHaveBeenLoggedOut": {
"message": "You have been logged out."
},
"myItems": {
"message": "My Items"
}
}

View File

@ -131,6 +131,8 @@ export class OssServeConfigurator {
this.serviceContainer.keyService,
this.serviceContainer.encryptService,
this.serviceContainer.organizationUserApiService,
this.serviceContainer.configService,
this.serviceContainer.i18nService,
);
this.restoreCommand = new RestoreCommand(
this.serviceContainer.cipherService,

View File

@ -16,8 +16,6 @@ import {
AuthRequestService,
LoginStrategyService,
LoginStrategyServiceAbstraction,
PinService,
PinServiceAbstraction,
UserDecryptionOptionsService,
SsoUrlService,
AuthRequestApiServiceAbstraction,
@ -68,6 +66,8 @@ import { DeviceTrustService } from "@bitwarden/common/key-management/device-trus
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/services/key-connector.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { MasterPasswordService } from "@bitwarden/common/key-management/master-password/services/master-password.service";
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
import { PinService } from "@bitwarden/common/key-management/pin/pin.service.implementation";
import {
DefaultVaultTimeoutService,
DefaultVaultTimeoutSettingsService,
@ -723,6 +723,7 @@ export class ServiceContainer {
this.accountService,
this.logService,
this.cipherEncryptionService,
this.messagingService,
);
this.folderService = new FolderService(

View File

@ -149,11 +149,11 @@ export class SendProgram extends BaseProgram {
private templateCommand(): Command {
return new Command("template")
.argument("<object>", "Valid objects are: send.text, send.file")
.argument("<object>", "Valid objects are: send.text, text, send.file, file")
.description("Get json templates for send objects")
.action((options: OptionValues) =>
this.processResponse(new SendTemplateCommand().run(options.object)),
);
.action((object: string) => {
this.processResponse(new SendTemplateCommand().run(object));
});
}
private getCommand(): Command {

View File

@ -432,6 +432,8 @@ export class VaultProgram extends BaseProgram {
this.serviceContainer.keyService,
this.serviceContainer.encryptService,
this.serviceContainer.organizationUserApiService,
this.serviceContainer.configService,
this.serviceContainer.i18nService,
);
const response = await command.run(object, id, cmd);
this.processResponse(response);

View File

@ -180,7 +180,7 @@ export class CreateCommand {
private async createFolder(req: FolderExport) {
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
const userKey = await this.keyService.getUserKeyWithLegacySupport(activeUserId);
const userKey = await this.keyService.getUserKey(activeUserId);
const folder = await this.folderService.encrypt(FolderExport.toView(req), userKey);
try {
await this.folderApiService.save(folder, activeUserId);

View File

@ -3,8 +3,8 @@ use std::os::windows::ffi::OsStringExt;
use windows::Win32::Foundation::{GetLastError, HWND};
use windows::Win32::UI::Input::KeyboardAndMouse::{
BlockInput, SendInput, INPUT, INPUT_0, INPUT_KEYBOARD, KEYBDINPUT, KEYEVENTF_KEYUP,
KEYEVENTF_UNICODE,
SendInput, INPUT, INPUT_0, INPUT_KEYBOARD, KEYBDINPUT, KEYEVENTF_KEYUP, KEYEVENTF_UNICODE,
VIRTUAL_KEY,
};
use windows::Win32::UI::WindowsAndMessaging::{
GetForegroundWindow, GetWindowTextLengthW, GetWindowTextW,
@ -28,21 +28,31 @@ pub fn get_foreground_window_title() -> std::result::Result<String, ()> {
///
/// https://learn.microsoft.com/en-in/windows/win32/api/winuser/nf-winuser-sendinput
pub fn type_input(input: Vec<u16>) -> Result<(), ()> {
const TAB_KEY: u16 = 9;
let mut keyboard_inputs: Vec<INPUT> = Vec::new();
// Release hotkeys
keyboard_inputs.push(build_virtual_key_input(InputKeyPress::Up, 0x12)); // alt
keyboard_inputs.push(build_virtual_key_input(InputKeyPress::Up, 0x11)); // ctrl
keyboard_inputs.push(build_unicode_input(InputKeyPress::Up, 105)); // i
for i in input {
let next_down_input = build_input(InputKeyPress::Down, i);
let next_up_input = build_input(InputKeyPress::Up, i);
let next_down_input = if i == TAB_KEY {
build_virtual_key_input(InputKeyPress::Down, i as u8)
} else {
build_unicode_input(InputKeyPress::Down, i)
};
let next_up_input = if i == TAB_KEY {
build_virtual_key_input(InputKeyPress::Up, i as u8)
} else {
build_unicode_input(InputKeyPress::Up, i)
};
keyboard_inputs.push(next_down_input);
keyboard_inputs.push(next_up_input);
}
let _ = block_input(true);
let result = send_input(keyboard_inputs);
let _ = block_input(false);
result
send_input(keyboard_inputs)
}
/// Gets the foreground window handle.
@ -103,11 +113,11 @@ enum InputKeyPress {
Up,
}
/// A function for easily building keyboard INPUT structs used in SendInput().
/// A function for easily building keyboard unicode INPUT structs used in SendInput().
///
/// Before modifying this function, make sure you read the SendInput() documentation:
/// https://learn.microsoft.com/en-in/windows/win32/api/winuser/nf-winuser-sendinput
fn build_input(key_press: InputKeyPress, character: u16) -> INPUT {
fn build_unicode_input(key_press: InputKeyPress, character: u16) -> INPUT {
match key_press {
InputKeyPress::Down => INPUT {
r#type: INPUT_KEYBOARD,
@ -136,14 +146,37 @@ fn build_input(key_press: InputKeyPress, character: u16) -> INPUT {
}
}
/// Block keyboard and mouse input events. This prevents the hotkey
/// key presses from interfering with the input sent via SendInput().
/// A function for easily building keyboard virtual-key INPUT structs used in SendInput().
///
/// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-blockinput
fn block_input(block: bool) -> Result<(), ()> {
match unsafe { BlockInput(block) } {
Ok(()) => Ok(()),
Err(_) => Err(()),
/// Before modifying this function, make sure you read the SendInput() documentation:
/// https://learn.microsoft.com/en-in/windows/win32/api/winuser/nf-winuser-sendinput
/// https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
fn build_virtual_key_input(key_press: InputKeyPress, virtual_key: u8) -> INPUT {
match key_press {
InputKeyPress::Down => INPUT {
r#type: INPUT_KEYBOARD,
Anonymous: INPUT_0 {
ki: KEYBDINPUT {
wVk: VIRTUAL_KEY(virtual_key as u16),
wScan: Default::default(),
dwFlags: Default::default(),
time: 0,
dwExtraInfo: 0,
},
},
},
InputKeyPress::Up => INPUT {
r#type: INPUT_KEYBOARD,
Anonymous: INPUT_0 {
ki: KEYBDINPUT {
wVk: VIRTUAL_KEY(virtual_key as u16),
wScan: Default::default(),
dwFlags: KEYEVENTF_KEYUP,
time: 0,
dwExtraInfo: 0,
},
},
},
}
}

View File

@ -16,7 +16,7 @@
"module-alias": "2.2.3",
"ts-node": "10.9.2",
"uuid": "11.1.0",
"yargs": "17.7.2"
"yargs": "18.0.0"
},
"devDependencies": {
"@types/node": "22.15.3",
@ -150,24 +150,24 @@
}
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
"license": "MIT",
"engines": {
"node": ">=8"
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
@ -180,37 +180,19 @@
"license": "MIT"
},
"node_modules/cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz",
"integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==",
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.1",
"wrap-ansi": "^7.0.0"
"string-width": "^7.2.0",
"strip-ansi": "^7.1.0",
"wrap-ansi": "^9.0.0"
},
"engines": {
"node": ">=12"
"node": ">=20"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
"node_modules/create-require": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
@ -227,9 +209,9 @@
}
},
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz",
"integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==",
"license": "MIT"
},
"node_modules/escalade": {
@ -250,13 +232,16 @@
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"node_modules/get-east-asian-width": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz",
"integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==",
"license": "MIT",
"engines": {
"node": ">=8"
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/make-error": {
@ -271,39 +256,36 @@
"integrity": "sha512-23g5BFj4zdQL/b6tor7Ji+QY4pEfNH784BMslY9Qb0UnJWRAt+lQGLYmRaM0KDBwIG23ffEBELhZDP2rhi9f/Q==",
"license": "MIT"
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
"emoji-regex": "^10.3.0",
"get-east-asian-width": "^1.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=8"
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=8"
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/ts-node": {
@ -388,17 +370,17 @@
"license": "MIT"
},
"node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz",
"integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
"ansi-styles": "^6.2.1",
"string-width": "^7.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=10"
"node": ">=18"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
@ -414,30 +396,29 @@
}
},
"node_modules/yargs": {
"version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
"version": "18.0.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz",
"integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==",
"license": "MIT",
"dependencies": {
"cliui": "^8.0.1",
"cliui": "^9.0.1",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.3",
"string-width": "^7.2.0",
"y18n": "^5.0.5",
"yargs-parser": "^21.1.1"
"yargs-parser": "^22.0.0"
},
"engines": {
"node": ">=12"
"node": "^20.19.0 || ^22.12.0 || >=23"
}
},
"node_modules/yargs-parser": {
"version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
"version": "22.0.0",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz",
"integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==",
"license": "ISC",
"engines": {
"node": ">=12"
"node": "^20.19.0 || ^22.12.0 || >=23"
}
},
"node_modules/yn": {

View File

@ -21,7 +21,7 @@
"module-alias": "2.2.3",
"ts-node": "10.9.2",
"uuid": "11.1.0",
"yargs": "17.7.2"
"yargs": "18.0.0"
},
"devDependencies": {
"@types/node": "22.15.3",

View File

@ -1,7 +1,7 @@
{
"name": "@bitwarden/desktop",
"description": "A secure and free password manager for all of your devices.",
"version": "2025.7.0",
"version": "2025.8.0",
"keywords": [
"bitwarden",
"password",

View File

@ -4,7 +4,6 @@ import { By } from "@angular/platform-browser";
import { mock } from "jest-mock-extended";
import { firstValueFrom, of } from "rxjs";
import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
@ -13,6 +12,7 @@ import { UserVerificationService } from "@bitwarden/common/auth/abstractions/use
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { DeviceType } from "@bitwarden/common/enums";
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
import {
VaultTimeoutSettingsService,
VaultTimeoutStringType,

View File

@ -9,7 +9,6 @@ import { concatMap, map, pairwise, startWith, switchMap, takeUntil, timeout } fr
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { VaultTimeoutInputComponent } from "@bitwarden/auth/angular";
import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { getFirstPolicy } from "@bitwarden/common/admin-console/services/policy/default-policy.service";
@ -20,6 +19,7 @@ import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/s
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { DeviceType } from "@bitwarden/common/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
import {
VaultTimeout,
VaultTimeoutAction,

View File

@ -24,11 +24,12 @@ import {
} from "rxjs";
import { CollectionService } from "@bitwarden/admin-console/common";
import { LoginApprovalDialogComponent } from "@bitwarden/angular/auth/login-approval";
import { DeviceTrustToastService } from "@bitwarden/angular/auth/services/device-trust-toast.service.abstraction";
import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
import { DocumentLangSetter } from "@bitwarden/angular/platform/i18n";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { FingerprintDialogComponent, LoginApprovalComponent } from "@bitwarden/auth/angular";
import { FingerprintDialogComponent } from "@bitwarden/auth/angular";
import {
DESKTOP_SSO_CALLBACK,
LogoutReason,
@ -476,7 +477,7 @@ export class AppComponent implements OnInit, OnDestroy {
case "openLoginApproval":
if (message.notificationId != null) {
this.dialogService.closeAll();
const dialogRef = LoginApprovalComponent.open(this.dialogService, {
const dialogRef = LoginApprovalDialogComponent.open(this.dialogService, {
notificationId: message.notificationId,
});
await firstValueFrom(dialogRef.closed);

View File

@ -91,6 +91,7 @@ export class InitService {
containerService.attachToGlobal(this.win);
await this.autofillService.init();
await this.autotypeService.init();
};
}
}

View File

@ -5,6 +5,7 @@ import { Router } from "@angular/router";
import { Subject, merge } from "rxjs";
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
import { LoginApprovalDialogComponentServiceAbstraction } from "@bitwarden/angular/auth/login-approval";
import { SetInitialPasswordService } from "@bitwarden/angular/auth/password-management/set-initial-password/set-initial-password.service.abstraction";
import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
import {
@ -31,9 +32,7 @@ import {
} from "@bitwarden/auth/angular";
import {
InternalUserDecryptionOptionsServiceAbstraction,
LoginApprovalComponentServiceAbstraction,
LoginEmailService,
PinServiceAbstraction,
SsoUrlService,
} from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
@ -50,12 +49,13 @@ import {
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { ClientType, DeviceType } from "@bitwarden/common/enums";
import { ClientType } from "@bitwarden/common/enums";
import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service";
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { WebCryptoFunctionService } from "@bitwarden/common/key-management/crypto/services/web-crypto-function.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
import { DefaultProcessReloadService } from "@bitwarden/common/key-management/services/default-process-reload.service";
import {
VaultTimeoutSettingsService,
@ -107,7 +107,7 @@ import {
import { LockComponentService } from "@bitwarden/key-management-ui";
import { DefaultSshImportPromptService, SshImportPromptService } from "@bitwarden/vault";
import { DesktopLoginApprovalComponentService } from "../../auth/login/desktop-login-approval-component.service";
import { DesktopLoginApprovalDialogComponentService } from "../../auth/login/desktop-login-approval-dialog-component.service";
import { DesktopLoginComponentService } from "../../auth/login/desktop-login-component.service";
import { DesktopTwoFactorAuthDuoComponentService } from "../../auth/services/desktop-two-factor-auth-duo-component.service";
import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service";
@ -444,8 +444,8 @@ const safeProviders: SafeProvider[] = [
deps: [],
}),
safeProvider({
provide: LoginApprovalComponentServiceAbstraction,
useClass: DesktopLoginApprovalComponentService,
provide: LoginApprovalDialogComponentServiceAbstraction,
useClass: DesktopLoginApprovalDialogComponentService,
deps: [I18nServiceAbstraction],
}),
safeProvider({
@ -455,17 +455,15 @@ const safeProviders: SafeProvider[] = [
}),
safeProvider({
provide: DesktopAutotypeService,
useFactory: (
configService: ConfigService,
globalStateProvider: GlobalStateProvider,
platformUtilsService: PlatformUtilsServiceAbstraction,
) =>
new DesktopAutotypeService(
configService,
globalStateProvider,
platformUtilsService.getDevice() === DeviceType.WindowsDesktop,
),
deps: [ConfigService, GlobalStateProvider, PlatformUtilsServiceAbstraction],
useClass: DesktopAutotypeService,
deps: [
AccountService,
AuthService,
CipherServiceAbstraction,
ConfigService,
GlobalStateProvider,
PlatformUtilsServiceAbstraction,
],
}),
];

View File

@ -2,13 +2,13 @@ import { TestBed } from "@angular/core/testing";
import { mock, MockProxy } from "jest-mock-extended";
import { Subject } from "rxjs";
import { LoginApprovalComponent } from "@bitwarden/auth/angular";
import { LoginApprovalDialogComponent } from "@bitwarden/angular/auth/login-approval";
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
import { DesktopLoginApprovalComponentService } from "./desktop-login-approval-component.service";
import { DesktopLoginApprovalDialogComponentService } from "./desktop-login-approval-dialog-component.service";
describe("DesktopLoginApprovalComponentService", () => {
let service: DesktopLoginApprovalComponentService;
describe("DesktopLoginApprovalDialogComponentService", () => {
let service: DesktopLoginApprovalDialogComponentService;
let i18nService: MockProxy<I18nServiceAbstraction>;
let originalIpc: any;
@ -31,12 +31,12 @@ describe("DesktopLoginApprovalComponentService", () => {
TestBed.configureTestingModule({
providers: [
DesktopLoginApprovalComponentService,
DesktopLoginApprovalDialogComponentService,
{ provide: I18nServiceAbstraction, useValue: i18nService },
],
});
service = TestBed.inject(DesktopLoginApprovalComponentService);
service = TestBed.inject(DesktopLoginApprovalDialogComponentService);
});
afterEach(() => {
@ -54,7 +54,7 @@ describe("DesktopLoginApprovalComponentService", () => {
const message = `Confirm access attempt for ${email}`;
const closeText = "Close";
const loginApprovalComponent = { email } as LoginApprovalComponent;
const loginApprovalDialogComponent = { email } as LoginApprovalDialogComponent;
i18nService.t.mockImplementation((key: string) => {
switch (key) {
case "accountAccessRequested":
@ -71,18 +71,20 @@ describe("DesktopLoginApprovalComponentService", () => {
jest.spyOn(ipc.platform, "isWindowVisible").mockResolvedValue(false);
jest.spyOn(ipc.auth, "loginRequest").mockResolvedValue();
await service.showLoginRequestedAlertIfWindowNotVisible(loginApprovalComponent.email);
await service.showLoginRequestedAlertIfWindowNotVisible(loginApprovalDialogComponent.email);
expect(ipc.auth.loginRequest).toHaveBeenCalledWith(title, message, closeText);
});
it("does not call ipc.auth.loginRequest when window is visible", async () => {
const loginApprovalComponent = { email: "test@bitwarden.com" } as LoginApprovalComponent;
const loginApprovalDialogComponent = {
email: "test@bitwarden.com",
} as LoginApprovalDialogComponent;
jest.spyOn(ipc.platform, "isWindowVisible").mockResolvedValue(true);
jest.spyOn(ipc.auth, "loginRequest");
await service.showLoginRequestedAlertIfWindowNotVisible(loginApprovalComponent.email);
await service.showLoginRequestedAlertIfWindowNotVisible(loginApprovalDialogComponent.email);
expect(ipc.auth.loginRequest).not.toHaveBeenCalled();
});

View File

@ -1,13 +1,15 @@
import { Injectable } from "@angular/core";
import { DefaultLoginApprovalComponentService } from "@bitwarden/auth/angular";
import { LoginApprovalComponentServiceAbstraction } from "@bitwarden/auth/common";
import {
DefaultLoginApprovalDialogComponentService,
LoginApprovalDialogComponentServiceAbstraction,
} from "@bitwarden/angular/auth/login-approval";
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
@Injectable()
export class DesktopLoginApprovalComponentService
extends DefaultLoginApprovalComponentService
implements LoginApprovalComponentServiceAbstraction
export class DesktopLoginApprovalDialogComponentService
extends DefaultLoginApprovalDialogComponentService
implements LoginApprovalDialogComponentServiceAbstraction
{
constructor(private i18nService: I18nServiceAbstraction) {
super();

View File

@ -1,33 +1,70 @@
import { autotype } from "@bitwarden/desktop-napi";
import { ipcMain, globalShortcut } from "electron";
import { DesktopAutotypeService } from "../services/desktop-autotype.service";
import { autotype } from "@bitwarden/desktop-napi";
import { LogService } from "@bitwarden/logging";
import { WindowMain } from "../../main/window.main";
import { stringIsNotUndefinedNullAndEmpty } from "../../utils";
export class MainDesktopAutotypeService {
constructor(private desktopAutotypeService: DesktopAutotypeService) {}
keySequence: string = "Alt+CommandOrControl+I";
constructor(
private logService: LogService,
private windowMain: WindowMain,
) {}
init() {
this.desktopAutotypeService.autotypeEnabled$.subscribe((enabled) => {
if (enabled) {
ipcMain.on("autofill.configureAutotype", (event, data) => {
if (data.enabled === true && !globalShortcut.isRegistered(this.keySequence)) {
this.enableAutotype();
} else {
} else if (data.enabled === false && globalShortcut.isRegistered(this.keySequence)) {
this.disableAutotype();
}
});
ipcMain.on("autofill.completeAutotypeRequest", (event, data) => {
const { response } = data;
if (
stringIsNotUndefinedNullAndEmpty(response.username) &&
stringIsNotUndefinedNullAndEmpty(response.password)
) {
this.doAutotype(response.username, response.password);
}
});
}
// TODO: this will call into desktop native code
private enableAutotype() {
// eslint-disable-next-line no-console
console.log("Enabling Autotype...");
disableAutotype() {
if (globalShortcut.isRegistered(this.keySequence)) {
globalShortcut.unregister(this.keySequence);
}
const result = autotype.getForegroundWindowTitle();
// eslint-disable-next-line no-console
console.log("Window Title: " + result);
this.logService.info("Autotype disabled.");
}
// TODO: this will call into desktop native code
private disableAutotype() {
// eslint-disable-next-line no-console
console.log("Disabling Autotype...");
private enableAutotype() {
const result = globalShortcut.register(this.keySequence, () => {
const windowTitle = autotype.getForegroundWindowTitle();
this.windowMain.win.webContents.send("autofill.listenAutotypeRequest", {
windowTitle,
});
});
result
? this.logService.info("Autotype enabled.")
: this.logService.info("Enabling autotype failed.");
}
private doAutotype(username: string, password: string) {
const inputPattern = username + "\t" + password;
const inputArray = new Array<number>(inputPattern.length);
for (let i = 0; i < inputPattern.length; i++) {
inputArray[i] = inputPattern.charCodeAt(i);
}
autotype.typeInput(inputArray);
}
}

View File

@ -127,4 +127,43 @@ export default {
},
);
},
configureAutotype: (enabled: boolean) => {
ipcRenderer.send("autofill.configureAutotype", { enabled });
},
listenAutotypeRequest: (
fn: (
windowTitle: string,
completeCallback: (
error: Error | null,
response: { username?: string; password?: string },
) => void,
) => void,
) => {
ipcRenderer.on(
"autofill.listenAutotypeRequest",
(
event,
data: {
windowTitle: string;
},
) => {
const { windowTitle } = data;
fn(windowTitle, (error, response) => {
if (error) {
ipcRenderer.send("autofill.completeError", {
windowTitle,
error: error.message,
});
return;
}
ipcRenderer.send("autofill.completeAutotypeRequest", {
windowTitle,
response,
});
});
},
);
},
};

View File

@ -1,12 +1,20 @@
import { combineLatest, map, Observable, of } from "rxjs";
import { combineLatest, filter, firstValueFrom, map, Observable, of, switchMap } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { DeviceType } from "@bitwarden/common/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import {
GlobalStateProvider,
AUTOTYPE_SETTINGS_DISK,
KeyDefinition,
} from "@bitwarden/common/platform/state";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { UserId } from "@bitwarden/user-core";
export const AUTOTYPE_ENABLED = new KeyDefinition<boolean>(
AUTOTYPE_SETTINGS_DISK,
@ -20,28 +28,83 @@ export class DesktopAutotypeService {
autotypeEnabled$: Observable<boolean> = of(false);
constructor(
private accountService: AccountService,
private authService: AuthService,
private cipherService: CipherService,
private configService: ConfigService,
private globalStateProvider: GlobalStateProvider,
private isWindows: boolean,
private platformUtilsService: PlatformUtilsService,
) {
if (this.isWindows) {
ipc.autofill.listenAutotypeRequest(async (windowTitle, callback) => {
const possibleCiphers = await this.matchCiphersToWindowTitle(windowTitle);
const firstCipher = possibleCiphers?.at(0);
return callback(null, {
username: firstCipher?.login?.username,
password: firstCipher?.login?.password,
});
});
}
async init() {
if (this.platformUtilsService.getDevice() === DeviceType.WindowsDesktop) {
this.autotypeEnabled$ = combineLatest([
this.autotypeEnabledState.state$,
this.configService.getFeatureFlag$(FeatureFlag.WindowsDesktopAutotype),
this.accountService.activeAccount$.pipe(
map((account) => account?.id),
switchMap((userId) => this.authService.authStatusFor$(userId)),
),
]).pipe(
map(
([autotypeEnabled, windowsDesktopAutotypeFeatureFlag]) =>
autotypeEnabled && windowsDesktopAutotypeFeatureFlag,
([autotypeEnabled, windowsDesktopAutotypeFeatureFlag, authStatus]) =>
autotypeEnabled &&
windowsDesktopAutotypeFeatureFlag &&
authStatus == AuthenticationStatus.Unlocked,
),
);
this.autotypeEnabled$.subscribe((enabled) => {
ipc.autofill.configureAutotype(enabled);
});
}
}
init() {}
async setAutotypeEnabledState(enabled: boolean): Promise<void> {
await this.autotypeEnabledState.update(() => enabled, {
shouldUpdate: (currentlyEnabled) => currentlyEnabled !== enabled,
});
}
async matchCiphersToWindowTitle(windowTitle: string): Promise<CipherView[]> {
const URI_PREFIX = "APP:";
windowTitle = windowTitle.toLowerCase();
const ciphers = await firstValueFrom(
this.accountService.activeAccount$.pipe(
map((account) => account?.id),
filter((userId): userId is UserId => userId != null),
switchMap((userId) => this.cipherService.cipherViews$(userId)),
),
);
const possibleCiphers = ciphers.filter((c) => {
return (
c.login?.username &&
c.login?.password &&
c.deletedDate == null &&
c.login?.uris.some((u) => {
if (u.uri?.indexOf(URI_PREFIX) !== 0) {
return false;
}
const uri = u.uri.substring(4).toLowerCase();
return windowTitle.indexOf(uri) > -1;
})
);
});
return possibleCiphers;
}
}

View File

@ -1,9 +1,9 @@
import { mock } from "jest-mock-extended";
import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { FakeMasterPasswordService } from "@bitwarden/common/key-management/master-password/services/fake-master-password.service";
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
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";

View File

@ -1,8 +1,8 @@
import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
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";

View File

@ -2,11 +2,9 @@ import { TestBed } from "@angular/core/testing";
import { mock, MockProxy } from "jest-mock-extended";
import { firstValueFrom, of } from "rxjs";
import {
PinServiceAbstraction,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
import { DeviceType } from "@bitwarden/common/enums";
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
import { VaultTimeoutSettingsService } from "@bitwarden/common/key-management/vault-timeout";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { UserId } from "@bitwarden/common/types/guid";

View File

@ -1,11 +1,9 @@
import { inject } from "@angular/core";
import { combineLatest, defer, map, Observable } from "rxjs";
import {
PinServiceAbstraction,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
import { DeviceType } from "@bitwarden/common/enums";
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { UserId } from "@bitwarden/common/types/guid";
import { BiometricsService, BiometricsStatus } from "@bitwarden/key-management";

View File

@ -3027,9 +3027,6 @@
"message": "Toggle character count",
"description": "'Character count' describes a feature that displays a number next to each character of the password."
},
"areYouTryingToAccessYourAccount": {
"message": "Are you trying to access your account?"
},
"accessAttemptBy": {
"message": "Access attempt by $EMAIL$",
"placeholders": {
@ -3039,6 +3036,50 @@
}
}
},
"loginRequestApprovedForEmailOnDevice": {
"message": "Login request approved for $EMAIL$ on $DEVICE$",
"placeholders": {
"email": {
"content": "$1",
"example": "name@example.com"
},
"device": {
"content": "$2",
"example": "Web app - Chrome"
}
}
},
"youDeniedLoginAttemptFromAnotherDevice": {
"message": "You denied a login attempt from another device. If this was you, try to log in with the device again."
},
"webApp": {
"message": "Web app"
},
"mobile": {
"message": "Mobile",
"description": "Mobile app"
},
"extension": {
"message": "Extension",
"description": "Browser extension/addon"
},
"desktop": {
"message": "Desktop",
"description": "Desktop app"
},
"cli": {
"message": "CLI"
},
"sdk": {
"message": "SDK",
"description": "Software Development Kit"
},
"server": {
"message": "Server"
},
"loginRequest": {
"message": "Login request"
},
"deviceType": {
"message": "Device Type"
},
@ -3054,22 +3095,6 @@
"denyAccess": {
"message": "Deny access"
},
"logInConfirmedForEmailOnDevice": {
"message": "Login confirmed for $EMAIL$ on $DEVICE$",
"placeholders": {
"email": {
"content": "$1",
"example": "name@example.com"
},
"device": {
"content": "$2",
"example": "iOS"
}
}
},
"youDeniedALogInAttemptFromAnotherDevice": {
"message": "You denied a login attempt from another device. If this really was you, try to log in with the device again."
},
"justNow": {
"message": "Just now"
},
@ -3595,7 +3620,7 @@
},
"uriMatchDefaultStrategyHint": {
"message": "URI match detection is how Bitwarden identifies autofill suggestions.",
"description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item."
"description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item."
},
"regExAdvancedOptionWarning": {
"message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.",
@ -4041,6 +4066,12 @@
}
}
},
"showMore": {
"message": "Show more"
},
"showLess": {
"message": "Show less"
},
"enableAutotype": {
"message": "Enable Autotype"
},

View File

@ -42,7 +42,7 @@
"message": "Rechercher dans le coffre"
},
"resetSearch": {
"message": "Reset search"
"message": "Réinitialiser la recherche"
},
"addItem": {
"message": "Ajouter un élément"
@ -1443,7 +1443,7 @@
"description": "Copy credit card security code (CVV)"
},
"cardNumber": {
"message": "card number"
"message": "numéro de carte"
},
"premiumMembership": {
"message": "Adhésion Premium"
@ -3575,11 +3575,11 @@
"description": "Link to match detection docs on warning dialog for advance match strategy"
},
"uriAdvancedOption": {
"message": "Advanced options",
"message": "Options avancées",
"description": "Advanced option placeholder for uri option component"
},
"warningCapitalized": {
"message": "Warning",
"message": "Avertissement",
"description": "Warning (should maintain locale-relevant capitalization)"
},
"success": {
@ -4007,9 +4007,9 @@
}
},
"enableAutotype": {
"message": "Enable Autotype"
"message": "Activer le type automatique"
},
"enableAutotypeDescription": {
"message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut."
"message": "Bitwarden ne valide pas les emplacements d'entrée, assurez-vous d'être dans la bonne fenêtre et le bon champ avant d'utiliser le raccourci."
}
}

View File

@ -4007,7 +4007,7 @@
}
},
"enableAutotype": {
"message": "Enable Autotype"
"message": "Omogući automatski unos"
},
"enableAutotypeDescription": {
"message": "Bitwarden ne provjerava lokacije unosa, prije korištenja prečaca provjeri da si u pravom prozoru i polju."

View File

@ -42,7 +42,7 @@
"message": "Претражи сеф"
},
"resetSearch": {
"message": "Reset search"
"message": "Ресетовати претрагу"
},
"addItem": {
"message": "Додај ставку"
@ -576,7 +576,7 @@
"message": "Копирај потврдни код (TOTP)"
},
"copyFieldCipherName": {
"message": "Copy $FIELD$, $CIPHERNAME$",
"message": "Копирај $FIELD$, $CIPHERNAME$",
"description": "Title for a button that copies a field value to the clipboard.",
"placeholders": {
"field": {
@ -1443,7 +1443,7 @@
"description": "Copy credit card security code (CVV)"
},
"cardNumber": {
"message": "card number"
"message": "број картице"
},
"premiumMembership": {
"message": "Премијум чланство"
@ -4007,9 +4007,9 @@
}
},
"enableAutotype": {
"message": "Enable Autotype"
"message": "Упали ауто-унос"
},
"enableAutotypeDescription": {
"message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut."
"message": "Bitwarden не потврђује локације уноса, будите сигурни да сте у добром прозору и поље пре употребе пречице."
}
}

View File

@ -3033,7 +3033,7 @@
}
},
"youDeniedALogInAttemptFromAnotherDevice": {
"message": "Ви відхилили спробу входу з іншого пристрою. Якщо це були дійсно ви, спробуйте увійти з пристроєм знову."
"message": "Ви відхилили спробу входу з іншого пристрою. Якщо це були дійсно ви, спробуйте ввійти з пристроєм знову."
},
"justNow": {
"message": "Щойно"

View File

@ -37,7 +37,6 @@ import { NodeCryptoFunctionService } from "@bitwarden/node/services/node-crypto-
import { MainDesktopAutotypeService } from "./autofill/main/main-desktop-autotype.service";
import { MainSshAgentService } from "./autofill/main/main-ssh-agent.service";
import { DesktopAutofillSettingsService } from "./autofill/services/desktop-autofill-settings.service";
import { DesktopAutotypeService } from "./autofill/services/desktop-autotype.service";
import { DesktopBiometricsService } from "./key-management/biometrics/desktop.biometrics.service";
import { MainBiometricsIPCListener } from "./key-management/biometrics/main-biometrics-ipc.listener";
import { MainBiometricsService } from "./key-management/biometrics/main-biometrics.service";
@ -48,7 +47,6 @@ import { PowerMonitorMain } from "./main/power-monitor.main";
import { TrayMain } from "./main/tray.main";
import { UpdaterMain } from "./main/updater.main";
import { WindowMain } from "./main/window.main";
import { SlimConfigService } from "./platform/config/slim-config.service";
import { NativeAutofillMain } from "./platform/main/autofill/native-autofill.main";
import { ClipboardMain } from "./platform/main/clipboard.main";
import { DesktopCredentialStorageListener } from "./platform/main/desktop-credential-storage-listener";
@ -307,13 +305,22 @@ export class Main {
void this.nativeAutofillMain.init();
this.mainDesktopAutotypeService = new MainDesktopAutotypeService(
new DesktopAutotypeService(
new SlimConfigService(this.environmentService, globalStateProvider),
globalStateProvider,
process.platform === "win32",
),
this.logService,
this.windowMain,
);
this.mainDesktopAutotypeService.init();
app
.whenReady()
.then(() => {
this.mainDesktopAutotypeService.init();
})
.catch((reason) => {
this.logService.error("Error initializing Autotype.", reason);
});
app.on("will-quit", () => {
this.mainDesktopAutotypeService.disableAutotype();
});
}
bootstrap() {

View File

@ -1,12 +1,12 @@
{
"name": "@bitwarden/desktop",
"version": "2025.7.0",
"version": "2025.8.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@bitwarden/desktop",
"version": "2025.7.1",
"version": "2025.8.0",
"license": "GPL-3.0",
"dependencies": {
"@bitwarden/desktop-napi": "file:../desktop_native/napi"

View File

@ -2,7 +2,7 @@
"name": "@bitwarden/desktop",
"productName": "Bitwarden",
"description": "A secure and free password manager for all of your devices.",
"version": "2025.7.0",
"version": "2025.8.0",
"author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)",
"homepage": "https://bitwarden.com",
"license": "GPL-3.0",

View File

@ -1,66 +0,0 @@
import { combineLatest, map, Observable, throwError } from "rxjs";
import { SemVer } from "semver";
import {
FeatureFlag,
FeatureFlagValueType,
getFeatureFlagValue,
} from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { ServerConfig } from "@bitwarden/common/platform/abstractions/config/server-config";
import {
EnvironmentService,
Region,
} from "@bitwarden/common/platform/abstractions/environment.service";
import { ServerSettings } from "@bitwarden/common/platform/models/domain/server-settings";
import { GLOBAL_SERVER_CONFIGURATIONS } from "@bitwarden/common/platform/services/config/default-config.service";
import { GlobalStateProvider } from "@bitwarden/common/platform/state";
import { UserId } from "@bitwarden/user-core";
/*
NOT FOR GENERAL USE
If you have more uses for the config service in the main process,
please reach out to platform.
*/
export class SlimConfigService implements ConfigService {
constructor(
private environmentService: EnvironmentService,
private globalStateProvider: GlobalStateProvider,
) {}
serverConfig$: Observable<ServerConfig> = throwError(() => {
return new Error("Method not implemented.");
});
serverSettings$: Observable<ServerSettings> = throwError(() => {
return new Error("Method not implemented.");
});
cloudRegion$: Observable<Region> = throwError(() => {
return new Error("Method not implemented.");
});
getFeatureFlag$<Flag extends FeatureFlag>(key: Flag): Observable<FeatureFlagValueType<Flag>> {
return combineLatest([
this.environmentService.environment$,
this.globalStateProvider.get(GLOBAL_SERVER_CONFIGURATIONS).state$,
]).pipe(
map(([environment, serverConfigMap]) =>
getFeatureFlagValue(serverConfigMap?.[environment.getApiUrl()], key),
),
);
}
userCachedFeatureFlag$<Flag extends FeatureFlag>(
key: Flag,
userId: UserId,
): Observable<FeatureFlagValueType<Flag>> {
throw new Error("Method not implemented.");
}
getFeatureFlag<Flag extends FeatureFlag>(key: Flag): Promise<FeatureFlagValueType<Flag>> {
throw new Error("Method not implemented.");
}
checkServerMeetsVersionRequirement$(minimumRequiredServerVersion: SemVer): Observable<boolean> {
throw new Error("Method not implemented.");
}
ensureConfigFetched(): Promise<void> {
throw new Error("Method not implemented.");
}
}

View File

@ -98,3 +98,11 @@ export function cleanUserAgent(userAgent: string): string {
.replace(userAgentItem("Bitwarden", " "), "")
.replace(userAgentItem("Electron", " "), "");
}
/**
* Returns `true` if the provided string is not undefined, not null, and not empty.
* Otherwise, returns `false`.
*/
export function stringIsNotUndefinedNullAndEmpty(str: string): boolean {
return str?.length > 0;
}

Some files were not shown because too many files have changed in this diff Show More