diff --git a/.gitignore b/.gitignore index d2335a07e..f4814c623 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,6 @@ cert.pfx css .license-gen-tmp view/licenses.html -./test +/test scripts/authenticator-build-key scripts/test-runner.js \ No newline at end of file diff --git a/_locales/ar/messages.json b/_locales/ar/messages.json index 9afd6c792..c441b2e5e 100644 --- a/_locales/ar/messages.json +++ b/_locales/ar/messages.json @@ -1,10 +1,10 @@ { "extName": { - "message": "الموثق", + "message": "OTPilot Authenticator", "description": "Extension Name." }, "extShortName": { - "message": "المدقق", + "message": "OTPilot", "description": "Extension Short Name." }, "extDesc": { @@ -120,10 +120,10 @@ "description": "Message that user is required to acknowledge before clearing all data." }, "delete_all": { - "message": "إعادة تعيين Authenticator" + "message": "إعادة تعيين OTPilot" }, "delete_all_warning": { - "message": "سيؤدي هذا إلى حذف جميع بياناتك وإعادة تعيين Authenticator بالكامل. لن تتمكن من استعادة أي بيانات محذوفة! يجب مراعاة حفظ نسخة احتياطية قبل إعادة تعيين Authenticator." + "message": "سيؤدي هذا إلى حذف جميع بياناتك وإعادة تعيين OTPilot بالكامل. لن تتمكن من استعادة أي بيانات محذوفة! يجب مراعاة حفظ نسخة احتياطية قبل إعادة تعيين OTPilot." }, "security_warning": { "message": "هذا الكود الخاص سوف يستخدم لتشفير حسابك.\nلا نقدر على مساعدتك إذا فقدت هذا الكود الخاص.", @@ -489,7 +489,7 @@ "message": "يمنح صلاحية الكتابة فقط إلى الحافظة لنسخ الرموز إلى الحافظة عند النقر على الحساب." }, "permission_context_menus": { - "message": "يضيف Authenticator إلى القائمة." + "message": "يضيف OTPilot إلى القائمة." }, "permission_sync_clock": { "message": "يسمح بمزامنة الساعة مع جوجل." diff --git a/_locales/bg/messages.json b/_locales/bg/messages.json index 74bd8712b..b06f2bd9b 100644 --- a/_locales/bg/messages.json +++ b/_locales/bg/messages.json @@ -1,10 +1,10 @@ { "extName": { - "message": "Удостоверител", + "message": "OTPilot Authenticator", "description": "Extension Name." }, "extShortName": { - "message": "Удостоверяване", + "message": "OTPilot", "description": "Extension Short Name." }, "extDesc": { diff --git a/_locales/bn/messages.json b/_locales/bn/messages.json index acef2f20c..d98f6da86 100644 --- a/_locales/bn/messages.json +++ b/_locales/bn/messages.json @@ -1,10 +1,10 @@ { "extName": { - "message": "অথেন্টিকেটর", + "message": "OTPilot Authenticator", "description": "Extension Name." }, "extShortName": { - "message": "অথেন্টিকেটর", + "message": "OTPilot", "description": "Extension Short Name." }, "extDesc": { diff --git a/_locales/ca/messages.json b/_locales/ca/messages.json index 1bc087388..54136f311 100644 --- a/_locales/ca/messages.json +++ b/_locales/ca/messages.json @@ -1,14 +1,14 @@ { "extName": { - "message": "Authenticator", + "message": "OTPilot Authenticator", "description": "Extension Name." }, "extShortName": { - "message": "Authenticator", + "message": "OTPilot", "description": "Extension Short Name." }, "extDesc": { - "message": "Authenticator genera codis d'autenticació en dos passos al vostre navegador.", + "message": "OTPilot genera codis d'autenticació en dos passos al vostre navegador.", "description": "Extension Description." }, "added": { @@ -120,10 +120,10 @@ "description": "Message that user is required to acknowledge before clearing all data." }, "delete_all": { - "message": "Reinicialitza l'Authenticator" + "message": "Reinicialitza l'OTPilot" }, "delete_all_warning": { - "message": "Això esborrarà totes les vostres dades i reiniciarà completament l'Authenticator. No podreu recuperar cap dada un cop esborrades! Considereu de fer una còpia de seguretat abans de reiniciar l'Authenticator." + "message": "Això esborrarà totes les vostres dades i reiniciarà completament l'OTPilot. No podreu recuperar cap dada un cop esborrades! Considereu de fer una còpia de seguretat abans de reiniciar l'OTPilot." }, "security_warning": { "message": "Aquesta contrasenya s'emprarà per xifrar els vostres comptes. Ningú us podrà ajudar si oblideu la contrasenya.", @@ -489,7 +489,7 @@ "message": "Atorga accés només d'escriptura al porta-retalls per copiar codis al porta-retalls quan feu clic al compte." }, "permission_context_menus": { - "message": "Afegeix Authenticator al menú contextual." + "message": "Afegeix OTPilot al menú contextual." }, "permission_sync_clock": { "message": "Permet la sincronització del rellotge amb Google." diff --git a/_locales/cs/messages.json b/_locales/cs/messages.json index d944e99cb..8d2986612 100644 --- a/_locales/cs/messages.json +++ b/_locales/cs/messages.json @@ -1,14 +1,14 @@ { "extName": { - "message": "Authenticator", + "message": "OTPilot Authenticator", "description": "Extension Name." }, "extShortName": { - "message": "Authenticator", + "message": "OTPilot", "description": "Extension Short Name." }, "extDesc": { - "message": "Authenticator generuje kódy dvoufázového ověření ve Vašem prohlížeči.", + "message": "OTPilot generuje kódy dvoufázového ověření ve Vašem prohlížeči.", "description": "Extension Description." }, "added": { @@ -489,7 +489,7 @@ "message": "Umožňuje přístup do schránky pouze pro zápis a kopírování kódů do schránky po kliknutí na účet." }, "permission_context_menus": { - "message": "Přidá Authenticator do kontextové nabídky." + "message": "Přidá OTPilot do kontextové nabídky." }, "permission_sync_clock": { "message": "Umožnit synchronizaci hodin s Google." diff --git a/_locales/da/messages.json b/_locales/da/messages.json index d7974aa39..38227b7c2 100644 --- a/_locales/da/messages.json +++ b/_locales/da/messages.json @@ -1,14 +1,14 @@ { "extName": { - "message": "Autentificering", + "message": "OTPilot Authenticator", "description": "Extension Name." }, "extShortName": { - "message": "Autentificering", + "message": "OTPilot", "description": "Extension Short Name." }, "extDesc": { - "message": "Authenticator genererer to-faktor autentificeringskoder i din browser.", + "message": "OTPilot genererer to-faktor autentificeringskoder i din browser.", "description": "Extension Description." }, "added": { @@ -123,7 +123,7 @@ "message": "Nulstil Autentificering" }, "delete_all_warning": { - "message": "Dette vil slette alle dine data og helt nulstille Authenticator. Du vil ikke være i stand til at gendanne slettede data! Du bør overveje at gemme en sikkerhedskopi, før du nulstiller Authenticator." + "message": "Dette vil slette alle dine data og helt nulstille OTPilot. Du vil ikke være i stand til at gendanne slettede data! Du bør overveje at gemme en sikkerhedskopi, før du nulstiller OTPilot." }, "security_warning": { "message": "Denne adgangskode bruges til at kryptere dine konti. Ingen kan hjælpe dig, hvis du glemmer adgangskoden.", @@ -489,7 +489,7 @@ "message": "Giver skrivebeskyttet adgang til udklipsholderen for at kopiere koder til udklipsholderen, når du klikker på kontoen." }, "permission_context_menus": { - "message": "Tilføjer Authenticator til kontekstmenuen." + "message": "Tilføjer OTPilot til kontekstmenuen." }, "permission_sync_clock": { "message": "Tillader ur-synkronisering med Google." diff --git a/_locales/de/messages.json b/_locales/de/messages.json index 49f1197d3..6e44d08db 100644 --- a/_locales/de/messages.json +++ b/_locales/de/messages.json @@ -1,14 +1,14 @@ { "extName": { - "message": "Authentifizierung", + "message": "OTPilot Authenticator", "description": "Extension Name." }, "extShortName": { - "message": "Authenticator", + "message": "OTPilot", "description": "Extension Short Name." }, "extDesc": { - "message": "Authenticator erzeugt Zwei-Faktor-Authentifizierungscodes in Ihrem Browser.", + "message": "OTPilot erzeugt Zwei-Faktor-Authentifizierungscodes in Ihrem Browser.", "description": "Extension Description." }, "added": { @@ -120,10 +120,10 @@ "description": "Message that user is required to acknowledge before clearing all data." }, "delete_all": { - "message": "Authenticator zurücksetzen" + "message": "OTPilot zurücksetzen" }, "delete_all_warning": { - "message": "Dies wird alle Ihre Daten löschen und den Authenticator komplett zurücksetzen. Sie werden keine gelöschten Daten wiederherstellen können! Sie sollten erwägen, ein Backup zu speichern, bevor Sie den Authenticator zurücksetzen." + "message": "Dies wird alle Ihre Daten löschen und den OTPilot komplett zurücksetzen. Sie werden keine gelöschten Daten wiederherstellen können! Sie sollten erwägen, ein Backup zu speichern, bevor Sie den OTPilot zurücksetzen." }, "security_warning": { "message": "Dieses Passwort wird für die Verschlüsselung ihrer Konten/Accounts benutzt werden. Niemand kann ihnen helfen, wenn Sie dieses Passwort vergessen.", @@ -489,7 +489,7 @@ "message": "Gewährt Schreibzugriff auf die Zwischenablage, um Codes in die Zwischenablage zu kopieren, wenn Sie auf das Konto klicken." }, "permission_context_menus": { - "message": "Fügt Authenticator dem Kontextmenü hinzu." + "message": "Fügt OTPilot dem Kontextmenü hinzu." }, "permission_sync_clock": { "message": "Synchronisierung mit Google zulassen." diff --git a/_locales/el/messages.json b/_locales/el/messages.json index 8aead6ffc..1dcf8312f 100644 --- a/_locales/el/messages.json +++ b/_locales/el/messages.json @@ -1,10 +1,10 @@ { "extName": { - "message": "Επαληθευτής", + "message": "OTPilot Authenticator", "description": "Extension Name." }, "extShortName": { - "message": "Επαληθευτής", + "message": "OTPilot", "description": "Extension Short Name." }, "extDesc": { diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 1509958f1..924c79caa 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1,14 +1,14 @@ { "extName": { - "message": "Authenticator", + "message": "OTPilot Authenticator", "description": "Extension Name." }, "extShortName": { - "message": "Authenticator", + "message": "OTPilot", "description": "Extension Short Name." }, "extDesc": { - "message": "Authenticator generates two-factor authentication codes in your browser.", + "message": "OTPilot Authenticator generates two-factor (2FA) codes, with encrypted cloud backup, host-matched autofill, and security advice.", "description": "Extension Description." }, "added": { @@ -71,6 +71,10 @@ "message": "Issuer", "description": "Issuer." }, + "host": { + "message": "Website", + "description": "Bound website host used to restrict autofill to a matching page." + }, "secret": { "message": "Secret", "description": "Secret." @@ -120,10 +124,10 @@ "description": "Message that user is required to acknowledge before clearing all data." }, "delete_all": { - "message": "Reset Authenticator" + "message": "Reset OTPilot" }, "delete_all_warning": { - "message": "This will delete all of your data and completely reset Authenticator. You will not be able to recover any deleted data! You should consider saving a backup before resetting Authenticator." + "message": "This will delete all of your data and completely reset OTPilot. You will not be able to recover any deleted data! You should consider saving a backup before resetting OTPilot." }, "security_warning": { "message": "This password will be used to encrypt your accounts. No one can help you if you forget the password.", @@ -241,6 +245,38 @@ "message": "Import OTP URLs", "description": "Import OTP URLs. Shown as add account method." }, + "import_drop_file": { + "message": "Drop a backup file or click to browse", + "description": "File import dropzone title" + }, + "import_file_accepts": { + "message": "Accepts .json and .txt exports", + "description": "File import dropzone hint" + }, + "import_choose_file": { + "message": "Choose file", + "description": "File import dropzone button" + }, + "import_encrypted_note": { + "message": "If the file is encrypted, you'll be asked for its passphrase after selecting it.", + "description": "File import note about encrypted backups" + }, + "import_enc_required": { + "message": "Encrypted — passphrase required", + "description": "Badge on a picked encrypted backup file" + }, + "import_pass_placeholder": { + "message": "Enter the file's passphrase", + "description": "Passphrase input placeholder for encrypted import" + }, + "import_pass_hint": { + "message": "This is the password you set when you created the backup.", + "description": "Hint under the import passphrase field" + }, + "import_decrypt": { + "message": "Decrypt & import", + "description": "Button to decrypt and import an encrypted backup" + }, "import_backup_qr_partly_failed": { "message": "Import successful, but some QR codes could not be recognized.", "description": "Import successful, but some QR image cannot be recognized." @@ -305,6 +341,106 @@ "message": "Theme", "description": "Theme" }, + "settings_appearance": { + "message": "Appearance", + "description": "Settings section: appearance" + }, + "settings_general": { + "message": "General", + "description": "Settings section: general" + }, + "edit_accounts": { + "message": "Edit accounts", + "description": "Header title while in edit mode" + }, + "show_qr": { + "message": "Show QR code", + "description": "Context-menu action to show an account's QR code" + }, + "pin_to_top": { + "message": "Pin to top", + "description": "Context-menu action to pin an account to the top" + }, + "unpin": { + "message": "Unpin", + "description": "Context-menu action to unpin an account" + }, + "vault_locked": { + "message": "Vault locked", + "description": "Title on the unlock screen" + }, + "unlock": { + "message": "Unlock", + "description": "Unlock button" + }, + "qr_transfer_title": { + "message": "Transfer this account", + "description": "QR sheet caption title" + }, + "qr_transfer_desc": { + "message": "Scan this code with OTPilot on another device to move this account across — it never leaves your devices.", + "description": "QR sheet caption body" + }, + "backup_on_device": { + "message": "On this device", + "description": "Backup section: local" + }, + "backup_cloud_sync": { + "message": "Cloud sync", + "description": "Backup section: cloud" + }, + "backup_requires_password": { + "message": "Set a password first to use cloud backup, so your secrets are encrypted before they leave this device.", + "description": "Shown in the cloud backup section when no master password is set." + }, + "backup_connected": { + "message": "Connected", + "description": "Cloud provider connected status" + }, + "backup_not_connected": { + "message": "Not connected", + "description": "Cloud provider not-connected status" + }, + "showing_codes_for": { + "message": "Showing codes for", + "description": "Smart-filter banner prefix, followed by the site domain" + }, + "other_accounts": { + "message": "Other accounts", + "description": "Divider above non-matching accounts in the filter view" + }, + "filter_to_site": { + "message": "Filter to", + "description": "Banner prefix to re-apply the site filter, followed by the domain" + }, + "onboarding_title": { + "message": "Codes that travel with you", + "description": "Onboarding headline" + }, + "onboarding_subtitle": { + "message": "Generate secure two-factor codes for every account — encrypted, offline, and always one tap from copy.", + "description": "Onboarding subtitle" + }, + "onboarding_feature_offline": { + "message": "Works fully offline — nothing leaves your device", + "description": "Onboarding feature bullet" + }, + "onboarding_feature_encrypted": { + "message": "Encrypted vault with optional password lock", + "description": "Onboarding feature bullet" + }, + "onboarding_feature_scan": { + "message": "Scan a QR or import an existing backup", + "description": "Onboarding feature bullet" + }, + "onboarding_get_started": { + "message": "Get started", + "description": "Onboarding primary button" + }, + "onboarding_import": { + "message": "I have a backup to import", + "description": "Onboarding secondary button" + }, "theme_light": { "message": "Light", "description": "Light theme" @@ -313,6 +449,10 @@ "message": "Dark", "description": "Dark theme" }, + "theme_auto": { + "message": "Auto", + "description": "Follow the system light/dark setting" + }, "theme_simple": { "message": "Simple", "description": "Simple theme" @@ -341,6 +481,22 @@ "message": "Sign in", "description": "Sign in to 3rd party storage services" }, + "drive_sync_title": { + "message": "Sync with Google Drive", + "description": "Drive connect page title" + }, + "drive_sync_desc": { + "message": "Keep an encrypted copy of your accounts in your own Google Drive. Codes are encrypted before they leave this device.", + "description": "Drive connect page description" + }, + "cloud_e2e_note": { + "message": "End-to-end encrypted before upload", + "description": "Reassurance note on the cloud connect page" + }, + "drive_sign_in": { + "message": "Sign in to Google", + "description": "Drive sign-in button" + }, "sign_in_business": { "message": "Sign in (Business)", "description": "Sign in to 3rd party storage services" @@ -489,7 +645,7 @@ "message": "Grants write-only access to the clipboard to copy codes to clipboard when you click on the account." }, "permission_context_menus": { - "message": "Adds Authenticator to context menu." + "message": "Adds OTPilot to context menu." }, "permission_sync_clock": { "message": "Allows clock sync with Google." @@ -520,5 +676,9 @@ }, "activate_auto_filter": { "message": "Warning: Smart filter loosely matches the domain name to an account. Always verify that you are on the correct website before entering a code!" + }, + "backup_unavailable": { + "message": "Not available yet", + "description": "Shown on a cloud provider that is not implemented yet." } } diff --git a/_locales/es/messages.json b/_locales/es/messages.json index 87c6775e5..88d5a8657 100644 --- a/_locales/es/messages.json +++ b/_locales/es/messages.json @@ -1,14 +1,14 @@ { "extName": { - "message": "Autenticador", + "message": "OTPilot Authenticator", "description": "Extension Name." }, "extShortName": { - "message": "Autenticación", + "message": "OTPilot", "description": "Extension Short Name." }, "extDesc": { - "message": "Authenticator genera códigos de autenticación de dos factores en su navegador.", + "message": "OTPilot genera códigos de autenticación de dos factores en su navegador.", "description": "Extension Description." }, "added": { @@ -123,7 +123,7 @@ "message": "Restablecer auntenticador" }, "delete_all_warning": { - "message": "Esto eliminará todos tus datos y restablecerá completamente Authenticator. ¡No podrás recuperar ningún dato eliminado! Deberías considerar guardar una copia de seguridad antes de resetear Authenticator." + "message": "Esto eliminará todos tus datos y restablecerá completamente OTPilot. ¡No podrás recuperar ningún dato eliminado! Deberías considerar guardar una copia de seguridad antes de resetear OTPilot." }, "security_warning": { "message": "Esta contraseña se utilizará para cifrar tus cuentas. Nadie puede ayudarte si olvidas la contraseña.", @@ -489,7 +489,7 @@ "message": "Otorga acceso de sólo escritura al portapapeles para copiar codigos al portapapeles cuando haga clic en la cuenta." }, "permission_context_menus": { - "message": "Agregar Authenticator al menú contextual." + "message": "Agregar OTPilot al menú contextual." }, "permission_sync_clock": { "message": "Habilitar sincronización del reloj con Google." diff --git a/_locales/et/messages.json b/_locales/et/messages.json index 802df3bb0..5888ea657 100644 --- a/_locales/et/messages.json +++ b/_locales/et/messages.json @@ -1,10 +1,10 @@ { "extName": { - "message": "Autentikaator", + "message": "OTPilot Authenticator", "description": "Extension Name." }, "extShortName": { - "message": "Autentikaator", + "message": "OTPilot", "description": "Extension Short Name." }, "extDesc": { @@ -489,7 +489,7 @@ "message": "Grants write-only access to the clipboard to copy codes to clipboard when you click on the account." }, "permission_context_menus": { - "message": "Adds Authenticator to context menu." + "message": "Adds OTPilot to context menu." }, "permission_sync_clock": { "message": "Allows clock sync with Google." diff --git a/_locales/fa/messages.json b/_locales/fa/messages.json index d2f67ac13..e4e71a629 100644 --- a/_locales/fa/messages.json +++ b/_locales/fa/messages.json @@ -1,14 +1,14 @@ { "extName": { - "message": "Authenticator", + "message": "OTPilot Authenticator", "description": "Extension Name." }, "extShortName": { - "message": "احراز هویت کننده", + "message": "OTPilot", "description": "Extension Short Name." }, "extDesc": { - "message": "افزونه Authenticator کدهای تأیید هویت دو مرحله‌ای را در مرورگر شما تولید می‌کند.", + "message": "افزونه OTPilot کدهای تأیید هویت دو مرحله‌ای را در مرورگر شما تولید می‌کند.", "description": "Extension Description." }, "added": { @@ -120,10 +120,10 @@ "description": "Message that user is required to acknowledge before clearing all data." }, "delete_all": { - "message": "بازنشانی Authenticator" + "message": "بازنشانی OTPilot" }, "delete_all_warning": { - "message": "این تمام داده‌های شما را حذف کرده و افزونه Authenticator را به طور کامل بازنشانی می‌کند. شما بعداً قادر نخواهید بود داده‌های حذف شده را بازیابی کنید! شما باید قبل از بازنشانی افزونه Authenticator از داده‌های خود نسخه پشتیبان تهیه کنید." + "message": "این تمام داده‌های شما را حذف کرده و افزونه OTPilot را به طور کامل بازنشانی می‌کند. شما بعداً قادر نخواهید بود داده‌های حذف شده را بازیابی کنید! شما باید قبل از بازنشانی افزونه OTPilot از داده‌های خود نسخه پشتیبان تهیه کنید." }, "security_warning": { "message": "این کلمه عبور برای رمزنگاری حساب‌های شما استفاده خواهد شد. اگر این کلمه عبور را فراموش کنید راه دیگری برای بازگردانی آن وجود ندارد.", @@ -489,7 +489,7 @@ "message": "وقتی روی حساب کلیک می‌کنید، به کلیپ‌بورد دسترسی فقط نوشتنی برای کپی کردن کدها در کلیپ‌بورد می‌دهد." }, "permission_context_menus": { - "message": "Authenticator را به منوی زمینه اضافه می کند." + "message": "OTPilot را به منوی زمینه اضافه می کند." }, "permission_sync_clock": { "message": "اجازه همگام سازی ساعت با Google را می دهد." diff --git a/_locales/fi/messages.json b/_locales/fi/messages.json index 26458d6e0..c5279b9d5 100644 --- a/_locales/fi/messages.json +++ b/_locales/fi/messages.json @@ -1,14 +1,14 @@ { "extName": { - "message": "Authenticator", + "message": "OTPilot Authenticator", "description": "Extension Name." }, "extShortName": { - "message": "Authenticator", + "message": "OTPilot", "description": "Extension Short Name." }, "extDesc": { - "message": "Authenticator-laajennus tuottaa kaksivaiheisen todennuksen tunnuslukuja selaimessasi.", + "message": "OTPilot-laajennus tuottaa kaksivaiheisen todennuksen tunnuslukuja selaimessasi.", "description": "Extension Description." }, "added": { @@ -120,10 +120,10 @@ "description": "Message that user is required to acknowledge before clearing all data." }, "delete_all": { - "message": "Palauta Authenticator" + "message": "Palauta OTPilot" }, "delete_all_warning": { - "message": "Tämä poistaa kaikki tiedot ja palauttaa Authenticatorin täysin. Poistettujen tietojen palautus ei ole mahdollista! Ennen palautusta on syytä harkita varmuuskopiointia." + "message": "Tämä poistaa kaikki tiedot ja palauttaa OTPilotin täysin. Poistettujen tietojen palautus ei ole mahdollista! Ennen palautusta on syytä harkita varmuuskopiointia." }, "security_warning": { "message": "Salasanaa käytetään tiliesi salaukseen. Kukaan ei voi auttaa, jos unohdat sen.", @@ -489,7 +489,7 @@ "message": "Myöntää pelkän kirjoitusoikeuden leikepöydälle koodien kopiointiin tilejä painettaessa." }, "permission_context_menus": { - "message": "Lisää Authenticator sisältövalikkoon." + "message": "Lisää OTPilot sisältövalikkoon." }, "permission_sync_clock": { "message": "Sallii kellon synkronoinnin Googlen kanssa." diff --git a/_locales/fr/messages.json b/_locales/fr/messages.json index 05a8f0593..03a537eb2 100644 --- a/_locales/fr/messages.json +++ b/_locales/fr/messages.json @@ -1,14 +1,14 @@ { "extName": { - "message": "Authenticator", + "message": "OTPilot Authenticator", "description": "Extension Name." }, "extShortName": { - "message": "Authenticator", + "message": "OTPilot", "description": "Extension Short Name." }, "extDesc": { - "message": "Authenticator génère des codes d'authentification à deux facteurs dans votre navigateur.", + "message": "OTPilot génère des codes d'authentification à deux facteurs dans votre navigateur.", "description": "Extension Description." }, "added": { @@ -489,7 +489,7 @@ "message": "Donne un accès en écriture seule au presse-papiers pour copier des codes dans le presse-papiers lorsque vous cliquez sur le compte." }, "permission_context_menus": { - "message": "Ajoute Authenticator au menu contextuel." + "message": "Ajoute OTPilot au menu contextuel." }, "permission_sync_clock": { "message": "Autoriser la synchronisation de l'horloge avec Google." diff --git a/_locales/fy/messages.json b/_locales/fy/messages.json index aff586db0..ba8bd8605 100644 --- a/_locales/fy/messages.json +++ b/_locales/fy/messages.json @@ -1,14 +1,14 @@ { "extName": { - "message": "Authenticator", + "message": "OTPilot Authenticator", "description": "Extension Name." }, "extShortName": { - "message": "Authenticator", + "message": "OTPilot", "description": "Extension Short Name." }, "extDesc": { - "message": "Authenticator generearret 2-stapsferifikaasjekoades yn jo browser.", + "message": "OTPilot generearret 2-stapsferifikaasjekoades yn jo browser.", "description": "Extension Description." }, "added": { @@ -120,10 +120,10 @@ "description": "Message that user is required to acknowledge before clearing all data." }, "delete_all": { - "message": "Authenticator opnij inisjalisearje" + "message": "OTPilot opnij inisjalisearje" }, "delete_all_warning": { - "message": "Dit sil al jo gegevens fuortsmite en de Authenticator folslein opnij inisjalisearje. Jo binne net yn steat om fuortsmiten gegevens te werstellen! Oerwaagje om in reservekopy te bewarjen eardat jo de Authenticator opnij inisjalisearje." + "message": "Dit sil al jo gegevens fuortsmite en de OTPilot folslein opnij inisjalisearje. Jo binne net yn steat om fuortsmiten gegevens te werstellen! Oerwaagje om in reservekopy te bewarjen eardat jo de OTPilot opnij inisjalisearje." }, "security_warning": { "message": "Dit wachtwurd wurdt brûkt foar it fersiferjen fan jo accounts. Net ien kin jo helpe as jo it wachtwurd ferjitte.", @@ -489,7 +489,7 @@ "message": "Jout allinnich-skriuwrjochttagong ta it klamboerd om koaden nei it klamboerd te kopiearjen wannear’t jo op de account klikke." }, "permission_context_menus": { - "message": "Foeget Authenticator ta oan kontekstmenu." + "message": "Foeget OTPilot ta oan kontekstmenu." }, "permission_sync_clock": { "message": "Tiidssyngronisaasje mei Google tastean." diff --git a/_locales/he/messages.json b/_locales/he/messages.json index 32dd7e5e0..5950c7077 100644 --- a/_locales/he/messages.json +++ b/_locales/he/messages.json @@ -1,10 +1,10 @@ { "extName": { - "message": "מאמת", + "message": "OTPilot Authenticator", "description": "Extension Name." }, "extShortName": { - "message": "מאמת", + "message": "OTPilot", "description": "Extension Short Name." }, "extDesc": { @@ -489,7 +489,7 @@ "message": "Grants write-only access to the clipboard to copy codes to clipboard when you click on the account." }, "permission_context_menus": { - "message": "Adds Authenticator to context menu." + "message": "Adds OTPilot to context menu." }, "permission_sync_clock": { "message": "Allows clock sync with Google." diff --git a/_locales/hi/messages.json b/_locales/hi/messages.json index 2f4dc41aa..d29c58e1c 100644 --- a/_locales/hi/messages.json +++ b/_locales/hi/messages.json @@ -1,10 +1,10 @@ { "extName": { - "message": "Authentication", + "message": "OTPilot Authenticator", "description": "Extension Name." }, "extShortName": { - "message": "सत्यापन", + "message": "OTPilot", "description": "Extension Short Name." }, "extDesc": { diff --git a/_locales/hr/messages.json b/_locales/hr/messages.json index fd27ec502..0911aae08 100644 --- a/_locales/hr/messages.json +++ b/_locales/hr/messages.json @@ -1,10 +1,10 @@ { "extName": { - "message": "Autentifikator", + "message": "OTPilot Authenticator", "description": "Extension Name." }, "extShortName": { - "message": "Autentifikator", + "message": "OTPilot", "description": "Extension Short Name." }, "extDesc": { diff --git a/_locales/hu/messages.json b/_locales/hu/messages.json index 98b01572a..ce443cd89 100644 --- a/_locales/hu/messages.json +++ b/_locales/hu/messages.json @@ -1,10 +1,10 @@ { "extName": { - "message": "Hitelesítő", + "message": "OTPilot Authenticator", "description": "Extension Name." }, "extShortName": { - "message": "Hitelesítő", + "message": "OTPilot", "description": "Extension Short Name." }, "extDesc": { @@ -123,7 +123,7 @@ "message": "Hitelesítő visszaállítása" }, "delete_all_warning": { - "message": "\nEzzel törli az összes adatot, és teljesen visszaállítja a Hitelesítőt. A törölt adatokat nem fogja tudni visszaállítani! Az Authenticator alaphelyzetbe állítása előtt fontolóra kell vennie egy biztonsági másolat készítését." + "message": "\nEzzel törli az összes adatot, és teljesen visszaállítja a Hitelesítőt. A törölt adatokat nem fogja tudni visszaállítani! Az OTPilot alaphelyzetbe állítása előtt fontolóra kell vennie egy biztonsági másolat készítését." }, "security_warning": { "message": "Ez a jelszó lesz használva a fiókjai titkosításához. Senki sem tud segíteni, ha elfelejti a jelszavát.", diff --git a/_locales/hy/messages.json b/_locales/hy/messages.json index c7c4ccf13..af048c9ee 100644 --- a/_locales/hy/messages.json +++ b/_locales/hy/messages.json @@ -1,14 +1,14 @@ { "extName": { - "message": "Authenticator", + "message": "OTPilot Authenticator", "description": "Extension Name." }, "extShortName": { - "message": "Authenticator", + "message": "OTPilot", "description": "Extension Short Name." }, "extDesc": { - "message": "Authenticator generates two-factor authentication codes in your browser.", + "message": "OTPilot generates two-factor authentication codes in your browser.", "description": "Extension Description." }, "added": { @@ -120,10 +120,10 @@ "description": "Message that user is required to acknowledge before clearing all data." }, "delete_all": { - "message": "Զրոյացնել «Authenticator»-ը" + "message": "Զրոյացնել «OTPilot»-ը" }, "delete_all_warning": { - "message": "Այս գործողության արդյունքում վերջնականապես կջնջվեն Ձեր բոլոր տվյալները և «Authenticator»-ի կարգավորումները։ Զրոյացում կատարելուց առաջ խորհուրդ է տրվում կատարել կրկնօրինակում։" + "message": "Այս գործողության արդյունքում վերջնականապես կջնջվեն Ձեր բոլոր տվյալները և «OTPilot»-ի կարգավորումները։ Զրոյացում կատարելուց առաջ խորհուրդ է տրվում կատարել կրկնօրինակում։" }, "security_warning": { "message": "Այս գաղտնաբառը կօգտագործվի Ձեր բոլոր հաշիվները կոդավորելու համար։ Ոչ ոք Ձեզ չի կարող օգնել, եթե հանկարծ մոռանաք այն։", @@ -489,7 +489,7 @@ "message": "Թույլատրում է փոփոխություններ կատարել կցատարում՝ հաշիվների կոդերը պատճենելու համար" }, "permission_context_menus": { - "message": "Ավելացնում է «Authenticator»-ը՝ կոնտեքստ ընտրացանկում" + "message": "Ավելացնում է «OTPilot»-ը՝ կոնտեքստ ընտրացանկում" }, "permission_sync_clock": { "message": "Թույլատրում է համաժամեցում՝ «Google»-ից" diff --git a/_locales/id/messages.json b/_locales/id/messages.json index 4172e26c9..fd07e4dd6 100644 --- a/_locales/id/messages.json +++ b/_locales/id/messages.json @@ -1,14 +1,14 @@ { "extName": { - "message": "Autentikator", + "message": "OTPilot Authenticator", "description": "Extension Name." }, "extShortName": { - "message": "Authenticator", + "message": "OTPilot", "description": "Extension Short Name." }, "extDesc": { - "message": "Authenticator menghasilkan kode verifikasi 2 langkah di browser anda.", + "message": "OTPilot menghasilkan kode verifikasi 2 langkah di browser anda.", "description": "Extension Description." }, "added": { @@ -120,7 +120,7 @@ "description": "Message that user is required to acknowledge before clearing all data." }, "delete_all": { - "message": "Reset Authenticator" + "message": "Reset OTPilot" }, "delete_all_warning": { "message": "Tindakan ini akan menghapus semua data dan mereset ulang Autentikator. Anda tidak dapat mengembalikan data yang sudah terhapus! Pastikan anda membuat Backup terlebih dahulu sebelum mereset ulang Authentikator." @@ -489,7 +489,7 @@ "message": "Memberikan akses write-only ke clipboard untuk menyalin kode ke clipboard saat Anda meng-klik akun." }, "permission_context_menus": { - "message": "Menambahkan Authenticator ke menu konteks." + "message": "Menambahkan OTPilot ke menu konteks." }, "permission_sync_clock": { "message": "Mengizinkan sinkronisasi jam dengan Google." diff --git a/_locales/it/messages.json b/_locales/it/messages.json index 2ae338f2c..d6c3aa617 100644 --- a/_locales/it/messages.json +++ b/_locales/it/messages.json @@ -1,14 +1,14 @@ { "extName": { - "message": "Authenticator", + "message": "OTPilot Authenticator", "description": "Extension Name." }, "extShortName": { - "message": "Authenticator", + "message": "OTPilot", "description": "Extension Short Name." }, "extDesc": { - "message": "Authenticator genera codici di verifica a due fattori nel tuo browser.", + "message": "OTPilot genera codici di verifica a due fattori nel tuo browser.", "description": "Extension Description." }, "added": { @@ -123,7 +123,7 @@ "message": "Resetta Autenticatore" }, "delete_all_warning": { - "message": "Questo eliminerà tutti i tuoi dati e ripristinerà completamente l'Authenticator. Non potrai recuperare alcun dato eliminato! Dovresti considerare di salvare un backup, prima di ripristinare l'Authenticator." + "message": "Questo eliminerà tutti i tuoi dati e ripristinerà completamente l'OTPilot. Non potrai recuperare alcun dato eliminato! Dovresti considerare di salvare un backup, prima di ripristinare l'OTPilot." }, "security_warning": { "message": "Questa password verrà usata per cifrare i tuoi account. Se dimentichi la password non ti potrà aiutare nessuno.", @@ -489,7 +489,7 @@ "message": "Concedi l'accesso di sola scrittura agli appunti per copiarvi i codici quando clicchi sul profilo." }, "permission_context_menus": { - "message": "Aggiungi Authenticatori al menu contestuale." + "message": "Aggiungi OTPiloti al menu contestuale." }, "permission_sync_clock": { "message": "Consenti la sincronizzazione dell'orario con Google." diff --git a/_locales/ja/messages.json b/_locales/ja/messages.json index aa2b9c707..816b231e9 100644 --- a/_locales/ja/messages.json +++ b/_locales/ja/messages.json @@ -1,14 +1,14 @@ { "extName": { - "message": "Authenticator", + "message": "OTPilot Authenticator", "description": "Extension Name." }, "extShortName": { - "message": "Authenticator", + "message": "OTPilot", "description": "Extension Short Name." }, "extDesc": { - "message": "Authenticator はお使いのブラウザーで2段階認証コードを生成します。", + "message": "OTPilot はお使いのブラウザーで2段階認証コードを生成します。", "description": "Extension Description." }, "added": { @@ -520,5 +520,149 @@ }, "activate_auto_filter": { "message": "警告:スマートフィルタはアカウントのドメイン名とほぼ一致します。常にあなたが正しいウェブサイトにいることを確認してください!" + }, + "backup_cloud_sync": { + "message": "クラウド同期", + "description": "Backup section: cloud" + }, + "backup_connected": { + "message": "接続済み", + "description": "Cloud provider connected status" + }, + "backup_not_connected": { + "message": "未接続", + "description": "Cloud provider not-connected status" + }, + "backup_on_device": { + "message": "この端末", + "description": "Backup section: local" + }, + "backup_requires_password": { + "message": "クラウドバックアップを使うには、先にパスワードを設定してください。シークレットはこの端末を離れる前に暗号化されます。", + "description": "Shown in the cloud backup section when no master password is set." + }, + "edit_accounts": { + "message": "アカウントを編集", + "description": "Header title while in edit mode" + }, + "filter_to_site": { + "message": "絞り込み:", + "description": "Banner prefix to re-apply the site filter, followed by the domain" + }, + "host": { + "message": "ウェブサイト", + "description": "Bound website host used to restrict autofill to a matching page." + }, + "import_choose_file": { + "message": "ファイルを選択", + "description": "File import dropzone button" + }, + "import_decrypt": { + "message": "復号してインポート", + "description": "Button to decrypt and import an encrypted backup" + }, + "import_drop_file": { + "message": "バックアップファイルをドロップ、またはクリックして選択", + "description": "File import dropzone title" + }, + "import_enc_required": { + "message": "暗号化済み — パスフレーズが必要", + "description": "Badge on a picked encrypted backup file" + }, + "import_encrypted_note": { + "message": "ファイルが暗号化されている場合、選択後にパスフレーズの入力を求められます。", + "description": "File import note about encrypted backups" + }, + "import_file_accepts": { + "message": ".json と .txt のエクスポートに対応", + "description": "File import dropzone hint" + }, + "import_pass_hint": { + "message": "バックアップ作成時に設定したパスワードです。", + "description": "Hint under the import passphrase field" + }, + "import_pass_placeholder": { + "message": "ファイルのパスフレーズを入力", + "description": "Passphrase input placeholder for encrypted import" + }, + "onboarding_feature_encrypted": { + "message": "パスワードロック(任意)付きの暗号化保管庫", + "description": "Onboarding feature bullet" + }, + "onboarding_feature_offline": { + "message": "完全オフラインで動作 — データは端末から出ません", + "description": "Onboarding feature bullet" + }, + "onboarding_feature_scan": { + "message": "QR コードをスキャン、または既存のバックアップをインポート", + "description": "Onboarding feature bullet" + }, + "onboarding_get_started": { + "message": "はじめる", + "description": "Onboarding primary button" + }, + "onboarding_import": { + "message": "インポートするバックアップがあります", + "description": "Onboarding secondary button" + }, + "onboarding_subtitle": { + "message": "すべてのアカウントに安全な2要素認証コードを生成 — 暗号化、オフライン、ワンタップでコピー。", + "description": "Onboarding subtitle" + }, + "onboarding_title": { + "message": "持ち歩ける認証コード", + "description": "Onboarding headline" + }, + "other_accounts": { + "message": "その他のアカウント", + "description": "Divider above non-matching accounts in the filter view" + }, + "pin_to_top": { + "message": "上部に固定", + "description": "Context-menu action to pin an account to the top" + }, + "qr_transfer_desc": { + "message": "別の端末の OTPilot でこのコードをスキャンすると、このアカウントを移行できます — データが端末の外に出ることはありません。", + "description": "QR sheet caption body" + }, + "qr_transfer_title": { + "message": "このアカウントを移行", + "description": "QR sheet caption title" + }, + "settings_appearance": { + "message": "外観", + "description": "Settings section: appearance" + }, + "settings_general": { + "message": "一般", + "description": "Settings section: general" + }, + "show_qr": { + "message": "QR コードを表示", + "description": "Context-menu action to show an account's QR code" + }, + "showing_codes_for": { + "message": "次のサイトのコードを表示中:", + "description": "Smart-filter banner prefix, followed by the site domain" + }, + "theme_auto": { + "message": "自動", + "description": "Follow the system light/dark setting" + }, + "unlock": { + "message": "ロック解除", + "description": "Unlock button" + }, + "unpin": { + "message": "固定を解除", + "description": "Context-menu action to unpin an account" + }, + "vault_locked": { + "message": "保管庫はロックされています", + "description": "Title on the unlock screen" + }, + "backup_unavailable": { + "message": "未対応", + "description": "Shown on a cloud provider that is not implemented yet." } } diff --git a/_locales/ka/messages.json b/_locales/ka/messages.json index d8e35d9be..c14492abf 100644 --- a/_locales/ka/messages.json +++ b/_locales/ka/messages.json @@ -1,10 +1,10 @@ { "extName": { - "message": "აუთენთიფიკატორი", + "message": "OTPilot Authenticator", "description": "Extension Name." }, "extShortName": { - "message": "აუთენთიფიკატორი", + "message": "OTPilot", "description": "Extension Short Name." }, "extDesc": { diff --git a/_locales/kaa/messages.json b/_locales/kaa/messages.json index 31a2e3666..52742bf44 100644 --- a/_locales/kaa/messages.json +++ b/_locales/kaa/messages.json @@ -1,10 +1,10 @@ { "extName": { - "message": "Autentifikator", + "message": "OTPilot Authenticator", "description": "Extension Name." }, "extShortName": { - "message": "Autentifikator", + "message": "OTPilot", "description": "Extension Short Name." }, "extDesc": { diff --git a/_locales/ko/messages.json b/_locales/ko/messages.json index 1de263b0f..9b801cdb1 100644 --- a/_locales/ko/messages.json +++ b/_locales/ko/messages.json @@ -1,10 +1,10 @@ { "extName": { - "message": "인증 도구", + "message": "OTPilot Authenticator", "description": "Extension Name." }, "extShortName": { - "message": "인증 도구", + "message": "OTPilot", "description": "Extension Short Name." }, "extDesc": { diff --git a/_locales/lt/messages.json b/_locales/lt/messages.json index 565ef8483..0b975ded9 100644 --- a/_locales/lt/messages.json +++ b/_locales/lt/messages.json @@ -1,10 +1,10 @@ { "extName": { - "message": "Autentifikatorius", + "message": "OTPilot Authenticator", "description": "Extension Name." }, "extShortName": { - "message": "Autentifikatorius", + "message": "OTPilot", "description": "Extension Short Name." }, "extDesc": { diff --git a/_locales/lv/messages.json b/_locales/lv/messages.json index 3e17c461c..3c35a1175 100644 --- a/_locales/lv/messages.json +++ b/_locales/lv/messages.json @@ -1,10 +1,10 @@ { "extName": { - "message": "Divpakāpju kodu ģenerators", + "message": "OTPilot Authenticator", "description": "Extension Name." }, "extShortName": { - "message": "Divpakāpju kodu ģenerators", + "message": "OTPilot", "description": "Extension Short Name." }, "extDesc": { diff --git a/_locales/nl/messages.json b/_locales/nl/messages.json index e627be5bf..e034acdde 100644 --- a/_locales/nl/messages.json +++ b/_locales/nl/messages.json @@ -1,14 +1,14 @@ { "extName": { - "message": "Authenticator", + "message": "OTPilot Authenticator", "description": "Extension Name." }, "extShortName": { - "message": "Authenticator", + "message": "OTPilot", "description": "Extension Short Name." }, "extDesc": { - "message": "Authenticator genereert 2-stapsverificatiecodes in uw browser.", + "message": "OTPilot genereert 2-stapsverificatiecodes in uw browser.", "description": "Extension Description." }, "added": { @@ -120,10 +120,10 @@ "description": "Message that user is required to acknowledge before clearing all data." }, "delete_all": { - "message": "Authenticator herinitialiseren" + "message": "OTPilot herinitialiseren" }, "delete_all_warning": { - "message": "Dit zal al uw gegevens verwijderen en de Authenticator volledig herinitialiseren. U bent niet in staat om verwijderde gegevens te herstellen! Overweeg om een back-up op te slaan voordat u de Authenticator herinitialiseert." + "message": "Dit zal al uw gegevens verwijderen en de OTPilot volledig herinitialiseren. U bent niet in staat om verwijderde gegevens te herstellen! Overweeg om een back-up op te slaan voordat u de OTPilot herinitialiseert." }, "security_warning": { "message": "Dit wachtwoord wordt gebruikt voor het versleutelen van uw accounts. Niemand kan u helpen als u het wachtwoord vergeet.", @@ -489,7 +489,7 @@ "message": "Geeft alleen-schrijventoegang tot het klembord om codes naar het klembord te kopiëren wanneer u op de account klikt." }, "permission_context_menus": { - "message": "Voegt Authenticator toe aan contextmenu." + "message": "Voegt OTPilot toe aan contextmenu." }, "permission_sync_clock": { "message": "Tijdssynchronisatie met Google toestaan." diff --git a/_locales/no/messages.json b/_locales/no/messages.json index 3d7e56c1b..5cc9eb60a 100644 --- a/_locales/no/messages.json +++ b/_locales/no/messages.json @@ -1,14 +1,14 @@ { "extName": { - "message": "Autentisering", + "message": "OTPilot Authenticator", "description": "Extension Name." }, "extShortName": { - "message": "Authenticator", + "message": "OTPilot", "description": "Extension Short Name." }, "extDesc": { - "message": "Authenticator genererer to-faktor autentiseringskoder i nettleseren din.", + "message": "OTPilot genererer to-faktor autentiseringskoder i nettleseren din.", "description": "Extension Description." }, "added": { diff --git a/_locales/pl/messages.json b/_locales/pl/messages.json index df2f4485d..db7790d96 100644 --- a/_locales/pl/messages.json +++ b/_locales/pl/messages.json @@ -1,14 +1,14 @@ { "extName": { - "message": "Authenticator\n", + "message": "OTPilot Authenticator", "description": "Extension Name." }, "extShortName": { - "message": "Uwierzytelnianie", + "message": "OTPilot", "description": "Extension Short Name." }, "extDesc": { - "message": "Authenticator generuje w przeglądarce kody uwierzytelniania dwuskładnikowego.", + "message": "OTPilot generuje w przeglądarce kody uwierzytelniania dwuskładnikowego.", "description": "Extension Description." }, "added": { @@ -120,10 +120,10 @@ "description": "Message that user is required to acknowledge before clearing all data." }, "delete_all": { - "message": "Zresetuj dodatek Authenticator" + "message": "Zresetuj dodatek OTPilot" }, "delete_all_warning": { - "message": "Spowoduje to usunięcie wszystkich danych i całkowite zresetowanie dodatku Authenticator. Nie będzie można odzyskać usuniętych danych! Przed zresetowaniem dodatku Authenticator należy rozważyć zapisanie kopii zapasowej." + "message": "Spowoduje to usunięcie wszystkich danych i całkowite zresetowanie dodatku OTPilot. Nie będzie można odzyskać usuniętych danych! Przed zresetowaniem dodatku OTPilot należy rozważyć zapisanie kopii zapasowej." }, "security_warning": { "message": "To hasło będzie służyć szyfrowaniu Twoich kont. Nikt ci nie pomoże, jeśli zapomnisz hasła.", @@ -489,7 +489,7 @@ "message": "Przyznaje dostęp tylko do zapisu do schowka w celu skopiowania kodów do schowka po kliknięciu konta." }, "permission_context_menus": { - "message": "Dodaje dodatek Authenticator do menu kontekstowego." + "message": "Dodaje dodatek OTPilot do menu kontekstowego." }, "permission_sync_clock": { "message": "Umożliwia synchronizację zegara z Google." diff --git a/_locales/pt/messages.json b/_locales/pt/messages.json index 9cc98065f..83584c54f 100644 --- a/_locales/pt/messages.json +++ b/_locales/pt/messages.json @@ -1,10 +1,10 @@ { "extName": { - "message": "Autenticador", + "message": "OTPilot Authenticator", "description": "Extension Name." }, "extShortName": { - "message": "Autenticador", + "message": "OTPilot", "description": "Extension Short Name." }, "extDesc": { @@ -489,7 +489,7 @@ "message": "Grants write-only access to the clipboard to copy codes to clipboard when you click on the account." }, "permission_context_menus": { - "message": "Adds Authenticator to context menu." + "message": "Adds OTPilot to context menu." }, "permission_sync_clock": { "message": "Allows clock sync with Google." diff --git a/_locales/pt_BR/messages.json b/_locales/pt_BR/messages.json index 938af5cbd..d41689d69 100644 --- a/_locales/pt_BR/messages.json +++ b/_locales/pt_BR/messages.json @@ -1,10 +1,10 @@ { "extName": { - "message": "Autenticador", + "message": "OTPilot Authenticator", "description": "Extension Name." }, "extShortName": { - "message": "Autenticador", + "message": "OTPilot", "description": "Extension Short Name." }, "extDesc": { diff --git a/_locales/ro/messages.json b/_locales/ro/messages.json index 8eb8cafbe..464d9653b 100644 --- a/_locales/ro/messages.json +++ b/_locales/ro/messages.json @@ -1,14 +1,14 @@ { "extName": { - "message": "Authenticator", + "message": "OTPilot Authenticator", "description": "Extension Name." }, "extShortName": { - "message": "Authenticator", + "message": "OTPilot", "description": "Extension Short Name." }, "extDesc": { - "message": "Authenticator generează coduri de verificare pentru al doilea pas în browserul tău.", + "message": "OTPilot generează coduri de verificare pentru al doilea pas în browserul tău.", "description": "Extension Description." }, "added": { @@ -489,7 +489,7 @@ "message": "Oferă acces doar la scriere în clipboard pentru a copia coduri în clipboard atunci când faceți clic pe cont." }, "permission_context_menus": { - "message": "Adds Authenticator to context menu." + "message": "Adds OTPilot to context menu." }, "permission_sync_clock": { "message": "Allows clock sync with Google." diff --git a/_locales/ru/messages.json b/_locales/ru/messages.json index e226d08b7..64fd11a23 100644 --- a/_locales/ru/messages.json +++ b/_locales/ru/messages.json @@ -1,10 +1,10 @@ { "extName": { - "message": "Аутентификатор", + "message": "OTPilot Authenticator", "description": "Extension Name." }, "extShortName": { - "message": "Аутентификатор", + "message": "OTPilot", "description": "Extension Short Name." }, "extDesc": { @@ -123,7 +123,7 @@ "message": "Сбросить ключ идентификации." }, "delete_all_warning": { - "message": "Это удалит все ваши данные и полностью сбросит Authenticator. Вы не сможете восстановить любые удаленные данные! Вам следует сохранить резервную копию перед сбросом аутентификатора." + "message": "Это удалит все ваши данные и полностью сбросит OTPilot. Вы не сможете восстановить любые удаленные данные! Вам следует сохранить резервную копию перед сбросом аутентификатора." }, "security_warning": { "message": "Этот пароль будет использоваться для шифрования ваших учетных записей. Никто не может помочь вам, если вы забыли пароль.", diff --git a/_locales/sq/messages.json b/_locales/sq/messages.json index 5a1c36461..8de98e15e 100644 --- a/_locales/sq/messages.json +++ b/_locales/sq/messages.json @@ -1,10 +1,10 @@ { "extName": { - "message": "Autentifikuesi", + "message": "OTPilot Authenticator", "description": "Extension Name." }, "extShortName": { - "message": "Autentifikuesi", + "message": "OTPilot", "description": "Extension Short Name." }, "extDesc": { @@ -120,10 +120,10 @@ "description": "Message that user is required to acknowledge before clearing all data." }, "delete_all": { - "message": "Kthe në gjendje fillestare Authenticator-in" + "message": "Kthe në gjendje fillestare OTPilot-in" }, "delete_all_warning": { - "message": "Kjo do të fshijë të gjithë plotësisht të dhënat tuaja dhe kthejë Authenticator në gjendje fillestare. Ju nuk do të jeni në gjendje të riktheni të dhënat e fshira! Ju duhet të konsideroni ruajtjen e një kopje rezervë përpara rikthimit në gjëndje fillestare të Authenticator-it." + "message": "Kjo do të fshijë të gjithë plotësisht të dhënat tuaja dhe kthejë OTPilot në gjendje fillestare. Ju nuk do të jeni në gjendje të riktheni të dhënat e fshira! Ju duhet të konsideroni ruajtjen e një kopje rezervë përpara rikthimit në gjëndje fillestare të OTPilot-it." }, "security_warning": { "message": "Ky fjalëkalim do të përdoret për të kriptuar llogarinë tënde. Askush nuk mund tju ndihmojë ju nëse ju harroni fjalëkalimin.", diff --git a/_locales/sr/messages.json b/_locales/sr/messages.json index 9e4ccff64..7a4d8fb8e 100644 --- a/_locales/sr/messages.json +++ b/_locales/sr/messages.json @@ -1,10 +1,10 @@ { "extName": { - "message": "Аутентификатор", + "message": "OTPilot Authenticator", "description": "Extension Name." }, "extShortName": { - "message": "Аутентификатор", + "message": "OTPilot", "description": "Extension Short Name." }, "extDesc": { @@ -489,7 +489,7 @@ "message": "Grants write-only access to the clipboard to copy codes to clipboard when you click on the account." }, "permission_context_menus": { - "message": "Adds Authenticator to context menu." + "message": "Adds OTPilot to context menu." }, "permission_sync_clock": { "message": "Allows clock sync with Google." diff --git a/_locales/sv/messages.json b/_locales/sv/messages.json index cd6e1c952..4814b9d55 100644 --- a/_locales/sv/messages.json +++ b/_locales/sv/messages.json @@ -1,14 +1,14 @@ { "extName": { - "message": "Authenticator", + "message": "OTPilot Authenticator", "description": "Extension Name." }, "extShortName": { - "message": "Authenticator", + "message": "OTPilot", "description": "Extension Short Name." }, "extDesc": { - "message": "Authenticator genererar 2-stegs verifieringskoder i din webbläsare.", + "message": "OTPilot genererar 2-stegs verifieringskoder i din webbläsare.", "description": "Extension Description." }, "added": { @@ -123,7 +123,7 @@ "message": "Återställ autentiserare" }, "delete_all_warning": { - "message": "Det här kommer att radera alla dina data och återställa Authenticator helt. Du kommer inte kunna återskapa några raderade data! Du bör överväga att spara en säkerhetskopia innan du återställer Authenticator." + "message": "Det här kommer att radera alla dina data och återställa OTPilot helt. Du kommer inte kunna återskapa några raderade data! Du bör överväga att spara en säkerhetskopia innan du återställer OTPilot." }, "security_warning": { "message": "Detta lösenord kommer att användas för att kryptera dina konton. Ingen kan hjälpa dig om du glömmer lösenordet.", @@ -489,7 +489,7 @@ "message": "Ger enbart skrivbehörighet till urklipp för att kopiera koder till urklipp när du klickar på kontot." }, "permission_context_menus": { - "message": "Lägger till Authenticator till snabbvalsmenyn." + "message": "Lägger till OTPilot till snabbvalsmenyn." }, "permission_sync_clock": { "message": "Tillåter klocksynkronisering med Google." diff --git a/_locales/th/messages.json b/_locales/th/messages.json index 287a34015..9acdbbc39 100644 --- a/_locales/th/messages.json +++ b/_locales/th/messages.json @@ -1,14 +1,14 @@ { "extName": { - "message": "Authentication", + "message": "OTPilot Authenticator", "description": "Extension Name." }, "extShortName": { - "message": "Authentication", + "message": "OTPilot", "description": "Extension Short Name." }, "extDesc": { - "message": "Authenticator สำหรับสร้างรหัสยืนยัน 2 ชั้นบนเบราว์เซอร์", + "message": "OTPilot สำหรับสร้างรหัสยืนยัน 2 ชั้นบนเบราว์เซอร์", "description": "Extension Description." }, "added": { @@ -489,7 +489,7 @@ "message": "ให้สิทธิ์การเข้าถึงแบบเขียนอย่างเดียวในคลิปบอร์ดเพื่อคัดลอกรหัสไปยังคลิปบอร์ดเมื่อคุณคลิกที่บัญชี" }, "permission_context_menus": { - "message": "เพิ่ม Authenticator ให้กับเมนูบริบท" + "message": "เพิ่ม OTPilot ให้กับเมนูบริบท" }, "permission_sync_clock": { "message": "อนุญาตให้ซิงค์นาฬิกากับ Google" diff --git a/_locales/tr/messages.json b/_locales/tr/messages.json index 2735c5238..76c9b3452 100644 --- a/_locales/tr/messages.json +++ b/_locales/tr/messages.json @@ -1,10 +1,10 @@ { "extName": { - "message": "Kimlik Doğrulayıcı", + "message": "OTPilot Authenticator", "description": "Extension Name." }, "extShortName": { - "message": "Kimlik Doğrulayıcı", + "message": "OTPilot", "description": "Extension Short Name." }, "extDesc": { diff --git a/_locales/uk/messages.json b/_locales/uk/messages.json index 76c3972c1..390300fb0 100644 --- a/_locales/uk/messages.json +++ b/_locales/uk/messages.json @@ -1,10 +1,10 @@ { "extName": { - "message": "Authenticator", + "message": "OTPilot Authenticator", "description": "Extension Name." }, "extShortName": { - "message": "Authenticator", + "message": "OTPilot", "description": "Extension Short Name." }, "extDesc": { diff --git a/_locales/vi/messages.json b/_locales/vi/messages.json index e35f6e85f..ce0361803 100644 --- a/_locales/vi/messages.json +++ b/_locales/vi/messages.json @@ -1,14 +1,14 @@ { "extName": { - "message": "Authenticator", + "message": "OTPilot Authenticator", "description": "Extension Name." }, "extShortName": { - "message": "Trình xác thực", + "message": "OTPilot", "description": "Extension Short Name." }, "extDesc": { - "message": "Authenticator tạo mã xác thực hai yếu tố trong trình duyệt của bạn", + "message": "OTPilot tạo mã xác thực hai yếu tố trong trình duyệt của bạn", "description": "Extension Description." }, "added": { @@ -120,10 +120,10 @@ "description": "Message that user is required to acknowledge before clearing all data." }, "delete_all": { - "message": "Thiết lập lại Authenticator" + "message": "Thiết lập lại OTPilot" }, "delete_all_warning": { - "message": "Hành động này sẽ xóa toàn bộ dữ liệu của bạn và thiết lập Authenticator về trạng thái ban đầu. Bạn sẽ không thể khôi phục dữ liệu đã xóa. Bạn nên cân nhắc việc sao lưu lại dữ liệu trước khi đặt lại Authenticator." + "message": "Hành động này sẽ xóa toàn bộ dữ liệu của bạn và thiết lập OTPilot về trạng thái ban đầu. Bạn sẽ không thể khôi phục dữ liệu đã xóa. Bạn nên cân nhắc việc sao lưu lại dữ liệu trước khi đặt lại OTPilot." }, "security_warning": { "message": "Mật khẩu này sẽ được sử dụng để mã hóa tài khoản của bạn. Không ai có thể giúp bạn nếu bạn quên mật khẩu.", @@ -489,7 +489,7 @@ "message": "Grants write-only access to the clipboard to copy codes to clipboard when you click on the account." }, "permission_context_menus": { - "message": "Adds Authenticator to context menu." + "message": "Adds OTPilot to context menu." }, "permission_sync_clock": { "message": "Allows clock sync with Google." diff --git a/_locales/zh_CN/messages.json b/_locales/zh_CN/messages.json index 96ee5b0ff..54cee1311 100644 --- a/_locales/zh_CN/messages.json +++ b/_locales/zh_CN/messages.json @@ -1,10 +1,10 @@ { "extName": { - "message": "身份验证器", + "message": "OTPilot Authenticator", "description": "Extension Name." }, "extShortName": { - "message": "身份验证器", + "message": "OTPilot", "description": "Extension Short Name." }, "extDesc": { @@ -520,5 +520,149 @@ }, "activate_auto_filter": { "message": "Warning: Smart filter loosely matches the domain name to an account. Always verify that you are on the correct website before entering a code!" + }, + "backup_cloud_sync": { + "message": "云同步", + "description": "Backup section: cloud" + }, + "backup_connected": { + "message": "已连接", + "description": "Cloud provider connected status" + }, + "backup_not_connected": { + "message": "未连接", + "description": "Cloud provider not-connected status" + }, + "backup_on_device": { + "message": "本机", + "description": "Backup section: local" + }, + "backup_requires_password": { + "message": "请先设置密码才能使用云备份,您的密钥会在离开此设备前先行加密。", + "description": "Shown in the cloud backup section when no master password is set." + }, + "edit_accounts": { + "message": "编辑账户", + "description": "Header title while in edit mode" + }, + "filter_to_site": { + "message": "筛选至", + "description": "Banner prefix to re-apply the site filter, followed by the domain" + }, + "host": { + "message": "网站", + "description": "Bound website host used to restrict autofill to a matching page." + }, + "import_choose_file": { + "message": "选择文件", + "description": "File import dropzone button" + }, + "import_decrypt": { + "message": "解密并导入", + "description": "Button to decrypt and import an encrypted backup" + }, + "import_drop_file": { + "message": "拖放备份文件到此,或点击浏览", + "description": "File import dropzone title" + }, + "import_enc_required": { + "message": "已加密 — 需要密码", + "description": "Badge on a picked encrypted backup file" + }, + "import_encrypted_note": { + "message": "若文件已加密,选择后会要求输入其密码。", + "description": "File import note about encrypted backups" + }, + "import_file_accepts": { + "message": "支持 .json 和 .txt 导出文件", + "description": "File import dropzone hint" + }, + "import_pass_hint": { + "message": "这是您创建备份时设置的密码。", + "description": "Hint under the import passphrase field" + }, + "import_pass_placeholder": { + "message": "输入文件的密码", + "description": "Passphrase input placeholder for encrypted import" + }, + "onboarding_feature_encrypted": { + "message": "加密保险库,可选密码锁", + "description": "Onboarding feature bullet" + }, + "onboarding_feature_offline": { + "message": "完全离线运行 — 数据不会离开您的设备", + "description": "Onboarding feature bullet" + }, + "onboarding_feature_scan": { + "message": "扫描二维码或导入现有备份", + "description": "Onboarding feature bullet" + }, + "onboarding_get_started": { + "message": "开始使用", + "description": "Onboarding primary button" + }, + "onboarding_import": { + "message": "我有备份要导入", + "description": "Onboarding secondary button" + }, + "onboarding_subtitle": { + "message": "为每个账户生成安全的两步验证码 — 加密、离线,且始终一键复制。", + "description": "Onboarding subtitle" + }, + "onboarding_title": { + "message": "随身携带的验证码", + "description": "Onboarding headline" + }, + "other_accounts": { + "message": "其他账户", + "description": "Divider above non-matching accounts in the filter view" + }, + "pin_to_top": { + "message": "置顶", + "description": "Context-menu action to pin an account to the top" + }, + "qr_transfer_desc": { + "message": "在另一台设备上用 OTPilot 扫描此二维码即可转移此账户 — 数据绝不离开您的设备。", + "description": "QR sheet caption body" + }, + "qr_transfer_title": { + "message": "转移此账户", + "description": "QR sheet caption title" + }, + "settings_appearance": { + "message": "外观", + "description": "Settings section: appearance" + }, + "settings_general": { + "message": "常规", + "description": "Settings section: general" + }, + "show_qr": { + "message": "显示二维码", + "description": "Context-menu action to show an account's QR code" + }, + "showing_codes_for": { + "message": "正在显示以下网站的验证码:", + "description": "Smart-filter banner prefix, followed by the site domain" + }, + "theme_auto": { + "message": "自动", + "description": "Follow the system light/dark setting" + }, + "unlock": { + "message": "解锁", + "description": "Unlock button" + }, + "unpin": { + "message": "取消置顶", + "description": "Context-menu action to unpin an account" + }, + "vault_locked": { + "message": "保险库已锁定", + "description": "Title on the unlock screen" + }, + "backup_unavailable": { + "message": "暂不支持", + "description": "Shown on a cloud provider that is not implemented yet." } } diff --git a/_locales/zh_TW/messages.json b/_locales/zh_TW/messages.json index 07d1f3791..4475e33f8 100644 --- a/_locales/zh_TW/messages.json +++ b/_locales/zh_TW/messages.json @@ -1,22 +1,30 @@ { "extName": { - "message": "Authenticator", + "message": "OTPilot Authenticator", "description": "Extension Name." }, "extShortName": { - "message": "Authenticator", + "message": "OTPilot", "description": "Extension Short Name." }, + "host": { + "message": "網站", + "description": "Bound website host used to restrict autofill to a matching page." + }, + "backup_requires_password": { + "message": "請先設定密碼才能使用雲端備份,您的密鑰會在離開此裝置前先行加密。", + "description": "Shown in the cloud backup section when no master password is set." + }, "extDesc": { - "message": "Authenticator 在你的瀏覽器中生成兩步驟驗證碼。", + "message": "OTPilot 在您的瀏覽器中產生兩步驟驗證碼。", "description": "Extension Description." }, "added": { - "message": "已新增。", + "message": " 已新增。", "description": "Added Account." }, "errorqr": { - "message": "無法識別的QR碼。", + "message": "無法識別的 QR Code。", "description": "QR Error." }, "errorsecret": { @@ -28,7 +36,7 @@ "description": "Add account." }, "add_qr": { - "message": "掃描QR碼", + "message": "掃描 QR Code", "description": "Scan QR Code." }, "add_secret": { @@ -36,11 +44,11 @@ "description": "Manual Entry." }, "migration_fail": { - "message": "匯入失敗。若您要從Google Authenticator搬遷資料,請重新從Google Autenticator匯出資料後再重試", + "message": "匯入失敗。若您要從 Google Authenticator 搬移資料,請重新從 Google Authenticator 匯出資料後再試一次。", "description": "Import migration data failed." }, "migration_partly_fail": { - "message": "部分帳號資料未被成功匯入", + "message": "部分帳號資料未成功匯入。", "description": "Some migration data is broken." }, "close": { @@ -60,15 +68,15 @@ "description": "No." }, "account": { - "message": "帳戶", + "message": "帳號", "description": "Account." }, "accountName": { - "message": "用戶名", + "message": "使用者名稱", "description": "Account Name." }, "issuer": { - "message": "簽發方", + "message": "發行者", "description": "Issuer." }, "secret": { @@ -96,7 +104,7 @@ "description": "Security." }, "current_phrase": { - "message": "當前密碼", + "message": "目前密碼", "description": "Current Passphrase." }, "new_phrase": { @@ -112,7 +120,7 @@ "description": "Confirm Passphrase." }, "confirm_delete": { - "message": "您確定要刪除此帳戶嗎?此操作無法復原。", + "message": "您確定要刪除此帳號嗎?此操作無法復原。", "description": "Remove entry confirmation" }, "confirm_delete_all": { @@ -120,13 +128,13 @@ "description": "Message that user is required to acknowledge before clearing all data." }, "delete_all": { - "message": "重設 Authenticator" + "message": "重設 OTPilot" }, "delete_all_warning": { - "message": "這會刪除您所有的資料並重設Authenticator。您將無法復原任何已刪除的資料!在重設Authenticator之前您應備份您的資料。" + "message": "這會刪除您所有的資料並重設 OTPilot。您將無法復原任何已刪除的資料!在重設 OTPilot 之前,建議您先備份資料。" }, "security_warning": { - "message": "您的金鑰將使用此密碼進行加密。請妥善保管,如果遺失您將失去對此金鑰的權限。", + "message": "此密碼將用於加密您的帳號。如果忘記密碼,將沒有人能夠幫助您。", "description": "Passphrase Warning." }, "update": { @@ -134,15 +142,15 @@ "description": "Update." }, "phrase_incorrect": { - "message": "部份帳號輸入密碼錯誤,您無法新增帳號。請在輸入正確的密碼後繼續。", + "message": "在所有帳號解密之前,您無法新增帳號。請先輸入正確的密碼再繼續。", "description": "Passphrase Incorrect." }, "phrase_incorrect_export": { - "message": "本次備份不包含無法解密的帳號", + "message": "本次備份將不包含無法解密的帳號。", "description": "Skip Unable-decripted Data." }, "phrase_not_match": { - "message": "密碼錯誤。", + "message": "密碼不相符。", "description": "Passphrase Not Match." }, "encrypted": { @@ -154,7 +162,7 @@ "description": "Copied." }, "feedback": { - "message": "問題反饋", + "message": "意見回饋", "description": "Feedback." }, "translate": { @@ -166,11 +174,11 @@ "description": "Source Code." }, "passphrase_info": { - "message": "輸入密碼以解碼帳戶資料。", + "message": "輸入密碼以解密帳號資料。", "description": "Passphrase Info" }, "sync_clock": { - "message": "通過Google校準時間", + "message": "透過 Google 校準時間", "description": "Sync Clock" }, "remember_phrase": { @@ -178,11 +186,11 @@ "description": "Remember Passphrase" }, "clock_too_far_off": { - "message": "注意!您的本地時鐘時間差異過大,請於效準時間後再進行操作。", + "message": "注意!您本機的時鐘時間誤差過大,請先校正後再繼續操作。", "description": "Local Time is Too Far Off" }, "remind_backup": { - "message": "您是否為金鑰建立了備份?別等到為時已晚才建立!", + "message": "您是否為帳號建立了備份?別等到為時已晚才建立!", "description": "Remind Backup" }, "capture_failed": { @@ -190,15 +198,15 @@ "description": "Capture Failed" }, "capture_local_file_failed": { - "message": "您是否試著從本地檔案掃描QR碼? 請改用 匯入QR碼圖片備份。", + "message": "您是否想從本機檔案掃描 QR Code?請改用「匯入 QR Code 圖片備份」。", "description": "Import QR image backup instead of scan local image" }, "based_on_time": { - "message": "驗證碼", + "message": "TOTP", "description": "Time Based" }, "based_on_counter": { - "message": "一次性驗證碼", + "message": "HOTP", "description": "Counter Based" }, "resize_popup_page": { @@ -210,7 +218,7 @@ "description": "Scale" }, "export_info": { - "message": "警告:所有備份均未加密。想要將賬號新增至其他應用?請點選賬號右上角隱藏的圖示。", + "message": "警告:所有備份均未加密。想將帳號新增至其他應用程式嗎?將游標移到任一帳號的右上角,並點選隱藏的按鈕。", "description": "Export menu info text" }, "download_backup": { @@ -222,35 +230,67 @@ "description": "Import backup." }, "import_backup_file": { - "message": "以檔案匯入", + "message": "匯入檔案", "description": "Import backup file." }, "import_backup_qr": { - "message": "导入 QR 图片备份", + "message": "匯入 QR Code 圖片", "description": "Import qr image backup." }, "import_qr_images": { - "message": "匯入 QR 圖片", + "message": "匯入 QR Code 圖片", "description": "Import qr images. Shown as add account method." }, "import_backup_code": { - "message": "以文字匯入", + "message": "匯入文字", "description": "Import backup code." }, "import_otp_urls": { "message": "匯入 OTP 網址", "description": "Import OTP URLs. Shown as add account method." }, + "import_drop_file": { + "message": "拖曳備份檔到此,或點擊瀏覽", + "description": "File import dropzone title" + }, + "import_file_accepts": { + "message": "支援 .json 與 .txt 匯出檔", + "description": "File import dropzone hint" + }, + "import_choose_file": { + "message": "選擇檔案", + "description": "File import dropzone button" + }, + "import_encrypted_note": { + "message": "若檔案已加密,選取後將會要求輸入密碼。", + "description": "File import note about encrypted backups" + }, + "import_enc_required": { + "message": "已加密——需要密碼", + "description": "Badge on a picked encrypted backup file" + }, + "import_pass_placeholder": { + "message": "輸入此檔案的密碼", + "description": "Passphrase input placeholder for encrypted import" + }, + "import_pass_hint": { + "message": "這是你建立備份時設定的密碼。", + "description": "Hint under the import passphrase field" + }, + "import_decrypt": { + "message": "解密並匯入", + "description": "Button to decrypt and import an encrypted backup" + }, "import_backup_qr_partly_failed": { - "message": "匯入成功,但部份QR碼無法辨識", + "message": "匯入成功,但部分 QR Code 無法辨識。", "description": "Import successful, but some QR image cannot be recognized." }, "import_backup_qr_in_batches": { - "message": "您可以選擇多個檔案進行批量匯入", + "message": "您可以選擇多個檔案以批次匯入備份。", "description": "You can select multiple image files to import backup in batches." }, "show_all_entries": { - "message": "顯示全部條目", + "message": "顯示所有項目", "description": "Show all entries." }, "dropbox_risk": { @@ -262,7 +302,7 @@ "description": "Error password warning when import backups." }, "local_passphrase_warning": { - "message": "您的密碼儲存在了本地,請立即通過安全選單更改密碼。", + "message": "您的密碼儲存在本機,請立即透過安全選單變更密碼。", "description": "localStorage password warning." }, "remove": { @@ -270,7 +310,7 @@ "description": "Remove password." }, "download_enc_backup": { - "message": "下載密碼保護備份", + "message": "下載受密碼保護的備份", "description": "Download Encrypted Backup" }, "search": { @@ -294,17 +334,113 @@ "description": "Manual backup" }, "use_autofill": { - "message": "啟用自動填充", + "message": "啟用自動填入", "description": "Use Autofill" }, "use_high_contrast": { - "message": "高對比", + "message": "使用高對比", "description": "Use High Contrast" }, "theme": { "message": "主題", "description": "Theme" }, + "settings_appearance": { + "message": "外觀", + "description": "Settings section: appearance" + }, + "settings_general": { + "message": "一般", + "description": "Settings section: general" + }, + "edit_accounts": { + "message": "編輯帳號", + "description": "Header title while in edit mode" + }, + "show_qr": { + "message": "顯示 QR Code", + "description": "Context-menu action to show an account's QR code" + }, + "pin_to_top": { + "message": "釘選到最上方", + "description": "Context-menu action to pin an account to the top" + }, + "unpin": { + "message": "取消釘選", + "description": "Context-menu action to unpin an account" + }, + "vault_locked": { + "message": "資料已鎖定", + "description": "Title on the unlock screen" + }, + "unlock": { + "message": "解鎖", + "description": "Unlock button" + }, + "qr_transfer_title": { + "message": "轉移此帳號", + "description": "QR sheet caption title" + }, + "qr_transfer_desc": { + "message": "在另一台裝置上用 OTPilot 掃描此碼即可轉移此帳號——資料不會離開你的裝置。", + "description": "QR sheet caption body" + }, + "backup_on_device": { + "message": "本機裝置", + "description": "Backup section: local" + }, + "backup_cloud_sync": { + "message": "雲端同步", + "description": "Backup section: cloud" + }, + "backup_connected": { + "message": "已連線", + "description": "Cloud provider connected status" + }, + "backup_not_connected": { + "message": "未連線", + "description": "Cloud provider not-connected status" + }, + "showing_codes_for": { + "message": "顯示以下網站的驗證碼:", + "description": "Smart-filter banner prefix, followed by the site domain" + }, + "other_accounts": { + "message": "其他帳號", + "description": "Divider above non-matching accounts in the filter view" + }, + "filter_to_site": { + "message": "只顯示此網站:", + "description": "Banner prefix to re-apply the site filter, followed by the domain" + }, + "onboarding_title": { + "message": "隨身攜帶的驗證碼", + "description": "Onboarding headline" + }, + "onboarding_subtitle": { + "message": "為每個帳號產生安全的兩步驟驗證碼——加密、離線,隨時一鍵複製。", + "description": "Onboarding subtitle" + }, + "onboarding_feature_offline": { + "message": "完全離線運作——資料不離開你的裝置", + "description": "Onboarding feature bullet" + }, + "onboarding_feature_encrypted": { + "message": "加密資料,可選用密碼鎖", + "description": "Onboarding feature bullet" + }, + "onboarding_feature_scan": { + "message": "掃描 QR Code 或匯入既有備份", + "description": "Onboarding feature bullet" + }, + "onboarding_get_started": { + "message": "開始使用", + "description": "Onboarding primary button" + }, + "onboarding_import": { + "message": "我有備份要匯入", + "description": "Onboarding secondary button" + }, "theme_light": { "message": "亮色", "description": "Light theme" @@ -313,12 +449,16 @@ "message": "暗色", "description": "Dark theme" }, + "theme_auto": { + "message": "自動(跟隨系統)", + "description": "Follow the system light/dark setting" + }, "theme_simple": { "message": "簡易", "description": "Simple theme" }, "theme_compact": { - "message": "壓縮", + "message": "緊湊", "description": "Compact theme" }, "theme_high_contrast": { @@ -326,7 +466,7 @@ "description": "High Contrast theme" }, "theme_flat": { - "message": "平", + "message": "扁平", "description": "Flat theme" }, "storage_sync_info": { @@ -341,19 +481,35 @@ "message": "登入", "description": "Sign in to 3rd party storage services" }, + "drive_sync_title": { + "message": "與 Google Drive 同步", + "description": "Drive connect page title" + }, + "drive_sync_desc": { + "message": "在你自己的 Google Drive 保留一份加密的帳號備份。驗證碼會在離開此裝置前先加密。", + "description": "Drive connect page description" + }, + "cloud_e2e_note": { + "message": "上傳前端對端加密", + "description": "Reassurance note on the cloud connect page" + }, + "drive_sign_in": { + "message": "登入 Google", + "description": "Drive sign-in button" + }, "sign_in_business": { "message": "登入(企業)", "description": "Sign in to 3rd party storage services" }, "onedrive_business_perms": { - "message": "為什麼企業賬戶需要更多權限?" + "message": "為什麼企業帳號需要更多權限?" }, "log_out": { "message": "登出", "description": "Sign out of 3rd party storage services" }, "token_revoked": { - "message": "連接到您的 $SERVICE$ 帳戶時出現問題,請重試。", + "message": "連線到您的 $SERVICE$ 帳號時發生問題,請重新登入。", "description": "Error authenticating to backup service. $SERVICE$ will be replaced with a proper noun (E.g.: 'Google Drive' or 'Dropbox')", "placeholders": { "service": { @@ -363,15 +519,15 @@ } }, "otp_unsupported_warn": { - "message": "您有一個或多個Steam或Blizzard帳戶。 未加密的備份將不使用標準化備份格式。", + "message": "您有一個或多個 Steam 或 Blizzard 帳號。未加密的備份將不會使用標準化的備份格式。", "description": "Warning if using account that is not supported by standard backup format." }, "otp_backup_inform": { - "message": "您可以從其他一些應用程序導入備份。", + "message": "您可以從其他一些應用程式匯入備份。", "description": "Info text on import page" }, "otp_backup_learn": { - "message": "瞭解詳情", + "message": "了解更多", "description": "learn more link on import page. Placed after otp_backup_inform" }, "loading": { @@ -387,7 +543,7 @@ "message": "進階設定" }, "period": { - "message": "有效期" + "message": "週期" }, "type": { "message": "類型" @@ -396,10 +552,10 @@ "message": "無效" }, "digits": { - "message": "數字" + "message": "位數" }, "algorithm": { - "message": "算法" + "message": "演算法" }, "smart_filter": { "message": "智慧過濾" @@ -408,34 +564,34 @@ "message": "備份" }, "backup_file_info": { - "message": "備份至檔案" + "message": "將您的資料備份至檔案。" }, "password_policy_default_hint": { - "message": "您的密碼不符合您組織的安全要求。 有關詳細信息,請聯繫您的管理員。" + "message": "您的密碼不符合貴組織的安全要求。如需更多資訊,請聯絡您的系統管理員。" }, "advisor": { "message": "顧問" }, "advisor_insight_password_not_set": { - "message": "設定密碼保護您的資料。" + "message": "設定密碼以保護您的資料。" }, "advisor_insight_auto_lock_not_set": { - "message": "啟用自動鎖定以保護你的資料" + "message": "啟用自動鎖定以保護您的資料。" }, "advisor_insight_browser_sync_not_enabled": { - "message": "已禁用瀏覽器同步。啟用以在不同瀏覽器同步賬號。" + "message": "已停用瀏覽器同步。啟用後即可在不同瀏覽器間同步帳號。" }, "advisor_insight_auto_fill_not_enabled": { - "message": "可以啟動自動填入以自動填入代碼到網站中" + "message": "可啟用自動填入,自動將驗證碼填入網站。" }, "advisor_insight_smart_filter_not_enabled": { - "message": "啟用智能過濾器可以快速訪問帳戶。" + "message": "啟用智慧過濾可快速存取帳號。" }, "show_all_insights": { "message": "顯示所有見解。" }, "no_insight_available": { - "message": "無任何見解被發現;一切完好!" + "message": "未發現任何見解,一切看起來都很好!" }, "danger": { "message": "危險" @@ -447,78 +603,82 @@ "message": "資訊" }, "dismiss": { - "message": "解除" + "message": "忽略" }, "learn_more": { "message": "了解更多" }, "enable_context_menu": { - "message": "加到右鍵選單" + "message": "加入右鍵選單" }, "no_entires": { - "message": "帳號不存在,請新增帳號。" + "message": "沒有可顯示的帳號,立即新增您的第一個帳號。" }, "permissions": { "message": "使用權限" }, "permission_revoke": { - "message": "註銷" + "message": "撤銷" }, "permission_show_required_permissions": { - "message": "顯示不可註銷的權限。" + "message": "顯示不可撤銷的權限。" }, "permission_required": { - "message": "這是必要的權限而且不可以註銷。" + "message": "這是必要的權限,無法撤銷。" }, "permission_active_tab": { - "message": "存取當前頁面並掃描QR碼" + "message": "存取目前分頁以掃描 QR Code。" }, "permission_storage": { - "message": "存取瀏覽器空間以便存放帳戶資訊" + "message": "存取瀏覽器儲存空間以存放帳號資料。" }, "permission_identity": { "message": "允許登入第三方儲存服務。" }, "permission_alarms": { - "message": "Allows auto-lock to work." + "message": "允許自動鎖定運作。" }, "permission_scripting": { - "message": "Inject scripts into he current tab to scan QR codes and allow auto-fill to work." + "message": "在目前分頁注入指令碼以掃描 QR Code,並讓自動填入功能運作。" }, "permission_clipboard_write": { - "message": "授權點選帳戶時複製驗證碼到剪貼簿。" + "message": "授權在您點選帳號時將驗證碼複製到剪貼簿。" }, "permission_context_menus": { - "message": "將驗證器加到右鍵選單" + "message": "將 OTPilot 加入右鍵選單。" }, "permission_sync_clock": { "message": "允許與 Google 連線校時。" }, "permission_dropbox": { - "message": "允許備份到Dropbox." + "message": "允許備份到 Dropbox。" }, "permission_dropbox_cannot_revoke": { - "message": "請先取消 Dropbox 備份" + "message": "請先停用 Dropbox 備份。" }, "permission_drive": { - "message": "允許備份到Google Drive." + "message": "允許備份到 Google Drive。" }, "permission_drive_cannot_revoke": { - "message": "請先取消 Google Drive 備份" + "message": "請先停用 Google Drive 備份。" }, "permission_onedrive": { - "message": "允許備份到OneDrive." + "message": "允許備份到 OneDrive。" }, "permission_onedrive_cannot_revoke": { - "message": "請先取消 OneDrvie 備份" + "message": "請先停用 OneDrive 備份。" }, "permission_unknown_permission": { - "message": "未知的權限。您如果看到這個訊息,請回報程式異常。" + "message": "未知的權限。如果您看到此訊息,請回報程式錯誤。" }, "phrase_wrong": { - "message": "Password incorrect" + "message": "密碼不正確" }, "activate_auto_filter": { - "message": "Warning: Smart filter loosely matches the domain name to an account. Always verify that you are on the correct website before entering a code!" + "message": "警告:智慧過濾會以較寬鬆的方式比對網域名稱與帳號。輸入驗證碼前,請務必確認您位於正確的網站!" + }, + "backup_unavailable": { + "message": "暫不支援", + "description": "Shown on a cloud provider that is not implemented yet." } } diff --git a/docs/plans/2026-06-23-host-bound-autofill-design.md b/docs/plans/2026-06-23-host-bound-autofill-design.md new file mode 100644 index 000000000..224ccd0ee --- /dev/null +++ b/docs/plans/2026-06-23-host-bound-autofill-design.md @@ -0,0 +1,114 @@ +# 設計文件:Host 綁定式 Autofill + +日期:2026-06-23 +分支:`feature/host-bound-autofill` +作者:Hank(經 Claude 協作) + +## 背景與動機 + +資安審查發現:popup 點擊帳號觸發的 autofill 路徑([EntryComponent.vue](../../src/components/Popup/EntryComponent.vue) `copyCode`)在送出 `pastecode` 前**完全沒有比對當前分頁網域**,惡意網頁可誘導使用者把任一帳號的活 OTP code 注入其 DOM 並竊取(單次 2FA 繞過)。鍵盤快捷鍵路徑([background.ts](../../src/background.ts))已有基於真實 hostname 的 strict 比對,但 popup 路徑缺漏。 + +本次將「帳號 ↔ host 綁定」升級為一級資料欄位,並讓兩條 autofill 路徑統一採「**無 host 不注入**」的嚴格策略。 + +## 需求(使用者拍板) + +1. **popup copyCode autofill 補嚴格 host 比對** — 不符當前分頁 host 則只複製到剪貼簿、不注入。 +2. **手動新增介面提供 host 輸入欄位** — 放主要欄位區。 +3. **掃 QR 時自動帶入當下分頁 host**。 + +## 設計決策(使用者拍板) + +| 決策 | 結論 | 理由 | +|---|---|---| +| host 儲存方式 | 新增獨立 `host` 欄位 | 語意清晰;加密為整包 JSON 加密故自動涵蓋 | +| 比對策略 | 兩路徑(popup+鍵盤)統一「無 host 不注入」 | 最一致、最嚴格 | +| host 欄位位置 | 手動新增的主要欄位區 | 顯眼、鼓勵綁定 | +| 既有帳號補 host | 編輯模式新增 host 欄位 | 否則既有帳號 autofill 全失效 | +| 舊 `issuer::host` 編碼 | 讀取時自動遷移到新欄位 | 既有上游隱藏功能使用者無痛升級 | + +## 資料模型 + +新增欄位 `host?: string`(正規化為小寫純網域,如 `accounts.google.com`,不含 scheme/path)。 + +- [otp.d.ts](../../src/definitions/otp.d.ts):`OTPEntryInterface` 與 `RawOTPStorage` 新增 `host?: string`。 +- [otp.ts](../../src/models/otp.ts):`OTPEntry` 新增 `host: string` 欄位;建構子讀取 `entry.host`;`applyEncryption` 反序列化 `decryptedData.host`。 +- 加密自動涵蓋:`getOTPStorageFromEntry`([storage.ts:280](../../src/models/storage.ts))將整個 `storageItem` JSON 做 AES-GCM 加密,只要 storageItem 帶 host 即被加密。 + +### 舊 `issuer::host` 讀取時遷移 + +在 `OTPEntry` 建構子(與 `applyEncryption`)收斂:若 `host` 為空且 `issuer` 含 `::`,則 +- `this.host = issuer.split("::")[1]`(去除前導點、轉小寫) +- `this.issuer = issuer.split("::")[0]` + +下次 `update()` 儲存時即正規化為新格式。顯示層 [EntryComponent.vue:27](../../src/components/Popup/EntryComponent.vue) 既有的 `split("::")[0]` 保留作為過渡相容。 + +## 比對邏輯([utils.ts](../../src/utils.ts) `isMatchedEntry`) + +新語意(strict 模式,autofill 用): +``` +若 entry.host 為空 → 不匹配(不注入) +否則 → hostMatchesDomain(siteHost, entry.host) 決定 +``` +- 複用既有 `hostMatchesDomain`(已防 `google.com.attacker.com`:僅精確或真子網域匹配)。 +- strict 模式**移除** issuer 名稱 fallback 與 title 比對。 +- loose 模式(顯示過濾,`strict=false`)維持現狀,確保帳號清單搜尋/highlight 不受影響。 + +## 三個需求落點 + +### ① copyCode([EntryComponent.vue](../../src/components/Popup/EntryComponent.vue)) +autofill 分支內、送 `pastecode` 前: +``` +const siteName = await getSiteName(); +const matched = getMatchedEntries(siteName, [entry], true); +if (matched && matched.length === 1) { /* sendMessage pastecode */ } +// 不論是否注入,後續剪貼簿複製照常執行 +``` + +### ② 手動新增([AddAccountPage.vue](../../src/components/Popup/AddAccountPage.vue)) +主要欄位區(issuer 下方)新增 host input(選填);`addNewAccount` 建立 entry 時帶入正規化後的 host。 + +### ③ QR 自動帶入 host([background.ts](../../src/background.ts) `getTotp`) +`getTotp` 自 `onMessage` 的 `sender.tab.url` 取得 hostname,往下傳入,建立 entry 時設為 host。需調整 `getTotp` 簽名以傳遞 host(遞迴的 migration 呼叫沿用同一 host)。 + +### ④ 編輯模式補 host([EntryComponent.vue](../../src/components/Popup/EntryComponent.vue) + [Accounts store](../../src/store/Accounts.ts)) +編輯模式新增 host 輸入欄;`setEntryField` 支援 `host` 欄位。 + +## 受影響檔案清單 + +| 檔案 | 變更 | +|---|---| +| `src/definitions/otp.d.ts` | 介面新增 `host?` | +| `src/models/otp.ts` | OTPEntry 欄位/建構子/applyEncryption/issuer::host 遷移 | +| `src/models/storage.ts` | getOTPStorageFromEntry 序列化、backupGetExport 清理、import 讀取 host | +| `src/utils.ts` | isMatchedEntry strict 改用 host 欄位 | +| `src/components/Popup/EntryComponent.vue` | copyCode 比對 + 編輯模式 host 欄位 | +| `src/components/Popup/AddAccountPage.vue` | 主要區 host 欄位 | +| `src/background.ts` | getTotp 帶入 sender.tab host;鍵盤 autofill 已用 strict | +| `src/store/Accounts.ts` | setEntryField 支援 host(若需) | +| `_locales/en/messages.json`(+ zh_TW) | host 欄位 i18n 字串 | + +## 測試策略(TDD) + +mocha + @vue/test-utils(`src/test/`)。先寫測試再實作: + +1. **`isMatchedEntry` 嚴格語意**(核心,優先): + - 有 host 且精確匹配 → 注入 + - 子網域匹配(`accounts.google.com` vs bound `google.com`)→ 注入 + - 不匹配 → 不注入 + - 攻擊者後綴(`google.com.attacker.com` vs `google.com`)→ 不注入 + - **無 host → 不注入**(新核心行為) + - loose 模式維持既有行為(回歸保護) +2. **issuer::host 讀取時遷移**:建構含 `Foo::bar.com` 的 entry → `host==="bar.com"` 且 `issuer==="Foo"`。 +3. **序列化往返**:OTPEntry(含 host)→ getOTPStorageFromEntry → 含 host;加密往返後 host 保留。 + +## 行為相容性影響(需告知使用者) + +- 既有「無 host」帳號的 **autofill(兩路徑)將失效**,直到使用者用編輯模式補 host 或重掃 QR。此為使用者明確選擇的安全優先取捨。 +- 既有 `issuer::host` 帳號自動遷移,autofill 持續可用。 +- 顯示、複製到剪貼簿、code 產生等其他功能不受影響。 + +## 驗證方式 + +- `npm test`(mocha)全綠,新增測試涵蓋上述案例。 +- 型別檢查(fork-ts-checker / tsc)通過。 +- 手動驗證:手動新增帶 host、掃 QR 自動帶 host、在不符 host 的頁面 popup autofill 只複製不注入。 diff --git a/images/icon.svg b/images/icon.svg index 6678b02b0..144e7737b 100644 --- a/images/icon.svg +++ b/images/icon.svg @@ -1,24 +1,8 @@ - - - - - - - - - - - - - - - - - - - - - - + + + + + 076 + 721 diff --git a/images/icon128.png b/images/icon128.png index b96b52c57..8c3df5156 100644 Binary files a/images/icon128.png and b/images/icon128.png differ diff --git a/images/icon16.png b/images/icon16.png index 2e5104503..d68669dcc 100644 Binary files a/images/icon16.png and b/images/icon16.png differ diff --git a/images/icon19.png b/images/icon19.png index 1e1823a5b..d82c9028f 100644 Binary files a/images/icon19.png and b/images/icon19.png differ diff --git a/images/icon38.png b/images/icon38.png index 079ee2936..92e5c6797 100644 Binary files a/images/icon38.png and b/images/icon38.png differ diff --git a/images/icon48.png b/images/icon48.png index d6fedbad6..34b593ec5 100644 Binary files a/images/icon48.png and b/images/icon48.png differ diff --git a/manifests/manifest-chrome.json b/manifests/manifest-chrome.json index e792d9c3b..4e6fa2416 100644 --- a/manifests/manifest-chrome.json +++ b/manifests/manifest-chrome.json @@ -1,5 +1,6 @@ { "manifest_version": 3, + "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv1vBiM9sm3mAj4/i0+XIgOVhVedsypV2XVpGW864RwWPuaOnVi0xJ3rhg4jxulOwA0CS/zoO5rxeK+yhT9PByZ9YlGtJGR7Cg6tPHlIXf37Oy5w6dCXCuqjhVAaI0Ka+oXA5XPWd07Em0v2xlqEZdHTu95XDy7oOexDKbSJEXzg6BsdymIzn/tw1LG7+iWUOYfm+7sTIOTYQPFvFgPV5AuVPSOeQNDloT89jxYpmJFD7LMkrd6gQx+MRB9J93kmG8LY2thfLpgQh3kbeuObGD0Zh1xoeCoMPOstrDYTK8kHRhB/CFSb3XAn1+wrh3CUnr+Rc/m7eGb85OsYArJyFoQIDAQAB", "name": "__MSG_extName__", "short_name": "__MSG_extShortName__", "version": "8.0.2", @@ -35,7 +36,7 @@ "managed_schema": "schema.json" }, "oauth2": { - "client_id": "292457304165-u8ve4j79gag5o231n5u2pdtdrbfdo1hh.apps.googleusercontent.com", + "client_id": "593829937694-vmijc4tj4q3j0oss6dpilv5o2e3d2jbo.apps.googleusercontent.com", "scopes": [ "https://www.googleapis.com/auth/drive.file" ] diff --git a/package-lock.json b/package-lock.json index 1bb8b48a7..fc0def058 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,32 +9,33 @@ "version": "0.1.0", "license": "MIT", "dependencies": { - "@types/lodash": "^4.14.166", "argon2-browser": "^1.18.0", + "crypto-js": "^4.1.1", "jsqr": "^1.3.1", "node-gost-crypto": "^1.0.2", "qrcode-generator": "^1.4.4", "qrcode-reader": "^1.0.4", - "vue": "^2.7.16", - "vue2-dragula": "^2.5.4", - "vuex": "^3.4.0" + "vue": "^3.4.21", + "vuedraggable": "^4.1.0", + "vuex": "^4.1.0" }, "devDependencies": { "@types/argon2-browser": "^1.18.1", "@types/chai": "^4.2.14", "@types/chrome": "^0.0.266", "@types/crypto-js": "^4.1.1", + "@types/lodash": "^4.14.166", "@types/mocha": "^10.0.6", "@types/sinon": "^17.0.2", "@types/sinon-chai": "^3.2.12", "@types/sinon-chrome": "^2.2.10", "@typescript-eslint/eslint-plugin": "^7.15.0", "@typescript-eslint/parser": "^7.15.0", - "@vue/test-utils": "^1.1.1", + "@vue/compiler-sfc": "^3.4.21", + "@vue/test-utils": "^2.4.5", "base64-loader": "^1.0.0", "buffer": "^6.0.3", "chai": "^4.2.0", - "crypto-js": "^4.1.1", "eslint": "^8.56.0", "fork-ts-checker-webpack-plugin": "^6.5.3", "lodash": "^4.17.21", @@ -53,9 +54,8 @@ "typescript": "^5.0.0", "url-loader": "^4.0.0", "util": "^0.12.5", - "vue-loader": "^15.10.1", - "vue-svg-loader": "^0.16.0", - "vue-template-compiler": "^2.7.16", + "vue-loader": "^17.4.2", + "vue-svg-loader": "0.17.0-beta.2", "webpack": "^5.94.0", "webpack-cli": "^5.0.0", "webpack-merge": "^5.0.0" @@ -282,19 +282,19 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", - "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", - "dev": true, + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", - "dev": true, + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -337,9 +337,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz", - "integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, "bin": { "parser": "bin/babel-parser.js" }, @@ -392,14 +396,13 @@ } }, "node_modules/@babel/types": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", - "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", - "dev": true, + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -773,10 +776,10 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "dev": true + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", @@ -823,6 +826,13 @@ "node": ">= 8" } }, + "node_modules/@one-ini/wasm": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", + "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", + "dev": true, + "license": "MIT" + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -1040,7 +1050,8 @@ "node_modules/@types/lodash": { "version": "4.14.192", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.192.tgz", - "integrity": "sha512-km+Vyn3BYm5ytMO13k9KTp27O75rbQ0NFw+U//g+PX7VZyjCioXaRFisqSIJRECljcTv73G3i6BpglNGHgUQ5A==" + "integrity": "sha512-km+Vyn3BYm5ytMO13k9KTp27O75rbQ0NFw+U//g+PX7VZyjCioXaRFisqSIJRECljcTv73G3i6BpglNGHgUQ5A==", + "dev": true }, "node_modules/@types/mocha": { "version": "10.0.6", @@ -1061,10 +1072,11 @@ "dev": true }, "node_modules/@types/q": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.5.tgz", - "integrity": "sha512-L28j2FcJfSZOnL1WBjDYp2vUHCeIFlyYI/53EwD/rKUBQ7MtUUfbQWiyKJGpcnv4/WgrhWsFKrcPstcAt/J0tQ==", - "dev": true + "version": "1.5.8", + "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.8.tgz", + "integrity": "sha512-hroOstUScF6zhIi+5+x0dzqrHA1EJi+Irri6b1fxolMTqqHIV/Cg77EtnQcZqZCu8hR3mX2BzIxN4/GzI68Kfw==", + "dev": true, + "license": "MIT" }, "node_modules/@types/sinon": { "version": "17.0.2", @@ -1326,90 +1338,131 @@ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", "dev": true }, + "node_modules/@vue/compiler-core": { + "version": "3.5.38", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.38.tgz", + "integrity": "sha512-s99aGxWYig9ErHbct27KXEGhrBYlRI6c4MwAgXErOAbX9xiW37/uMa+XUDO69zLz83dng8UUZ70CTOJrLrYrEQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/shared": "3.5.38", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.38", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.38.tgz", + "integrity": "sha512-JTqp25l8aFfJYF7/KmsXZjAxJz7T+SjmTJLoXVjHtc2BrSgSiW2n9Aem/cWq1OPe68A8JL06B3eVdhlP0H4TVw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.38", + "@vue/shared": "3.5.38" + } + }, "node_modules/@vue/compiler-sfc": { - "version": "2.7.16", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.16.tgz", - "integrity": "sha512-KWhJ9k5nXuNtygPU7+t1rX6baZeqOYLEforUPjgNDBnLicfHCoi48H87Q8XyLZOrNNsmhuwKqtpDQWjEFe6Ekg==", + "version": "3.5.38", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.38.tgz", + "integrity": "sha512-DuA2GiZawSEW442iw/9+Fkol8hTgb4Ke5KkhmSry65QA7YuyMbIdy8p0XZRMvNwJdgRz307W8g1CSzdvS4nuNg==", + "license": "MIT", "dependencies": { - "@babel/parser": "^7.23.5", - "postcss": "^8.4.14", - "source-map": "^0.6.1" - }, - "optionalDependencies": { - "prettier": "^1.18.2 || ^2.0.0" + "@babel/parser": "^7.29.7", + "@vue/compiler-core": "3.5.38", + "@vue/compiler-dom": "3.5.38", + "@vue/compiler-ssr": "3.5.38", + "@vue/shared": "3.5.38", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.15", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.38", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.38.tgz", + "integrity": "sha512-7s+W5Gc42FGxZMcuwl8H5B29T8BJPMdBT7KHFE+BbAuZ/iTEdTtv7z2XiMjiaUUw4w3ZcCEdHs36RuYJ2VA7bA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.38", + "@vue/shared": "3.5.38" } }, - "node_modules/@vue/component-compiler-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@vue/component-compiler-utils/-/component-compiler-utils-3.3.0.tgz", - "integrity": "sha512-97sfH2mYNU+2PzGrmK2haqffDpVASuib9/w2/noxiFi31Z54hW+q3izKQXXQZSNhtiUpAI36uSuYepeBe4wpHQ==", - "dev": true, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/reactivity": { + "version": "3.5.38", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.38.tgz", + "integrity": "sha512-pG6LV/NDNRbKizcUjFFLAfjaL8mcv4DmR9avNcUw2gDHBzZneuS2TWCmp633ynzxz9YYKNeEPK2I8Wraqy2HUQ==", + "license": "MIT", "dependencies": { - "consolidate": "^0.15.1", - "hash-sum": "^1.0.2", - "lru-cache": "^4.1.2", - "merge-source-map": "^1.1.0", - "postcss": "^7.0.36", - "postcss-selector-parser": "^6.0.2", - "source-map": "~0.6.1", - "vue-template-es2015-compiler": "^1.9.0" - }, - "optionalDependencies": { - "prettier": "^1.18.2 || ^2.0.0" + "@vue/shared": "3.5.38" } }, - "node_modules/@vue/component-compiler-utils/node_modules/lru-cache": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", - "dev": true, + "node_modules/@vue/runtime-core": { + "version": "3.5.38", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.38.tgz", + "integrity": "sha512-iyW8WVfF1CpCXxncZY5Ei6rSd6oZr5DgEom//fUjRBRl56AXPD+s9ATvukRt77ZFTuYlnVA1bxY+dJB94tWVYw==", + "license": "MIT", "dependencies": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" + "@vue/reactivity": "3.5.38", + "@vue/shared": "3.5.38" } }, - "node_modules/@vue/component-compiler-utils/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true + "node_modules/@vue/runtime-dom": { + "version": "3.5.38", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.38.tgz", + "integrity": "sha512-apX2wt9sdfDshS+a2xueFZLVpt0GkRJZSoPmrW/SA4yzXTznhfcMVW59gr7h4YQeY0vJhdJkk2rsIDwgfFgC5A==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.38", + "@vue/runtime-core": "3.5.38", + "@vue/shared": "3.5.38", + "csstype": "^3.2.3" + } }, - "node_modules/@vue/component-compiler-utils/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, + "node_modules/@vue/server-renderer": { + "version": "3.5.38", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.38.tgz", + "integrity": "sha512-vue8vbf2QlV4quHqzwmJy6dWfmRhP1J8l4wtZg60CL6VoKqcPY2oe7may3+1d9qfpedjK5PRLFqd5k3Isj9mUw==", + "license": "MIT", "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" + "@vue/compiler-ssr": "3.5.38", + "@vue/shared": "3.5.38" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" + "peerDependencies": { + "vue": "3.5.38" } }, - "node_modules/@vue/component-compiler-utils/node_modules/yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", - "dev": true + "node_modules/@vue/shared": { + "version": "3.5.38", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.38.tgz", + "integrity": "sha512-FTW0AFZNaK5/mOqvGBwVfUlNLU38TiQn4+DQgIFUnrBBJQ1crMJ82yeGQLV5jyKFsO8yRukpbuP7x+nRbH6aug==", + "license": "MIT" }, "node_modules/@vue/test-utils": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-1.3.6.tgz", - "integrity": "sha512-udMmmF1ts3zwxUJEIAj5ziioR900reDrt6C9H3XpWPsLBx2lpHKoA4BTdd9HNIYbkGltWw+JjWJ+5O6QBwiyEw==", + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.4.11.tgz", + "integrity": "sha512-GDqaqZsA6m2E5vNzej0aYiIb6BX8xV9pNSbbbXKOfEYwg7ZNblVX8suyqmUBThq8VIrgAJNxn+z72hVtUeiWHA==", "dev": true, + "license": "MIT", "dependencies": { - "dom-event-types": "^1.0.0", - "lodash": "^4.17.15", - "pretty": "^2.0.0" + "js-beautify": "^1.14.9", + "vue-component-type-helpers": "^3.0.0" }, "peerDependencies": { - "vue": "2.x", - "vue-template-compiler": "^2.x" + "@vue/compiler-dom": "3.x", + "@vue/server-renderer": "3.x", + "vue": "3.x" + }, + "peerDependenciesMeta": { + "@vue/server-renderer": { + "optional": true + } } }, "node_modules/@webassemblyjs/ast": { @@ -1615,10 +1668,14 @@ "dev": true }, "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } }, "node_modules/acorn": { "version": "8.12.1", @@ -1776,13 +1833,17 @@ } }, "node_modules/array-buffer-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", - "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "is-array-buffer": "^3.0.1" + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -1811,16 +1872,42 @@ } }, "node_modules/array.prototype.reduce": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/array.prototype.reduce/-/array.prototype.reduce-1.0.5.tgz", - "integrity": "sha512-kDdugMl7id9COE8R7MHF5jWk7Dqt/fs4Pv+JXoICnYwqpjjjbUurz6w5fT5IG6brLdJhv6/VoHB0H7oyIBXd+Q==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/array.prototype.reduce/-/array.prototype.reduce-1.0.8.tgz", + "integrity": "sha512-DwuEqgXFBwbmZSRqt3BpQigWNUoqw9Ml2dTWdF3B2zQlQX4OeUE0zyuzX0fX0IbTvjdkZbcBTU3idgpO78qkTw==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", "es-array-method-boxes-properly": "^1.0.0", - "is-string": "^1.0.7" + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "is-string": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" }, "engines": { "node": ">= 0.4" @@ -1862,6 +1949,16 @@ "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", "dev": true }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -1877,16 +1974,15 @@ "node": ">= 4.0.0" } }, - "node_modules/atoa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/atoa/-/atoa-1.0.0.tgz", - "integrity": "sha512-VVE1H6cc4ai+ZXo/CRWoJiHXrA1qfA31DPnx6D20+kSI547hQN5Greh51LQ1baMRMfxO5K5M4ImMtZbZt2DODQ==" - }, "node_modules/available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -2005,17 +2101,12 @@ "node": ">=8" } }, - "node_modules/bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", - "dev": true - }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/brace-expansion": { "version": "1.1.11", @@ -2132,16 +2223,47 @@ } }, "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", "dependencies": { - "es-define-property": "^1.0.0", "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { "node": ">= 0.4" @@ -2336,6 +2458,7 @@ "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz", "integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==", "dev": true, + "license": "MIT", "dependencies": { "@types/q": "^1.5.1", "chalk": "^2.4.1", @@ -2405,51 +2528,17 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, - "node_modules/condense-newlines": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/condense-newlines/-/condense-newlines-0.2.1.tgz", - "integrity": "sha512-P7X+QL9Hb9B/c8HI5BFFKmjgBu2XpQuF98WZ9XkO+dBGgk5XgwiQz7o1SmpglNWId3581UcS0SFAWfoIhMHPfg==", - "dev": true, - "dependencies": { - "extend-shallow": "^2.0.1", - "is-whitespace": "^0.3.0", - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/config-chain": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", "dev": true, + "license": "MIT", "dependencies": { "ini": "^1.3.4", "proto-list": "~1.2.1" } }, - "node_modules/consolidate": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/consolidate/-/consolidate-0.15.1.tgz", - "integrity": "sha512-DW46nrsMJgy9kqAbPt5rKaCr7uFtpo4mSUvLHIUbJEjm0vo+aY5QLwBUq3FK4tRnJr/X0Psc0C4jf/h+HtXSMw==", - "dev": true, - "dependencies": { - "bluebird": "^3.1.1" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/contra": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/contra/-/contra-1.9.4.tgz", - "integrity": "sha512-N9ArHAqwR/lhPq4OdIAwH4e1btn6EIZMAz4TazjnzCiVECcWUPTma+dRAM38ERImEJBh8NiCCpjoQruSZ+agYg==", - "dependencies": { - "atoa": "1.0.0", - "ticky": "1.0.1" - } - }, "node_modules/convert-source-map": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", @@ -2479,10 +2568,11 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -2492,52 +2582,17 @@ "node": ">= 8" } }, - "node_modules/crossvent": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/crossvent/-/crossvent-1.5.4.tgz", - "integrity": "sha512-b6gEmNAh3kemyfNJ0LQzA/29A+YeGwevlSkNp2x0TzLOMYc0b85qRAD06OUuLWLQpR7HdJHNZQTlD1cfwoTrzg==", - "dependencies": { - "custom-event": "1.0.0" - } - }, "node_modules/crypto-js": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", - "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", - "dev": true - }, - "node_modules/css-loader": { - "version": "6.7.3", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.3.tgz", - "integrity": "sha512-qhOH1KlBMnZP8FzRO6YCH9UHXQhVMcEGLyNdb7Hv2cpcmJbW0YrddO+tG1ab5nT41KpHIYGsbeHqxB9xPu1pKQ==", - "dev": true, - "peer": true, - "dependencies": { - "icss-utils": "^5.1.0", - "postcss": "^8.4.19", - "postcss-modules-extract-imports": "^3.0.0", - "postcss-modules-local-by-default": "^4.0.0", - "postcss-modules-scope": "^3.0.0", - "postcss-modules-values": "^4.0.0", - "postcss-value-parser": "^4.2.0", - "semver": "^7.3.8" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - } + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" }, "node_modules/css-select": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz", "integrity": "sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0", "css-what": "^3.2.1", @@ -2549,13 +2604,15 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz", "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/css-tree": { "version": "1.0.0-alpha.37", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz", "integrity": "sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg==", "dev": true, + "license": "MIT", "dependencies": { "mdn-data": "2.0.4", "source-map": "^0.6.1" @@ -2569,6 +2626,7 @@ "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz", "integrity": "sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">= 6" }, @@ -2576,23 +2634,12 @@ "url": "https://github.com/sponsors/fb55" } }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/csso": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", "dev": true, + "license": "MIT", "dependencies": { "css-tree": "^1.1.2" }, @@ -2605,6 +2652,7 @@ "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", "dev": true, + "license": "MIT", "dependencies": { "mdn-data": "2.0.14", "source-map": "^0.6.1" @@ -2617,17 +2665,14 @@ "version": "2.0.14", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", - "dev": true + "dev": true, + "license": "CC0-1.0" }, "node_modules/csstype": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", - "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" - }, - "node_modules/custom-event": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.0.tgz", - "integrity": "sha512-6nOXX3UitrmdvSJWoVR2dlzhbX5bEUqmqsMUyx1ypCLZkHHkcuYtdpW3p94RGvcFkTV7DkLo+Ilbwnlwi8L+jw==" + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" }, "node_modules/data-uri-to-buffer": { "version": "6.0.2", @@ -2638,11 +2683,59 @@ "node": ">= 14" } }, - "node_modules/de-indent": { + "node_modules/data-view-buffer": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", - "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", - "dev": true + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/debug": { "version": "4.3.4", @@ -2730,11 +2823,13 @@ } }, "node_modules/define-properties": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", - "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "dev": true, + "license": "MIT", "dependencies": { + "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" }, @@ -2817,17 +2912,12 @@ "node": ">=6.0.0" } }, - "node_modules/dom-event-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/dom-event-types/-/dom-event-types-1.1.0.tgz", - "integrity": "sha512-jNCX+uNJ3v38BKvPbpki6j5ItVlnSqVV6vDWGS6rExzCMjsc39frLjm1n91o6YaKK6AZl0wLloItW6C6mr61BQ==", - "dev": true - }, "node_modules/dom-serializer": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", "dev": true, + "license": "MIT", "dependencies": { "domelementtype": "^2.0.1", "entities": "^2.0.0" @@ -2843,31 +2933,50 @@ "type": "github", "url": "https://github.com/sponsors/fb55" } - ] + ], + "license": "BSD-2-Clause" }, - "node_modules/domelementtype": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", - "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", - "dev": true + "node_modules/dom-serializer/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", + "dev": true, + "license": "BSD-2-Clause" }, "node_modules/domutils": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "dom-serializer": "0", "domelementtype": "1" } }, - "node_modules/dragula": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/dragula/-/dragula-3.7.2.tgz", - "integrity": "sha512-iDPdNTPZY7P/l0CQ800QiX+PNA2XF9iC3ePLWfGxeb/j8iPPedRuQdfSOfZrazgSpmaShYvYQ/jx7keWb4YNzA==", + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", "dependencies": { - "contra": "1.9.4", - "crossvent": "1.5.4" + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/eastasianwidth": { @@ -2877,44 +2986,59 @@ "dev": true }, "node_modules/editorconfig": { - "version": "0.15.3", - "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.3.tgz", - "integrity": "sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.7.tgz", + "integrity": "sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==", "dev": true, + "license": "MIT", "dependencies": { - "commander": "^2.19.0", - "lru-cache": "^4.1.5", - "semver": "^5.6.0", - "sigmund": "^1.0.1" + "@one-ini/wasm": "0.1.1", + "commander": "^10.0.0", + "minimatch": "^9.0.1", + "semver": "^7.5.3" }, "bin": { "editorconfig": "bin/editorconfig" + }, + "engines": { + "node": ">=14" } }, - "node_modules/editorconfig/node_modules/lru-cache": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "node_modules/editorconfig/node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", "dev": true, + "license": "MIT", "dependencies": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" + "balanced-match": "^1.0.0" } }, - "node_modules/editorconfig/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "node_modules/editorconfig/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", "dev": true, - "bin": { - "semver": "bin/semver" + "license": "MIT", + "engines": { + "node": ">=14" } }, - "node_modules/editorconfig/node_modules/yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", - "dev": true + "node_modules/editorconfig/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, "node_modules/electron-to-chromium": { "version": "1.5.13", @@ -2960,10 +3084,13 @@ } }, "node_modules/entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", - "dev": true, + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, "funding": { "url": "https://github.com/fb55/entities?sponsor=1" } @@ -2999,45 +3126,85 @@ } }, "node_modules/es-abstract": { - "version": "1.21.2", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.2.tgz", - "integrity": "sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==", + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", "dev": true, + "license": "MIT", "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "es-set-tostringtag": "^2.0.1", - "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.2.0", - "get-symbol-description": "^1.0.0", - "globalthis": "^1.0.3", - "gopd": "^1.0.1", - "has": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.5", - "is-array-buffer": "^3.0.2", + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-typed-array": "^1.1.10", - "is-weakref": "^1.0.2", - "object-inspect": "^1.12.3", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.4.3", - "safe-regex-test": "^1.0.0", - "string.prototype.trim": "^1.2.7", - "string.prototype.trimend": "^1.0.6", - "string.prototype.trimstart": "^1.0.6", - "typed-array-length": "^1.0.4", - "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.9" + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-abstract-get": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-abstract-get/-/es-abstract-get-1.0.0.tgz", + "integrity": "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.2", + "is-callable": "^1.2.7", + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -3053,13 +3220,11 @@ "dev": true }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "dev": true, - "dependencies": { - "get-intrinsic": "^1.2.4" - }, + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -3079,29 +3244,47 @@ "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", "dev": true }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-set-tostringtag": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", - "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "dev": true, + "license": "MIT", "dependencies": { - "get-intrinsic": "^1.1.3", - "has": "^1.0.3", - "has-tostringtag": "^1.0.0" + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" } }, "node_modules/es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.1.tgz", + "integrity": "sha512-CxN9N56HYfd2m/acc/NOFrZQsN9kU4eh+2kk6A707Kz1krH8tKmfrs5RnftB8WNX80T0NS7vSQsDOlg23diR2g==", "dev": true, + "license": "MIT", "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" + "es-abstract-get": "^1.0.0", + "es-errors": "^1.3.0", + "is-callable": "^1.2.7", + "is-date-object": "^1.1.0", + "is-symbol": "^1.1.1" }, "engines": { "node": ">= 0.4" @@ -3462,6 +3645,12 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -3480,18 +3669,6 @@ "node": ">=0.8.x" } }, - "node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dev": true, - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/extract-zip": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", @@ -3675,12 +3852,19 @@ "dev": true }, "node_modules/for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "dev": true, + "license": "MIT", "dependencies": { - "is-callable": "^1.1.3" + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/foreground-child": { @@ -3907,6 +4091,21 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -3917,15 +4116,21 @@ } }, "node_modules/function.prototype.name": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", - "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.2.0.tgz", + "integrity": "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0", - "functions-have-names": "^1.2.2" + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2", + "hasown": "^2.0.4", + "is-callable": "^1.2.7", + "is-document.all": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -3939,6 +4144,7 @@ "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -3971,16 +4177,22 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, + "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -3998,6 +4210,20 @@ "node": ">=8.0.0" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", @@ -4014,13 +4240,15 @@ } }, "node_modules/get-symbol-description": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", - "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" }, "engines": { "node": ">= 0.4" @@ -4124,12 +4352,14 @@ } }, "node_modules/globalthis": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", - "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", "dev": true, + "license": "MIT", "dependencies": { - "define-properties": "^1.1.3" + "define-properties": "^1.2.1", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -4159,12 +4389,13 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true, - "dependencies": { - "get-intrinsic": "^1.1.3" + "license": "MIT", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4195,10 +4426,14 @@ } }, "node_modules/has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -4225,10 +4460,14 @@ } }, "node_modules/has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -4237,10 +4476,11 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -4249,12 +4489,13 @@ } }, "node_modules/has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, + "license": "MIT", "dependencies": { - "has-symbols": "^1.0.2" + "has-symbols": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -4264,10 +4505,11 @@ } }, "node_modules/hash-sum": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-1.0.2.tgz", - "integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==", - "dev": true + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-2.0.0.tgz", + "integrity": "sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==", + "dev": true, + "license": "MIT" }, "node_modules/hasha": { "version": "5.2.2", @@ -4286,10 +4528,11 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "dev": true, + "license": "MIT", "dependencies": { "function-bind": "^1.1.2" }, @@ -4347,19 +4590,6 @@ "node": ">= 14" } }, - "node_modules/icss-utils": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", - "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "dev": true, - "peer": true, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -4468,17 +4698,19 @@ "version": "1.3.8", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/internal-slot": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", - "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", "dev": true, + "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.0", - "has": "^1.0.3", - "side-channel": "^1.0.4" + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -4529,14 +4761,18 @@ } }, "node_modules/is-array-buffer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", - "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.0", - "is-typed-array": "^1.1.10" + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4548,13 +4784,37 @@ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "dev": true }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", "dev": true, + "license": "MIT", "dependencies": { - "has-bigints": "^1.0.1" + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4573,13 +4833,14 @@ } }, "node_modules/is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -4588,12 +4849,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -4618,13 +4873,33 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", "dev": true, + "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -4633,13 +4908,20 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "node_modules/is-document.all": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-document.all/-/is-document.all-1.0.0.tgz", + "integrity": "sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==", "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4" + }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-extglob": { @@ -4651,10 +4933,26 @@ "node": ">=0.10.0" } }, - "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/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", "dev": true, "engines": { "node": ">=8" @@ -4687,11 +4985,25 @@ "node": ">=0.10.0" } }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -4709,12 +5021,14 @@ } }, "node_modules/is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", "dev": true, + "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -4754,14 +5068,30 @@ } }, "node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -4770,12 +5100,16 @@ } }, "node_modules/is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2" + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4794,12 +5128,14 @@ } }, "node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", "dev": true, + "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -4809,12 +5145,15 @@ } }, "node_modules/is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", "dev": true, + "license": "MIT", "dependencies": { - "has-symbols": "^1.0.2" + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -4824,16 +5163,13 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", - "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "dev": true, + "license": "MIT", "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" + "which-typed-array": "^1.1.16" }, "engines": { "node": ">= 0.4" @@ -4860,25 +5196,50 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2" + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-whitespace": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/is-whitespace/-/is-whitespace-0.3.0.tgz", - "integrity": "sha512-RydPhl4S6JwAyj0JJjshWJEFG6hNye3pZFBRZaTUfZFwGHxzppNaNOVgQuS/E/SlhrApuMXrpnK1EEIXfdo3Dg==", + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-windows": { @@ -5098,15 +5459,17 @@ } }, "node_modules/js-beautify": { - "version": "1.14.7", - "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.14.7.tgz", - "integrity": "sha512-5SOX1KXPFKx+5f6ZrPsIPEY7NwKeQz47n3jm2i+XeHx9MoRsfQenlOP13FQhWvg8JRS0+XLO6XYUQ2GX+q+T9A==", + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.4.tgz", + "integrity": "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==", "dev": true, + "license": "MIT", "dependencies": { "config-chain": "^1.1.13", - "editorconfig": "^0.15.3", - "glob": "^8.0.3", - "nopt": "^6.0.0" + "editorconfig": "^1.0.4", + "glob": "^10.4.2", + "js-cookie": "^3.0.5", + "nopt": "^7.2.1" }, "bin": { "css-beautify": "js/bin/css-beautify.js", @@ -5114,49 +5477,94 @@ "js-beautify": "js/bin/js-beautify.js" }, "engines": { - "node": ">=10" + "node": ">=14" } }, "node_modules/js-beautify/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, - "node_modules/js-beautify/node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "node_modules/js-beautify/node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "dev": true, + "license": "ISC", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" }, "engines": { - "node": ">=12" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/js-beautify/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/js-beautify/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, + "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { - "node": ">=10" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/js-beautify/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/js-cookie": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.8.tgz", + "integrity": "sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==", + "dev": true, + "license": "MIT" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -5262,18 +5670,6 @@ "json-buffer": "3.0.1" } }, - "node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -5465,6 +5861,15 @@ "yallist": "^3.0.2" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -5489,11 +5894,22 @@ "semver": "bin/semver.js" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/mdn-data": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==", - "dev": true + "dev": true, + "license": "CC0-1.0" }, "node_modules/memfs": { "version": "3.4.13", @@ -5507,15 +5923,6 @@ "node": ">= 4.0.0" } }, - "node_modules/merge-source-map": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.1.0.tgz", - "integrity": "sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==", - "dev": true, - "dependencies": { - "source-map": "^0.6.1" - } - }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -5603,6 +6010,7 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -5727,6 +6135,7 @@ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, + "license": "MIT", "dependencies": { "minimist": "^1.2.6" }, @@ -5999,18 +6408,19 @@ "dev": true }, "node_modules/nopt": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", - "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", "dev": true, + "license": "ISC", "dependencies": { - "abbrev": "^1.0.0" + "abbrev": "^2.0.0" }, "bin": { "nopt": "bin/nopt.js" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/normalize-path": { @@ -6332,6 +6742,7 @@ "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "boolbase": "~1.0.0" } @@ -6538,10 +6949,11 @@ } }, "node_modules/object-inspect": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", - "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -6559,14 +6971,17 @@ } }, "node_modules/object.assign": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", - "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "has-symbols": "^1.0.3", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", "object-keys": "^1.1.1" }, "engines": { @@ -6577,32 +6992,38 @@ } }, "node_modules/object.getownpropertydescriptors": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.5.tgz", - "integrity": "sha512-yDNzckpM6ntyQiGTik1fKV1DcVDRS+w8bvpWNCBanvH5LfRX9O8WTHqQzG4RZwRAM4I0oU7TV11Lj5v0g20ibw==", + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.9.tgz", + "integrity": "sha512-mt8YM6XwsTTovI+kdZdHSxoyF2DI59up034orlC9NfweclcWOt7CVascNNLp6U+bjFVCVCIh9PwS76tDM/rH8g==", "dev": true, + "license": "MIT", "dependencies": { - "array.prototype.reduce": "^1.0.5", - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "array.prototype.reduce": "^1.0.8", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "gopd": "^1.2.0", + "safe-array-concat": "^1.1.3" }, "engines": { - "node": ">= 0.8" + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/object.values": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", - "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -6637,6 +7058,24 @@ "node": ">= 0.8.0" } }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -6860,9 +7299,10 @@ "dev": true }, "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", @@ -6940,10 +7380,20 @@ "node": ">=8" } }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/postcss": { - "version": "8.4.32", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.32.tgz", - "integrity": "sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==", + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", "funding": [ { "type": "opencollective", @@ -6958,108 +7408,27 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" } }, - "node_modules/postcss-modules-extract-imports": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", - "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", - "dev": true, - "peer": true, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-local-by-default": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz", - "integrity": "sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==", - "dev": true, - "peer": true, - "dependencies": { - "icss-utils": "^5.0.0", - "postcss-selector-parser": "^6.0.2", - "postcss-value-parser": "^4.1.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-scope": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", - "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", - "dev": true, - "peer": true, - "dependencies": { - "postcss-selector-parser": "^6.0.4" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-values": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", - "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", - "dev": true, - "peer": true, - "dependencies": { - "icss-utils": "^5.0.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.0.11", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz", - "integrity": "sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==", - "dev": true, - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true, - "peer": true - }, "node_modules/postcss/node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.14", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.14.tgz", + "integrity": "sha512-U9kYi5bpVMEI31yC8iw4bJJp0avcHXA0W8/wNfLfnvJYzihQo2ZRPYPvpAAd570HAcCBjCTN7vnr+v4StKl1IQ==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -7080,7 +7449,7 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.2.1.tgz", "integrity": "sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q==", - "devOptional": true, + "dev": true, "bin": { "prettier": "bin-prettier.js" }, @@ -7088,20 +7457,6 @@ "node": ">=10.13.0" } }, - "node_modules/pretty": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pretty/-/pretty-2.0.0.tgz", - "integrity": "sha512-G9xUchgTEiNpormdYBl+Pha50gOUovT18IvAe7EYMZ1/f9W/WWMPRn+xI68yXNMUk3QXHDwo/1wV/4NejVNe1w==", - "dev": true, - "dependencies": { - "condense-newlines": "^0.2.1", - "extend-shallow": "^2.0.1", - "js-beautify": "^1.6.12" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -7136,7 +7491,8 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/proxy-agent": { "version": "6.4.0", @@ -7172,12 +7528,6 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "dev": true }, - "node_modules/pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==", - "dev": true - }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -7297,7 +7647,9 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", + "deprecated": "You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.\n\n(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)", "dev": true, + "license": "MIT", "engines": { "node": ">=0.6.0", "teleport": ">=0.2.0" @@ -7401,15 +7753,42 @@ "node": ">= 10.13.0" } }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/regexp.prototype.flags": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", - "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -7541,6 +7920,33 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-array-concat": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", + "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-array-concat/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -7561,15 +7967,43 @@ } ] }, - "node_modules/safe-regex-test": { + "node_modules/safe-push-apply": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", - "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "is-regex": "^1.1.4" + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -7596,7 +8030,8 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/schema-utils": { "version": "3.3.0", @@ -7681,6 +8116,37 @@ "node": ">= 0.4" } }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/shallow-clone": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", @@ -7724,15 +8190,17 @@ } }, "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -7741,11 +8209,61 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/sigmund": { + "node_modules/side-channel-list": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", - "integrity": "sha512-fCvEXfh6NWpm+YSuY2bpXb/VIihqWA6hLsgboC+0nl71Q7N7o2eaCW8mJa/NLvQhs6jpd3VZV4UiUQlV6+lc8g==", - "dev": true + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/signal-exit": { "version": "3.0.7", @@ -7935,18 +8453,26 @@ "node": ">= 14" } }, + "node_modules/sortablejs": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.14.0.tgz", + "integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==", + "license": "MIT" + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, "engines": { "node": ">=0.10.0" } }, "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -8011,7 +8537,22 @@ "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", "deprecated": "Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } }, "node_modules/stream-browserify": { "version": "3.0.0", @@ -8076,14 +8617,20 @@ } }, "node_modules/string.prototype.trim": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz", - "integrity": "sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==", + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.11.tgz", + "integrity": "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-object-atoms": "^1.1.2", + "has-property-descriptors": "^1.0.2", + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -8093,28 +8640,37 @@ } }, "node_modules/string.prototype.trimend": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", - "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.10.tgz", + "integrity": "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/string.prototype.trimstart": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", - "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -8210,24 +8766,13 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/svg-to-vue": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/svg-to-vue/-/svg-to-vue-0.7.0.tgz", - "integrity": "sha512-Tg2nMmf3BQorYCAjxbtTkYyWPVSeox5AZUFvfy4MoWK/5tuQlnA/h3LAlTjV3sEvOC5FtUNovRSj3p784l4KOA==", - "dev": true, - "dependencies": { - "svgo": "^1.3.2" - }, - "peerDependencies": { - "vue-template-compiler": "^2.0.0" - } - }, "node_modules/svgo": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz", "integrity": "sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==", "deprecated": "This SVGO version is no longer supported. Upgrade to v2.x.x.", "dev": true, + "license": "MIT", "dependencies": { "chalk": "^2.4.1", "coa": "^2.0.2", @@ -8421,20 +8966,6 @@ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", "dev": true }, - "node_modules/ticky": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ticky/-/ticky-1.0.1.tgz", - "integrity": "sha512-RX35iq/D+lrsqhcPWIazM9ELkjOe30MSeoBHQHSsRwd1YuhJO5ui1K1/R0r7N3mFvbLBs33idw+eR6j+w6i/DA==" - }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -8578,15 +9109,79 @@ "node": ">=8" } }, - "node_modules/typed-array-length": { + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", - "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", "for-each": "^0.3.3", - "is-typed-array": "^1.1.9" + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.8.tgz", + "integrity": "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "for-each": "^0.3.5", + "gopd": "^1.2.0", + "is-typed-array": "^1.1.15", + "possible-typed-array-names": "^1.1.0", + "reflect.getprototypeof": "^1.0.10" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -8605,7 +9200,7 @@ "version": "5.0.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.3.tgz", "integrity": "sha512-xv8mOEDnigb/tN9PSMTwSEqAnUvkoXMQlicOb0IUVDBSQCgBSaAAROUZYy2IcUy5qU6XajK5jjjO7TMWqBTKZA==", - "dev": true, + "devOptional": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8615,15 +9210,19 @@ } }, "node_modules/unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", + "call-bound": "^1.0.3", "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -8676,7 +9275,8 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz", "integrity": "sha512-vRCqFv6UhXpWxZPyGDh/F3ZpNv8/qo7w6iufLpQg9aKnQ71qM4B5KiI7Mia9COcjEhrO9LueHpMYjYzsWH3OIg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/update-browserslist-db": { "version": "1.1.0", @@ -8780,6 +9380,7 @@ "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.1.tgz", "integrity": "sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA==", "dev": true, + "license": "MIT", "dependencies": { "define-properties": "^1.1.3", "es-abstract": "^1.17.2", @@ -8791,180 +9392,169 @@ } }, "node_modules/vue": { - "version": "2.7.16", - "resolved": "https://registry.npmjs.org/vue/-/vue-2.7.16.tgz", - "integrity": "sha512-4gCtFXaAA3zYZdTp5s4Hl2sozuySsgz4jy1EnpBHNfpMa9dK1ZCG7viqBPCwXtmgc8nHqUsAu3G4gtmXkkY3Sw==", - "deprecated": "Vue 2 has reached EOL and is no longer actively maintained. See https://v2.vuejs.org/eol/ for more details.", + "version": "3.5.38", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.38.tgz", + "integrity": "sha512-vAMKHfImQlYSy0C+PBue4s3ERZ2xGKfgZg5GXAsLInq1dyh2H78ILVP5sK0KPFPVW4kv+OGCIvBEondcjpZp7A==", + "license": "MIT", "dependencies": { - "@vue/compiler-sfc": "2.7.16", - "csstype": "^3.1.0" + "@vue/compiler-dom": "3.5.38", + "@vue/compiler-sfc": "3.5.38", + "@vue/runtime-dom": "3.5.38", + "@vue/server-renderer": "3.5.38", + "@vue/shared": "3.5.38" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/vue-hot-reload-api": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz", - "integrity": "sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog==", - "dev": true + "node_modules/vue-component-type-helpers": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-3.3.5.tgz", + "integrity": "sha512-Fe1jyPJoUGpJOYKOri44jduR7My4yYINOMJISuMAbmrs+L5LbIDUc8NTWZYY3EJLK0yPLuCmcd5zoCsE4k2/KA==", + "dev": true, + "license": "MIT" }, "node_modules/vue-loader": { - "version": "15.11.1", - "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-15.11.1.tgz", - "integrity": "sha512-0iw4VchYLePqJfJu9s62ACWUXeSqM30SQqlIftbYWM3C+jpPcEHKSPUZBLjSF9au4HTHQ/naF6OGnO3Q/qGR3Q==", + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-17.4.2.tgz", + "integrity": "sha512-yTKOA4R/VN4jqjw4y5HrynFL8AK0Z3/Jt7eOJXEitsm0GMRHDBjCfCiuTiLP7OESvsZYo2pATCWhDqxC5ZrM6w==", "dev": true, + "license": "MIT", "dependencies": { - "@vue/component-compiler-utils": "^3.1.0", - "hash-sum": "^1.0.2", - "loader-utils": "^1.1.0", - "vue-hot-reload-api": "^2.3.0", - "vue-style-loader": "^4.1.0" + "chalk": "^4.1.0", + "hash-sum": "^2.0.0", + "watchpack": "^2.4.0" }, "peerDependencies": { - "css-loader": "*", - "webpack": "^3.0.0 || ^4.1.0 || ^5.0.0-0" + "webpack": "^4.1.0 || ^5.0.0-0" }, "peerDependenciesMeta": { - "cache-loader": { - "optional": true - }, - "prettier": { + "@vue/compiler-sfc": { "optional": true }, - "vue-template-compiler": { + "vue": { "optional": true } } }, - "node_modules/vue-loader/node_modules/json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "node_modules/vue-loader/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==", "dev": true, + "license": "MIT", "dependencies": { - "minimist": "^1.2.0" + "color-convert": "^2.0.1" }, - "bin": { - "json5": "lib/cli.js" + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/vue-loader/node_modules/loader-utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", - "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "node_modules/vue-loader/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, + "license": "MIT", "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^1.0.1" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/vue-style-loader": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.3.tgz", - "integrity": "sha512-sFuh0xfbtpRlKfm39ss/ikqs9AbKCoXZBpHeVZ8Tx650o0k0q/YCM7FRvigtxpACezfq6af+a7JeqVTWvncqDg==", - "dev": true, - "dependencies": { - "hash-sum": "^1.0.2", - "loader-utils": "^1.0.2" - } - }, - "node_modules/vue-style-loader/node_modules/json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", - "dev": true, - "dependencies": { - "minimist": "^1.2.0" + "node": ">=10" }, - "bin": { - "json5": "lib/cli.js" + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/vue-style-loader/node_modules/loader-utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", - "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "node_modules/vue-loader/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==", "dev": true, + "license": "MIT", "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^1.0.1" + "color-name": "~1.1.4" }, "engines": { - "node": ">=4.0.0" + "node": ">=7.0.0" } }, - "node_modules/vue-svg-loader": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/vue-svg-loader/-/vue-svg-loader-0.16.0.tgz", - "integrity": "sha512-2RtFXlTCYWm8YAEO2qAOZ2SuIF2NvLutB5muc3KDYoZq5ZeCHf8ggzSan3ksbbca7CJ/Aw57ZnDF4B7W/AkGtw==", + "node_modules/vue-loader/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==", "dev": true, - "dependencies": { - "loader-utils": "^1.2.3", - "svg-to-vue": "^0.7.0" - }, - "peerDependencies": { - "vue-template-compiler": "^2.0.0" - } + "license": "MIT" }, - "node_modules/vue-svg-loader/node_modules/json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "node_modules/vue-loader/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" + "license": "MIT", + "engines": { + "node": ">=8" } }, - "node_modules/vue-svg-loader/node_modules/loader-utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", - "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "node_modules/vue-loader/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, + "license": "MIT", "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^1.0.1" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=4.0.0" + "node": ">=8" } }, - "node_modules/vue-template-compiler": { - "version": "2.7.16", - "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz", - "integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==", + "node_modules/vue-svg-loader": { + "version": "0.17.0-beta.2", + "resolved": "https://registry.npmjs.org/vue-svg-loader/-/vue-svg-loader-0.17.0-beta.2.tgz", + "integrity": "sha512-iMUGJTKEcuNAG8VXOchjA8443IqEmEi2Aw6EVIHWma2cC4TUQ7Oet5Yry9IFfqXQXXvyzXz5EyttVvfRGTNH4Q==", "dev": true, + "license": "MIT", "dependencies": { - "de-indent": "^1.0.2", - "he": "^1.2.0" + "loader-utils": "^2.0.0", + "semver": "^7.3.2", + "svgo": "^1.3.2" + }, + "peerDependencies": { + "vue": "^2.5.0 || ^3.0.0-0" } }, - "node_modules/vue-template-es2015-compiler": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz", - "integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==", - "dev": true - }, - "node_modules/vue2-dragula": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/vue2-dragula/-/vue2-dragula-2.5.5.tgz", - "integrity": "sha512-y+s2S1s6p11ds5ay6kWgAzxmXa4LwM8HBrQG+q8+rPehrmOlV/kvEyNidEYL+glskofL5vTGhno4xGYfg+wm3Q==", + "node_modules/vuedraggable": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-4.1.0.tgz", + "integrity": "sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==", + "license": "MIT", "dependencies": { - "dragula": "3.7.2" + "sortablejs": "1.14.0" + }, + "peerDependencies": { + "vue": "^3.0.1" } }, "node_modules/vuex": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/vuex/-/vuex-3.6.2.tgz", - "integrity": "sha512-ETW44IqCgBpVomy520DT5jf8n0zoCac+sxWnn+hMe/CzaSejb/eVw2YToiXYX+Ex/AuHHia28vWTq4goAexFbw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vuex/-/vuex-4.1.0.tgz", + "integrity": "sha512-hmV6UerDrPcgbSy9ORAtNXDr9M4wlNP4pEFKye4ujJF8oqgFFuxDCdOLS3eNoRTtq5O3hoBDh9Doj1bQMYHRbQ==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.0.0-beta.11" + }, "peerDependencies": { - "vue": "^2.0.0" + "vue": "^3.2.0" } }, "node_modules/watchpack": { @@ -9118,16 +9708,74 @@ } }, "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/which-collection": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", "dev": true, + "license": "MIT", "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -9140,17 +9788,19 @@ "dev": true }, "node_modules/which-typed-array": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", - "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", + "version": "1.1.22", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", + "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", "dev": true, + "license": "MIT", "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0", - "is-typed-array": "^1.1.10" + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" diff --git a/package.json b/package.json index 4970fa5be..dffba57c6 100644 --- a/package.json +++ b/package.json @@ -27,17 +27,18 @@ "@types/chai": "^4.2.14", "@types/chrome": "^0.0.266", "@types/crypto-js": "^4.1.1", + "@types/lodash": "^4.14.166", "@types/mocha": "^10.0.6", "@types/sinon": "^17.0.2", "@types/sinon-chai": "^3.2.12", "@types/sinon-chrome": "^2.2.10", "@typescript-eslint/eslint-plugin": "^7.15.0", "@typescript-eslint/parser": "^7.15.0", - "@vue/test-utils": "^1.1.1", + "@vue/compiler-sfc": "^3.4.21", + "@vue/test-utils": "^2.4.5", "base64-loader": "^1.0.0", "buffer": "^6.0.3", "chai": "^4.2.0", - "crypto-js": "^4.1.1", "eslint": "^8.56.0", "fork-ts-checker-webpack-plugin": "^6.5.3", "lodash": "^4.17.21", @@ -56,22 +57,21 @@ "typescript": "^5.0.0", "url-loader": "^4.0.0", "util": "^0.12.5", - "vue-loader": "^15.10.1", - "vue-svg-loader": "^0.16.0", - "vue-template-compiler": "^2.7.16", + "vue-loader": "^17.4.2", + "vue-svg-loader": "0.17.0-beta.2", "webpack": "^5.94.0", "webpack-cli": "^5.0.0", "webpack-merge": "^5.0.0" }, "dependencies": { - "@types/lodash": "^4.14.166", "argon2-browser": "^1.18.0", + "crypto-js": "^4.1.1", "jsqr": "^1.3.1", "node-gost-crypto": "^1.0.2", "qrcode-generator": "^1.4.4", "qrcode-reader": "^1.0.4", - "vue": "^2.7.16", - "vue2-dragula": "^2.5.4", - "vuex": "^3.4.0" + "vue": "^3.4.21", + "vuedraggable": "^4.1.0", + "vuex": "^4.1.0" } } diff --git a/privacy-policy.md b/privacy-policy.md new file mode 100644 index 000000000..04eafe9f1 --- /dev/null +++ b/privacy-policy.md @@ -0,0 +1,57 @@ +# Privacy Policy — OTPilot Authenticator + +_Last updated: 2026-06-26_ + +OTPilot Authenticator ("the extension") is a browser extension that generates two-factor +authentication (2FA) one-time codes. This policy explains what data the extension handles +and how. + +## What we collect + +We — the developer — **do not collect, receive, or transmit any of your data**. There are +no analytics, no tracking, and no telemetry. The extension has no server of its own. + +## Data the extension stores on your device + +The extension stores the following **locally**, in your browser's extension storage: + +- Your 2FA accounts: issuer/label and the account secret. +- Your preferences (e.g. theme, auto-lock timeout). + +If you set a master password, account secrets are encrypted with **Argon2id** key +derivation and **AES-GCM** before being stored. The encrypted vault auto-locks after the +idle period you configure. + +## Optional cloud backup + +If you explicitly enable cloud backup, the extension uploads your **encrypted** vault to a +cloud drive **you own and authorize** — Google Drive or Dropbox. The backup is encrypted +with your master password; the developer cannot read it and never receives a copy. The +extension connects only to the chosen provider's official API endpoints, and only after +you opt in. You can disconnect at any time. + +## Autofill + +When you trigger autofill, the current code is inserted into the active tab's login field, +**only** when the page's host matches the saved account. No page content is read, stored, +or transmitted to the developer. + +## Permissions + +The extension requests only the permissions needed for the above functions (storage, +active-tab access for autofill, scripting for autofill, identity for cloud-backup OAuth, +and alarms for auto-lock). Cloud-provider host access is requested optionally, only when +you enable backup. + +## Data sharing + +We do not sell, share, or transfer your data to any third party. The only data that leaves +your device is the encrypted backup you choose to upload to your own cloud account. + +## Changes + +We may update this policy; the "Last updated" date will change accordingly. + +## Contact + +Questions: kyvevcwmm@mozmail.com diff --git a/sass/_tokens.scss b/sass/_tokens.scss new file mode 100644 index 000000000..ec7e37a45 --- /dev/null +++ b/sass/_tokens.scss @@ -0,0 +1,112 @@ +// Design tokens — CSS custom properties. +// Replaces the old SCSS $themes map + themify mixin (see _ui.scss). +// Light is the :root default; theme classes on the app root override it. + +:root { + // Surfaces + --canvas: #e7e5df; + --app-bg: #ffffff; + --header: #ffffff; + --row: #f6f6f3; + --row-hover: #eeeeea; + + // Lines + --border: rgba(20, 20, 18, 0.08); + --border-strong: rgba(20, 20, 18, 0.16); + + // Text + --text: #1a1a17; + --text-dim: #6c6c66; + --text-faint: #a3a39a; + --label: #8a8a82; + + // Accent (hue 258 — cobalt) + --accent: oklch(0.53 0.19 258); + --accent-fg: #ffffff; + --accent-soft: oklch(0.95 0.04 258); + + // Status + --ok: oklch(0.64 0.15 155); + --warn: oklch(0.76 0.15 70); + --warn-soft: oklch(0.96 0.05 70); + --danger: oklch(0.6 0.2 25); + + // Misc + --ring-track: rgba(20, 20, 18, 0.1); + --shadow: 0 14px 40px rgba(20, 20, 18, 0.16); + --overlay: rgba(20, 20, 18, 0.5); // dim scrim over the app (modals) + --scrim: rgba(255, 255, 255, 0.5); // light wash (QR overlay) + + // Type & shape + --font-sans: system-ui, -apple-system, "Segoe UI", "Microsoft JhengHei", + "Microsoft YaHei", Roboto, Helvetica, Arial, sans-serif; + --font-mono: ui-monospace, "SF Mono", "JetBrains Mono", "Cascadia Code", + "Roboto Mono", Consolas, "Liberation Mono", monospace; + --radius-card: 22px; + --radius-row: 14px; + --radius-pill: 999px; +} + +@mixin dark-tokens { + --canvas: #19191c; + --app-bg: #1a1a1e; + --header: #1e1e22; + --row: #222227; + --row-hover: #2a2a30; + --border: rgba(255, 255, 255, 0.08); + --border-strong: rgba(255, 255, 255, 0.16); + --text: #f1f1ef; + --text-dim: #9b9ba2; + --text-faint: #6a6a72; + --label: rgba(255, 255, 255, 0.5); + --accent: oklch(0.72 0.15 258); + --accent-fg: #16161a; + --accent-soft: oklch(0.3 0.07 258); + --ok: oklch(0.72 0.15 155); + --warn: oklch(0.8 0.14 70); + --warn-soft: oklch(0.32 0.06 70); + --danger: oklch(0.68 0.19 25); + --ring-track: rgba(255, 255, 255, 0.12); + --shadow: 0 18px 48px rgba(0, 0, 0, 0.55); + --overlay: rgba(0, 0, 0, 0.6); + --scrim: rgba(0, 0, 0, 0.5); +} + +.theme-dark { + @include dark-tokens; +} + +// auto = light by default, dark when the OS asks for it +.theme-auto { + @media (prefers-color-scheme: dark) { + @include dark-tokens; + } +} + +// High-contrast (was the old "accessibility" theme): white-on-black + yellow. +.theme-accessibility { + --canvas: #000; + --app-bg: #000; + --header: #000; + --row: #000; + --row-hover: #1a1a1a; + --border: #fff; + --border-strong: #fff; + --text: #fff; + --text-dim: #fff; + --text-faint: #fff; + --label: #fff; + --accent: #ffff00; + --accent-fg: #000; + --accent-soft: #333300; + --ok: #33ff66; + --warn: #ffd400; + --warn-soft: #332600; + --danger: #ff5040; + --ring-track: rgba(255, 255, 255, 0.45); + --shadow: none; + --overlay: rgba(0, 0, 0, 0.85); + --scrim: rgba(0, 0, 0, 0.7); +} + +// Compact reuses the light palette; only density changes (see popup.scss). diff --git a/sass/_ui.scss b/sass/_ui.scss index d33693e17..5f79f2a43 100644 --- a/sass/_ui.scss +++ b/sass/_ui.scss @@ -1,217 +1,56 @@ -// Re-usable ui components +// Re-usable ui components. +// Theming is CSS custom properties (see _tokens.scss). -// Colors -// go from darkest to lightest - -$themes: ( - normal: ( - black-1: black, - black-transparent: rgba(0, 0, 0, 0.5), - white-1: white, - white-transparent: rgba(255, 255, 255, 0.5), - grey-1: grey, - grey-2: #ccc, - grey-3: #eee, - grey-background: #eee, - blue-1: #08c, - yellow-1: #fff1ba, - yellow-2: #fff4cc, - red-1: #dd4b39, - red-2: #eea59c, - black-search: #2a2a2e, - white-search: #f9f9fa, - grey-search: #b1b1b3, - blue-menu: #f4fcff, - ), - accessibility: ( - black-1: white, - black-transparent: rgba(255, 255, 255, 1), - white-1: black, - white-transparent: rgba(0, 0, 0, 0.5), - grey-1: white, - grey-2: white, - grey-3: white, - grey-background: black, - blue-1: yellow, - yellow-1: yellow, - yellow-2: yellow, - red-1: red, - red-2: red, - black-search: white, - white-search: black, - grey-search: white, - blue-menu: cyan, - ), - dark: ( - black-1: #ccc, - black-transparent: rgba(255, 255, 255, 0.5), - white-1: #242424, - white-transparent: rgba(0, 0, 0, 0.5), - grey-1: grey, - grey-2: rgba(255, 255, 255, 0.15), - grey-3: #444, - grey-background: #1e1e1e, - blue-1: white, - yellow-1: rgba(255, 255, 255, 0.5), - yellow-2: rgba(255, 255, 255, 0.35), - red-1: #dd4b39, - red-2: #61221a, - black-search: white, - white-search: #202020, - grey-search: rgba(255, 255, 255, 0.35), - blue-menu: #2a2d2e, - ), - simple: ( - black-1: black, - black-transparent: rgba(0, 0, 0, 0.5), - white-1: white, - white-transparent: rgba(255, 255, 255, 0.5), - grey-1: grey, - grey-2: #ccc, - grey-3: #eee, - grey-background: #fff, - blue-1: #08c, - yellow-1: #fff1ba, - yellow-2: #fff4cc, - red-1: #dd4b39, - red-2: #eea59c, - black-search: #2a2a2e, - white-search: #f9f9fa, - grey-search: #b1b1b3, - blue-menu: #f4fcff, - ), - compact: ( - black-1: black, - black-transparent: rgba(0, 0, 0, 0.5), - white-1: white, - white-transparent: rgba(255, 255, 255, 0.5), - grey-1: grey, - grey-2: #ccc, - grey-3: #eee, - grey-background: #fff, - blue-1: #08c, - yellow-1: #fff1ba, - yellow-2: #fff4cc, - red-1: #dd4b39, - red-2: #eea59c, - black-search: #2a2a2e, - white-search: #f9f9fa, - grey-search: #b1b1b3, - blue-menu: #f4fcff, - ), - flat: ( - black-1: black, - black-transparent: rgba(0, 0, 0, 0.5), - white-1: white, - white-transparent: rgba(255, 255, 255, 0.5), - grey-1: grey, - grey-2: #ccc, - grey-3: #eee, - grey-background: #eee, - blue-1: #08c, - yellow-1: #fff1ba, - yellow-2: #fff4cc, - red-1: #dd4b39, - red-2: #eea59c, - black-search: #2a2a2e, - white-search: #f9f9fa, - grey-search: #b1b1b3, - blue-menu: #f4fcff, - ), -); - -$theme-map: null; - -@mixin themify($themes: $themes) { - @each $theme, $map in $themes { - .theme-#{$theme} & { - $theme-map: () !global; - @each $key, $submap in $map { - $value: map-get(map-get($themes, $theme), "#{$key}"); - $theme-map: map-merge( - $theme-map, - ( - $key: $value, - ) - ) !global; - } - @content; - $theme-map: null !global; - } - } -} - -@function themed($key) { - @return map-get($theme-map, $key); -} - -// Shared -@mixin hover-black { - &:hover { - svg { - @include themify($themes) { - fill: themed("black-1"); - } - } - } -} - -@mixin icon-special($size, $color) { - svg { - vertical-align: middle; - fill: $color; - height: $size; - width: $size; - } -} +@import "tokens"; // Classes .button { margin: 10px; - padding: 20px; - border-radius: 2px; + padding: 14px; + border-radius: var(--radius-row); position: relative; text-align: center; - font-size: 16px; + font-size: 15px; + font-weight: 600; + font-family: var(--font-sans); width: -moz-available; width: -webkit-fill-available; - @include themify($themes) { - background: themed("white-1"); - border: themed("grey-2") 1px solid; - color: themed("grey-1"); - } + background: var(--row); + border: var(--border) 1px solid; + color: var(--text); cursor: pointer; &:hover { - @include themify($themes) { - color: themed("black-1"); - } + background: var(--row-hover); + color: var(--text); } } .button-small { @extend .button; - font-size: 12px; - margin: 20px 100px; - padding: 10px; + font-size: 13px; + margin: 16px 0; + padding: 11px; } .input { display: block; margin: 15px 10px 20px 10px; - padding: 5px 10px; + padding: 11px 13px; width: 260px; - border: none; - @include themify($themes) { - color: themed("black-1"); - border-bottom: themed("black-1") 1px solid; - background: themed("grey-3"); - } + border: var(--border-strong) 1px solid; + border-radius: var(--radius-row); + font-family: var(--font-sans); + color: var(--text); + background: var(--row); outline: none; + + &:focus { + border-color: var(--accent); + background: var(--app-bg); + } } a { - @include themify($themes) { - color: themed("blue-1"); - } + color: var(--accent); } diff --git a/sass/import.scss b/sass/import.scss index 798beefd6..0473d7555 100644 --- a/sass/import.scss +++ b/sass/import.scss @@ -1,75 +1,143 @@ @import "ui"; -[v-cloak] { - display: none; +* { + box-sizing: border-box; } -* { - font-family: arial, "Microsoft YaHei"; +html, +body { + margin: 0; +} + +body { + font-family: var(--font-sans); +} + +[v-cloak] { + display: none; } p { - font-size: 16px; + font-size: 15px; } #import { - width: 900px; - position: relative; - margin: 0 auto; + min-height: 100vh; + background: var(--canvas); + color: var(--text); + padding: 48px 24px; } -#import_info { - margin: 10px 20px 20px 20px; +.import-card { + max-width: 760px; + margin: 0 auto; + background: var(--app-bg); + border: 1px solid var(--border); + border-radius: var(--radius-card); + box-shadow: var(--shadow); + overflow: hidden; } -.import_tab { - text-align: center; - font-size: 0; +.import-header { + display: flex; + align-items: center; + gap: 13px; + padding: 34px 44px 0; - input { - display: none; + .import-logo { + width: 32px; + height: 32px; + border-radius: 10px; + background: var(--accent); + color: var(--accent-fg); + display: flex; + align-items: center; + justify-content: center; + flex: none; - &:checked + label { - background: #eee; + svg { + width: 18px; + height: 18px; } } - label { - width: 250px; - height: 50px; - font-size: 18px; - text-align: center; - display: inline-grid; - align-items: center; - margin: 20px; - cursor: pointer; - border-radius: 2px; + h1 { + font-size: 24px; + font-weight: 800; + letter-spacing: -0.02em; + margin: 0; + } +} - &:hover { - background: #eee; - } +.import-desc { + padding: 8px 44px 0; + font-size: 14px; + line-height: 1.55; + color: var(--text-dim); + max-width: 620px; +} + +// Interactive underline tabs +.import-tabs { + display: flex; + gap: 4px; + padding: 24px 44px 0; + border-bottom: 1px solid var(--border); +} + +.import-tab { + display: flex; + align-items: center; + gap: 9px; + margin-bottom: -1px; + padding: 13px 20px; + border: none; + border-bottom: 2.5px solid transparent; + background: transparent; + color: var(--text-dim); + font-family: var(--font-sans); + font-size: 14px; + font-weight: 700; + cursor: pointer; + + svg { + width: 17px; + height: 17px; + } + + &:hover { + color: var(--text); + } + + &.active { + color: var(--accent); + border-bottom-color: var(--accent); } } -button, -.import_file label { - display: inline-grid; - width: 260px !important; - height: 60px; - border: #ccc 1px solid; - background: white; - border-radius: 2px; - position: relative; - text-align: center; +.import-panel { + padding: 30px 44px 38px; +} + +.import-panel button { + display: inline-flex; align-items: center; - font-size: 16px; - color: gray; + justify-content: center; + min-width: 200px; + height: 46px; + padding: 0 22px; + border: none; + border-radius: var(--radius-row); + background: var(--accent); + color: var(--accent-fg); + font-family: var(--font-sans); + font-size: 14px; + font-weight: 700; cursor: pointer; outline: none; - margin-left: 0px !important; &:hover { - color: black; + filter: brightness(1.06); } } @@ -79,45 +147,272 @@ button, input { display: none; } + + label { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 8px; + padding: 44px 20px; + border: 2px dashed var(--border-strong); + border-radius: 18px; + background: var(--row); + color: var(--text-dim); + font-family: var(--font-sans); + font-size: 15px; + font-weight: 700; + cursor: pointer; + + &:hover { + border-color: var(--accent); + } + } +} + +// File import panel (design "18 IMPORT" — file tab) +.file-import { + .dropzone { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 16px; + padding: 46px 20px; + border: 2px dashed var(--border-strong); + border-radius: 18px; + background: var(--row); + text-align: center; + cursor: pointer; + + &:hover { + border-color: var(--accent); + } + + input { + display: none; + } + } + + .dropzone-icon { + width: 58px; + height: 58px; + border-radius: 16px; + background: var(--accent-soft); + color: var(--accent); + display: flex; + align-items: center; + justify-content: center; + + svg { + width: 28px; + height: 28px; + } + } + + .dropzone-title { + font-size: 16px; + font-weight: 700; + margin-bottom: 4px; + } + + .dropzone-hint { + font-size: 13px; + color: var(--text-faint); + } + + .dropzone-btn { + padding: 11px 22px; + border-radius: 12px; + background: var(--accent); + color: var(--accent-fg); + font-size: 13.5px; + font-weight: 700; + } + + .import-note { + display: flex; + align-items: center; + gap: 11px; + margin-top: 18px; + padding: 14px 16px; + border-radius: 13px; + background: var(--row); + color: var(--text-dim); + + svg { + flex: none; + width: 18px; + height: 18px; + } + + div { + flex: 1; + font-size: 13px; + } + } + + .file-card { + display: flex; + align-items: center; + gap: 13px; + padding: 16px; + border-radius: 14px; + background: var(--row); + border: 1px solid var(--border); + margin-bottom: 20px; + } + + .file-card-icon { + width: 42px; + height: 42px; + border-radius: 11px; + flex: none; + background: var(--accent-soft); + color: var(--accent); + display: flex; + align-items: center; + justify-content: center; + + svg { + width: 21px; + height: 21px; + } + } + + .file-card-text { + flex: 1; + min-width: 0; + } + + .file-card-name { + font-size: 14px; + font-weight: 700; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .file-card-badge { + display: flex; + align-items: center; + gap: 5px; + font-size: 12px; + font-weight: 600; + color: var(--warn); + + svg { + width: 12px; + height: 12px; + flex: none; + } + } + + .file-card-remove { + width: 30px; + height: 30px; + border-radius: 9px; + flex: none; + background: var(--app-bg); + color: var(--text-dim); + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + + svg { + width: 15px; + height: 15px; + } + + &:hover { + color: var(--text); + } + } + + .field-label { + display: block; + font-size: 12px; + font-weight: 700; + color: var(--text-dim); + margin-bottom: 8px; + } + + .pass-input { + width: 100%; + padding: 14px; + border-radius: 13px; + border: 1px solid var(--accent); + background: var(--app-bg); + color: var(--text); + font-family: var(--font-mono); + font-size: 15px; + letter-spacing: 2px; + outline: none; + } + + .pass-hint { + font-size: 12px; + color: var(--text-faint); + margin-top: 8px; + } + + .pass-submit { + margin-top: 18px; + } } .import_encrypted { - margin-bottom: 20px; + margin-bottom: 18px; input { margin-left: 0; + accent-color: var(--accent); } } .import_code { - float: left; - margin-left: 30px; - margin-right: 40px; - textarea { - width: 250px; - height: 400px; - padding: 10px; + width: 100%; + height: 320px; + padding: 12px; + border: 1px solid var(--border-strong); + border-radius: var(--radius-row); + background: var(--row); + color: var(--text); + font-family: var(--font-mono); + font-size: 13px; outline: none; resize: none; - box-sizing: border-box; + + &:focus { + border-color: var(--accent); + background: var(--app-bg); + } } } .error_password { - font-size: 18px; - color: gray; + font-size: 16px; + color: var(--text-dim); text-align: center; } .import_passphrase input, .import_file_passphrase_input input { - padding: 10px; - margin-bottom: 20px; - width: 250px; - border: #ccc 1px solid; - background: white; + padding: 11px 13px; + margin-bottom: 16px; + width: 100%; + border: 1px solid var(--border-strong); + border-radius: var(--radius-row); + background: var(--row); + color: var(--text); outline: none; + + &:focus { + border-color: var(--accent); + background: var(--app-bg); + } } .import_file_passphrase { @@ -126,6 +421,6 @@ button, } .import_file_passphrase_input { - display: inline-grid; + display: grid; grid-template-rows: min-content min-content; } diff --git a/sass/options.scss b/sass/options.scss new file mode 100644 index 000000000..55f545523 --- /dev/null +++ b/sass/options.scss @@ -0,0 +1,69 @@ +@import "ui"; + +* { + box-sizing: border-box; +} + +html, +body { + margin: 0; +} + +body { + font-family: var(--font-sans); +} + +#options { + min-height: 100vh; + background: var(--canvas); + color: var(--text); + padding: 48px 24px; + + .options-card { + max-width: 480px; + margin: 0 auto; + background: var(--app-bg); + border: var(--border) 1px solid; + border-radius: var(--radius-card); + box-shadow: var(--shadow); + padding: 32px 28px; + } + + h2 { + font-size: 20px; + font-weight: 800; + letter-spacing: -0.01em; + margin: 0 0 10px; + } + + p { + font-size: 14px; + line-height: 1.55; + color: var(--text-dim); + margin: 0 0 16px; + } + + label { + font-size: 14px; + color: var(--text); + } + + button { + margin-top: 18px; + padding: 13px 18px; + border: none; + border-radius: var(--radius-row); + background: var(--danger); + color: #fff; + font-family: var(--font-sans); + font-size: 15px; + font-weight: 700; + cursor: pointer; + + &:disabled { + background: var(--row); + color: var(--text-faint); + cursor: default; + } + } +} diff --git a/sass/permissions.scss b/sass/permissions.scss index 0bef0c1bf..789d8c5ed 100644 --- a/sass/permissions.scss +++ b/sass/permissions.scss @@ -1,43 +1,139 @@ @import "ui"; -[v-cloak] { - display: none; +* { + box-sizing: border-box; } -* { - font-family: arial, "Microsoft YaHei"; +html, +body { + margin: 0; } -p { - font-size: 16px; +body { + font-family: var(--font-sans); +} + +[v-cloak] { + display: none; } #permissions { - width: 900px; - position: relative; + min-height: 100vh; + background: var(--canvas); + color: var(--text); + padding: 48px 24px; +} + +.perm-card { + max-width: 560px; margin: 0 auto; + background: var(--app-bg); + border: 1px solid var(--border); + border-radius: var(--radius-card); + box-shadow: var(--shadow); + padding: 32px 28px; } -h2 { - margin-top: 3em; +.perm-head { + display: flex; + align-items: center; + gap: 14px; + margin-bottom: 22px; + + .perm-icon { + width: 52px; + height: 52px; + border-radius: 16px; + flex: none; + background: var(--accent-soft); + color: var(--accent); + display: flex; + align-items: center; + justify-content: center; + + svg { + width: 26px; + height: 26px; + } + } + + h1 { + font-size: 22px; + font-weight: 800; + letter-spacing: -0.02em; + margin: 0; + } } -button { - display: inline-grid; - padding: 10px 20px; - border: #ccc 1px solid; - background: white; - border-radius: 2px; - position: relative; - text-align: center; +.perm-toggle { + display: flex; align-items: center; - font-size: 16px; - color: gray; + gap: 9px; + font-size: 13.5px; + color: var(--text-dim); + margin-bottom: 18px; cursor: pointer; - outline: none; - margin-left: 0px !important; - &:not(:disabled):hover { - color: black; + input { + accent-color: var(--accent); + } +} + +.perm-list { + display: flex; + flex-direction: column; + gap: 10px; +} + +.perm-item { + display: flex; + align-items: center; + gap: 14px; + padding: 14px 16px; + border-radius: var(--radius-row); + background: var(--row); + border: 1px solid var(--border); + + .perm-item-text { + flex: 1; + min-width: 0; + } + + .perm-item-id { + font-size: 15px; + font-weight: 700; + color: var(--text); + margin-bottom: 3px; + } + + p { + font-size: 13px; + line-height: 1.5; + color: var(--text-dim); + margin: 0; + } + + .perm-required { + color: var(--text-faint); + font-style: italic; + margin-top: 4px; + } + + button { + flex: none; + padding: 9px 16px; + border: 1px solid var(--border-strong); + border-radius: var(--radius-row); + background: var(--app-bg); + color: var(--text); + font-family: var(--font-sans); + font-size: 13px; + font-weight: 600; + cursor: pointer; + + &:hover { + border-color: var(--danger); + color: var(--danger); + } } } diff --git a/sass/popup.scss b/sass/popup.scss index 6849dd8c3..2e7f1f69e 100644 --- a/sass/popup.scss +++ b/sass/popup.scss @@ -1,6 +1,9 @@ @import "ui"; -// Structure +// Layout constants +$header-h: 56px; +$popup-w: 360px; +$popup-h: 580px; * { margin: 0; @@ -13,12 +16,14 @@ } body { - width: 320px; - height: 480px; + width: $popup-w; + height: $popup-h; transform-origin: left top; overflow: hidden; - font-family: arial, "Microsoft YaHei"; + font-family: var(--font-sans); font-size: 16px; + color: var(--text); + background: var(--app-bg); cursor: default; user-select: none; @@ -36,13 +41,12 @@ svg { } .icon { - @include themify($themes) { - fill: themed("grey-1"); - } + fill: var(--text-dim); vertical-align: middle; + svg { - height: 16px; - width: 16px; + height: 17px; + width: 17px; } } @@ -51,210 +55,288 @@ svg { top: -1000px; } -// Header +// ── Header ──────────────────────────────────────────────────────────── .header { - height: 38px; - line-height: 38px; + height: $header-h; + display: flex; + align-items: center; + gap: 11px; + padding: 0 16px; + background: var(--header); + border-bottom: 1px solid var(--border); position: relative; - text-align: center; - font-size: 16px; - @include themify($themes) { - color: themed("black-1"); - background: themed("white-1"); - border-bottom: themed("grey-2") 1px solid; + + .brand { + display: flex; + align-items: center; + gap: 10px; + flex: 1; + min-width: 0; } - .icon { - @include hover-black; - cursor: pointer; + .brand-logo { + width: 28px; + height: 28px; + border-radius: 8px; + background: var(--accent); + color: var(--accent-fg); + display: flex; + align-items: center; + justify-content: center; + flex: none; svg { - vertical-align: sub; + width: 20px; + height: 20px; } } -} -#i-menu { - position: absolute; - left: 20px; - bottom: 0; -} + .brand-name { + font-size: 16px; + font-weight: 700; + letter-spacing: -0.01em; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } -#i-lock { - position: absolute; - left: 45px; - bottom: 0; -} + // MenuPage reuses .header with a title + back button + #menuName { + font-size: 16px; + font-weight: 700; + flex: 1; + } -#i-sync { - position: absolute; - left: 70px; - bottom: 0; - cursor: default; - @include themify($themes) { - fill: themed("grey-2"); + .header-actions { + display: flex; + align-items: center; + gap: 7px; + flex: none; } - &:hover { - svg { - fill: inherit; + .icon { + width: 34px; + height: 34px; + border-radius: 10px; + background: var(--row); + color: var(--text-dim); + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: background 0.15s, color 0.15s; + + &:hover { + background: var(--row-hover); + svg { + fill: var(--text); + } } } -} -#i-plus, -#i-qr { - position: absolute; - right: 45px; - bottom: 0; -} + #i-plus { + background: var(--accent); + svg { + fill: var(--accent-fg); + } + &:hover { + filter: brightness(1.06); + svg { + fill: var(--accent-fg); + } + } + } -#i-edit { - position: absolute; - right: 20px; - bottom: 0; -} + #i-sync { + background: transparent; + cursor: default; + svg { + fill: var(--text-faint); + } + &:hover { + background: transparent; + } + } -#i-close { - position: absolute; - left: 20px; - bottom: 0; + #i-close { + background: transparent; + &:hover { + background: var(--row); + } + } } -// Search & Filter +// ── Search & filter (under header) ────────────────────────────────────── .under-header { - padding: 0 10px; - margin-left: 10px; - font-size: 12px; - height: 24px; - line-height: 24px; - cursor: pointer; display: none; } #filter { - @include themify($themes) { - background: themed("yellow-2"); - } + display: flex; + align-items: center; + gap: 9px; + padding: 11px 14px; + border-radius: 12px; + background: var(--accent-soft); + cursor: pointer; &:hover { - @include themify($themes) { - background: themed("yellow-1"); - } + filter: brightness(0.98); + } + + .filter-globe { + width: 15px; + height: 15px; + flex: none; + color: var(--accent); + } + + .filter-text { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + color: var(--accent); + } + + .filter-label { + font-size: 12.5px; + font-weight: 600; + } + + .filter-domain { + font-size: 13px; + font-weight: 700; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .filter-showall { + flex: none; + font-size: 12px; + font-weight: 700; + color: var(--accent); + text-decoration: underline; } } #search { position: relative; - @include themify($themes) { - background: themed("white-1"); - border: themed("grey-2") 1px solid; - border-top: none; - } + display: flex; + align-items: center; + gap: 9px; + padding: 10px 13px; + border-radius: 12px; + background: var(--row); + border: 1px solid var(--border); - &:hover { - @include themify($themes) { - background: themed("white-search"); - } + &:focus-within { + border-color: var(--accent); + background: var(--app-bg); } input { - font-family: arial, "Microsoft YaHei"; + flex: 1; + min-width: 0; + font-family: var(--font-sans); + font-size: 14px; background: none; border: none; - width: 100%; - height: 100%; - @include themify($themes) { - color: themed("black-search"); - } + outline: none; + color: var(--text); } #searchHint { - position: absolute; - top: -1px; - right: 0px; - padding-right: 10px; - display: grid; - grid-template-rows: 4.5px 15px 4.5px; - grid-template-columns: 15px; + flex: none; + color: var(--text-faint); } #searchHintBorder { - @include themify($themes) { - color: themed("white-1"); - background: themed("grey-search"); - } - text-align: center; - border-radius: 1.5px; + min-width: 16px; + padding: 0 5px; + border-radius: 4px; + background: var(--border); + color: var(--text-faint); + font-size: 11px; line-height: 16px; - font-weight: bolder; + text-align: center; + font-weight: 700; } } -// Codes +// ── Code list ─────────────────────────────────────────────────────────── #codes { - height: 442px; + height: $popup-h - $header-h; overflow-x: hidden; - overflow-y: hidden; - @include themify($themes) { - background: themed("grey-background"); - } - padding-right: 10px; + overflow-y: auto; + background: var(--app-bg); + padding: 10px 12px; + display: flex; + flex-direction: column; + gap: 8px; .deleteAction { - @include themify($themes) { - @include icon-special(20px, themed("red-1")); + flex: none; + width: 24px; + height: 24px; + border-radius: var(--radius-pill); + display: none; // shown in edit mode + align-items: center; + justify-content: center; + fill: var(--danger); + cursor: pointer; + + &:hover { + background: var(--app-bg); } - position: absolute; - top: -10px; - left: -10px; - z-index: 10; - display: none; - } - &:hover { - padding-right: 0; - overflow-y: scroll; + svg { + width: 20px; + height: 20px; + } } &.edit { - .code { - @include themify($themes) { - color: themed("grey-2") !important; - } - user-select: none; - cursor: default; - } - .issuer, - .showqr, - .showqr.hidden, - .pin, + .account, + .code, + .entry-actions, + .timebar, + .remaining-badge, .no-entry { display: none; } - .issuerEdit, - .movehandle, - #add { + .issuerEdit { display: block; } + .movehandle, .deleteAction { - display: block; - cursor: pointer; + display: flex; + } + + .entry { + cursor: default; + padding: 10px 11px; + } + + .monogram { + width: 34px; + height: 34px; + font-size: 15px; + border-radius: 10px; } - .sector, - .counter { - position: absolute; - left: -1000px; - opacity: 0; + .entry-text { + display: flex; + flex-direction: column; + gap: 4px; } } - &.filter .entry[filtered], - &.search .entry[notSearched] { + &.search .entry.notSearched { height: 0; margin: 0; padding: 0; @@ -264,218 +346,204 @@ svg { position: absolute; } - &.filter #filter, &.search #search { - display: block; + display: flex; } +} - &:not(.edit) { - // Is this used? - .entry[unencrypted="true"]:hover .warning { - height: 24px; - } - - .code.timeout:not(.hotp) { - animation: twinkling 1s infinite ease-in-out; - } - } +// vuedraggable wraps the rows in its own element, so the gap must live here +// (a gap on #codes only spaces the search/filter/list groups, not the rows). +.entries { + display: flex; + flex-direction: column; + gap: 8px; +} - .no-entry { - @include themify($themes) { - color: themed("grey-1"); - } +// ── Entry row ─────────────────────────────────────────────────────────── +.entry { + display: flex; + align-items: center; + gap: 12px; + padding: 11px 13px 14px; + border-radius: var(--radius-row); + background: var(--row); + border: 1px solid transparent; + position: relative; + overflow: hidden; + cursor: pointer; + transition: background 0.15s, border-color 0.15s; - margin: 20px; - text-align: center; + &:hover { + background: var(--row-hover); + border-color: var(--border); - svg { - width: 48px; - height: 48px; - margin: 20px; + .movehandle svg { + fill: var(--text); } } -} -.entry { - margin: 10px; - margin-right: 0; - padding: 10px; - @include themify($themes) { - border: themed("grey-2") 1px solid; - background: themed("white-1"); - } - border-radius: 2px; - position: relative; - display: block; - cursor: pointer; + .monogram { + width: 38px; + height: 38px; + border-radius: 11px; + flex: none; + display: flex; + align-items: center; + justify-content: center; + font-weight: 800; + font-size: 17px; + } + + .entry-text { + flex: 1; + min-width: 0; + } .issuer { - font-size: 12px; - @include themify($themes) { - color: themed("black-1"); - } - width: 80%; + font-size: 14px; + font-weight: 700; + color: var(--text); + white-space: nowrap; + overflow: hidden; text-overflow: ellipsis; + } + + .account { + font-size: 12px; + color: var(--text-faint); + white-space: nowrap; overflow: hidden; + text-overflow: ellipsis; } .code { - font-size: 36px; - @include themify($themes) { - color: themed("blue-1"); - } - width: 80%; + font-family: var(--font-mono); + font-size: 21px; + font-weight: 600; + letter-spacing: 0.5px; + color: var(--accent); + font-variant-numeric: tabular-nums; user-select: text; - font-family: "Droid Sans Mono"; - } + white-space: nowrap; - .sector, - .counter { - width: 20px; - height: 20px; - position: absolute; - right: 10px; - bottom: 10px; + &.timeout:not(.hotp) { + animation: glint 1s infinite ease-in-out; + } } - .sector { - svg { - width: 16px; - height: 16px; - margin: 2px; - } + .issuerEdit { + display: none; // shown in edit mode - circle { - fill: none; - transform: rotate(-90deg); - transform-origin: 50% 50%; - @include themify($themes) { - stroke: themed("grey-1"); + input { + width: 100%; + padding: 6px 8px; + border-radius: 8px; + border: 1px solid var(--border); + background: var(--app-bg); + font-family: var(--font-sans); + font-size: 13px; + color: var(--text); + outline: none; + + &:focus { + border-color: var(--accent); } - stroke-width: 8px; - stroke-dasharray: 25.12; - animation-name: timer; - animation-iteration-count: infinite; - animation-timing-function: linear; } + + &.issuerEdit-issuer input { + font-weight: 700; + } + + &.issuerEdit-account input { + font-size: 12px; + color: var(--text-dim); + } + } + + // Right-side actions (QR / HOTP refresh always visible; pin on hover) + .entry-actions { + display: flex; + align-items: center; + gap: 4px; + flex: none; } .counter { - @include themify($themes) { - @include icon-special(18px, themed("grey-1")); - } - text-align: center; + width: 32px; + height: 32px; + border-radius: 9px; + flex: none; + display: flex; + align-items: center; + justify-content: center; cursor: pointer; + fill: var(--text-faint); + background: transparent; - .disabled { - svg { - @include themify($themes) { - fill: themed("grey-2"); - } - } - cursor: default; + svg { + width: 17px; + height: 17px; } - &:not(.disabled):hover { + &:hover { + background: var(--app-bg); svg { - @include themify($themes) { - fill: themed("black-1"); - } + fill: var(--accent); } } } - .issuerEdit { - display: none; - - input { - border: none; - height: 14px; - width: 70%; - font-size: 12px; - outline: none; - @include themify($themes) { - background: themed("grey-3"); - } + .counter.disabled { + cursor: default; + svg { + fill: var(--border-strong); } } - .movehandle { - @include themify($themes) { - @include icon-special(24px, themed("grey-2")); - } - height: 98px; - line-height: 98px; - right: 10px; - top: 0; + // Countdown: depleting bar along the bottom edge + seconds badge + .timebar { position: absolute; - cursor: move; - display: none; + left: 0; + right: 0; + bottom: 0; + height: 3px; + background: var(--ring-track); } - .showqr, - .pin { - @include themify($themes) { - @include icon-special(20px, themed("grey-2")); - } - @include hover-black; - right: 10px; - top: 10px; - position: absolute; - cursor: pointer; - opacity: 0; + .timebar-fill { + height: 100%; + border-radius: 0 3px 3px 0; + transition: width 1s linear, background 0.4s ease; } - .showqr { - right: 35px; + .remaining-badge { + position: absolute; + right: 13px; + bottom: 8px; + font-family: var(--font-mono); + font-size: 9.5px; + font-weight: 600; + color: var(--text-faint); + font-variant-numeric: tabular-nums; + line-height: 1; } - &:hover { - .showqr, - .pin { - opacity: 1; - } + .movehandle { + flex: none; + display: none; // shown in edit mode + align-items: center; + padding: 4px; + fill: var(--text-faint); + cursor: grab; - .movehandle { - svg { - @include themify($themes) { - fill: themed("black-1"); - } - } + svg { + width: 18px; + height: 18px; } } - /* - // Is this used? - &[dropOver="true"] { - border: $grey-1 1px dashed; - } - - // Is this used? - &[unencrypted="true"] .warning { - position: absolute; - height: 0; - line-height: 12px; - font-size: 12px; - padding: 0 10px; - margin: 0 4px; - width: 250px; - bottom: 4px; - left: 0; - background: #EC6959; - color: $white; - cursor: pointer; - overflow: hidden; - border-radius: 2px; - transition: height 0.2s; - } - */ } .pinnedEntry { - .pin { - opacity: 1; - } - .movehandle { display: none !important; } @@ -485,100 +553,207 @@ svg { cursor: default; } -#add { - @extend .button; - @include hover-black; - margin-right: 0; - line-height: 56px; - display: none; +// Smart-filter view (design "12 FILTERED TO SITE") +.entry.matchedEntry { + border-color: var(--accent); } -// Modals -#notification { - position: absolute; - left: 60px; - top: -1000px; - width: 200px; - height: 60px; - line-height: 60px; - text-align: center; - @include themify($themes) { - background: themed("black-transparent"); - color: themed("white-1"); - } - font-size: 20px; - border-radius: 2px; - - &.fadein { - top: 190px; - animation: fadeshow 0.2s 1 ease-out; - } +.entry.dimmed { + opacity: 0.5; +} - &.fadeout { - top: 190px; - animation: fadehide 0.2s 1 ease-in; - } +.other-accounts { + font-size: 11px; + font-weight: 700; + color: var(--text-faint); + letter-spacing: 0.06em; + text-transform: uppercase; + padding: 14px 4px 6px; } -.message-box { - position: absolute; - width: 300px; - padding: 10px; - @include themify($themes) { - color: themed("black-1"); - border: themed("grey-1") 1px solid; - background: themed("white-1"); - box-shadow: 1px 1px 3px themed("grey-1"); - } - border-radius: 2px; - left: 10px; - top: 150px; - z-index: 1000; +// Right-click context menu on an entry +.entry-ctx-backdrop { + position: fixed; + inset: 0; + z-index: 1150; +} - .button-small { - margin-top: 10px; - margin-bottom: 10px; - } +.entry-ctx { + position: fixed; + z-index: 1200; + min-width: 160px; + padding: 6px; + background: var(--app-bg); + border: 1px solid var(--border); + border-radius: 12px; + box-shadow: var(--shadow); + cursor: default; } -.buttons { +.entry-ctx-item { display: flex; - justify-content: center; - text-align: center; + align-items: center; + gap: 10px; + padding: 9px 11px; + border-radius: 8px; + font-size: 13.5px; + font-weight: 600; + color: var(--text); + fill: var(--text-dim); + cursor: pointer; - .button-small { - display: inline-block; - flex-basis: content; - margin: 10px; - padding: 5px 20px; + svg { + width: 17px; + height: 17px; } -} -#qr { - width: 320px; - height: 480px; - top: -1000px; - left: 0; + &:hover { + background: var(--row); + } +} + +// Edit-mode "Add account" row (design "10 EDIT MODE") +.edit-add { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 14px; + border-radius: 14px; + border: 2px dashed var(--border-strong); + color: var(--text-dim); + fill: none; + stroke: currentColor; + font-size: 13.5px; + font-weight: 700; + cursor: pointer; + + svg { + width: 17px; + height: 17px; + } + + &:hover { + border-color: var(--accent); + color: var(--accent); + } +} + +// ── Empty state ───────────────────────────────────────────────────────── +.no-entry { + margin: auto; + text-align: center; + display: flex; + flex-direction: column; + align-items: center; + padding: 30px 24px; + color: var(--text-dim); + + .no-entry-icon { + width: 64px; + height: 64px; + border-radius: 18px; + border: 2px dashed var(--border-strong); + color: var(--text-faint); + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 18px; + + svg { + width: 28px; + height: 28px; + } + } + + .no-entry-title { + font-size: 17px; + font-weight: 700; + color: var(--text); + margin-bottom: 6px; + } + + p { + font-size: 13px; + margin-bottom: 20px; + } + + .no-entry-add { + padding: 13px 22px; + border: none; + border-radius: 13px; + background: var(--accent); + color: var(--accent-fg); + font-family: var(--font-sans); + font-size: 14px; + font-weight: 700; + cursor: pointer; + + &:hover { + filter: brightness(1.06); + } + } +} + +// ── Copy toast ────────────────────────────────────────────────────────── +#notification { position: absolute; - z-index: 10; - @include themify($themes) { - background-color: themed("white-transparent"); + left: 50%; + bottom: 20px; + transform: translateX(-50%); + max-width: calc(100% - 40px); + padding: 11px 18px; + border-radius: var(--radius-pill); + background: var(--text); + color: var(--app-bg); + font-size: 13px; + font-weight: 700; + box-shadow: 0 8px 22px rgba(0, 0, 0, 0.22); + white-space: nowrap; + opacity: 0; + pointer-events: none; + z-index: 1100; + + &.fadein { + opacity: 1; + animation: toastUp 0.22s ease; } - background-repeat: no-repeat; - background-position: center; - canvas { - display: none; + &.fadeout { + opacity: 0; + animation: fadehide 0.2s ease; } +} - &.qrfadein { - top: 0; - animation: fadeshow 0.2s 1 ease-out; +// ── Alert / confirm dialog ────────────────────────────────────────────── +.message-box { + position: absolute; + width: calc(100% - 36px); + left: 18px; + top: 150px; + padding: 20px; + border-radius: var(--radius-card); + color: var(--text); + background: var(--app-bg); + border: 1px solid var(--border); + box-shadow: var(--shadow); + z-index: 1000; + + .button-small { + margin: 14px auto 0; } +} - &.qrfadeout { - top: 0; - animation: fadehide 0.2s 1 ease-in; +.buttons { + display: flex; + justify-content: center; + gap: 10px; + + .button-small { + margin: 14px 0 0; + padding: 9px 22px; + flex: 0 0 auto; + width: auto; } } @@ -588,125 +763,278 @@ svg { height: 100%; top: 0; left: 0; + background: var(--overlay); z-index: 900; } -// Info -#info { +// ── QR overlay ────────────────────────────────────────────────────────── +// QR bottom sheet (design "04 LIVE LIST · interactive") +#qr { position: absolute; - height: 460px; - width: 300px; - padding: 10px; - @include themify($themes) { - color: themed("black-1"); - border: themed("grey-1"); - background: themed("white-1"); - box-shadow: 1px 1px 3px themed("grey-1"); + width: $popup-w; + height: $popup-h; + top: -1000px; + left: 0; + z-index: 10; + background: rgba(10, 10, 12, 0.45); + display: flex; + align-items: flex-end; + + &.qrfadein { + top: 0; + animation: fadeshow 0.18s 1 ease-out; + } + + &.qrfadeout { + top: 0; + animation: fadehide 0.18s 1 ease-in; + } + + .qr-sheet { + width: 100%; + padding: 24px 22px 26px; + background: var(--app-bg); + border-radius: 22px; + box-shadow: 0 -10px 40px rgba(0, 0, 0, 0.25); + display: flex; + flex-direction: column; + align-items: center; + animation: sheetUp 0.22s ease; } + + .qr-head { + display: flex; + align-items: center; + gap: 11px; + width: 100%; + margin-bottom: 20px; + } + + .qr-mono { + width: 38px; + height: 38px; + border-radius: 11px; + flex: none; + display: flex; + align-items: center; + justify-content: center; + font-weight: 800; + font-size: 17px; + } + + .qr-headtext { + flex: 1; + min-width: 0; + } + + .qr-issuer { + font-size: 15px; + font-weight: 700; + color: var(--text); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .qr-account { + font-size: 12px; + color: var(--text-faint); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .qr-close { + width: 32px; + height: 32px; + border-radius: 9px; + flex: none; + border: none; + background: var(--row); + color: var(--text-dim); + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + + svg { + width: 16px; + height: 16px; + } + + &:hover { + background: var(--row-hover); + color: var(--text); + } + } + + .qr-img { + padding: 16px; + background: #fff; + border-radius: 18px; + box-shadow: 0 4px 18px rgba(0, 0, 0, 0.1); + margin-bottom: 16px; + + img { + display: block; + width: 180px; + height: 180px; + image-rendering: pixelated; + } + } + + .qr-caption-title { + font-size: 13px; + font-weight: 700; + color: var(--text); + margin-bottom: 4px; + } + + .qr-caption { + font-size: 12.5px; + line-height: 1.5; + color: var(--text-dim); + text-align: center; + max-width: 250px; + } +} + +// ── Modal pane (#info) ────────────────────────────────────────────────── +#info { + position: absolute; + height: $popup-h - 20px; + width: calc(100% - 20px); left: 10px; top: -1000px; + padding: 0; + border-radius: var(--radius-card); + color: var(--text); + background: var(--app-bg); + border: 1px solid var(--border); + box-shadow: var(--shadow); z-index: 100; + overflow: hidden; #infoContent { - height: 420px; + height: 100%; + padding: 18px; overflow-y: auto; overflow-x: hidden; p { - margin-bottom: 20px; + margin-bottom: 16px; } } #infoClose { - @include hover-black; - @include themify($themes) { - @include icon-special(14px, themed("grey-1")); - } - height: 20px; - width: 20px; + position: absolute; + top: 14px; + right: 14px; + z-index: 2; + width: 28px; + height: 28px; + border-radius: 9px; + display: flex; + align-items: center; + justify-content: center; cursor: pointer; + fill: var(--text-dim); + + &:hover { + background: var(--row); + svg { + fill: var(--text); + } + } + + svg { + width: 16px; + height: 16px; + } } label { display: block; - margin: 10px 0 0 10px; + margin: 10px 0 0 4px; + font-size: 13px; } .control-group { display: flex; justify-content: space-between; align-items: center; + padding: 4px 0; } .control-group select { - width: 100px; - padding: 5px 10px; - @include themify($themes) { - border: themed("grey-2") 1px solid; - background: themed("white-1"); - color: themed("black-1"); - } + width: 130px; + padding: 9px 12px; + border-radius: var(--radius-row); + border: 1px solid var(--border-strong); + background: var(--row); + color: var(--text); + font-family: var(--font-sans); } + // Pill toggle .control-group input[type="checkbox"] { position: relative; border: none; background: none; - width: 34px; - height: 16px; + width: 46px; + height: 27px; -webkit-appearance: none; -moz-appearance: none; appearance: none; outline: none; + cursor: pointer; } .control-group input[type="checkbox"]::before { content: ""; display: block; - width: 28px; - height: 12px; - border-radius: 8px; - background: grey; - margin: 2px 3px; - transition: all 80ms linear; + width: 46px; + height: 27px; + border-radius: var(--radius-pill); + background: var(--border-strong); + transition: background 0.2s linear; } .control-group input[type="checkbox"]::after { content: ""; display: block; position: absolute; - width: 16px; - height: 16px; - border-radius: 10px; - background: white; - top: 0; - left: 0; - box-shadow: 0 0 2px black; - transition: all 80ms linear; + width: 23px; + height: 23px; + border-radius: var(--radius-pill); + background: #fff; + top: 2px; + left: 2px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); + transition: transform 0.2s linear; } .control-group input[type="checkbox"]:checked::before { - background: #08c; + background: var(--accent); } .control-group input[type="checkbox"]:checked::after { - transform: matrix(1, 0, 0, 1, 18, 0); + transform: translateX(19px); } .combo-label { display: flex; - margin: 20px; - margin-right: 0px; - font-size: 16px; + align-items: center; + margin: 16px 4px; + font-size: 14px; } .checkbox { - margin: 20px; + margin: 16px 4px; } select { - margin: 20px; - font-size: 12px; + font-size: 13px; } a { @@ -719,13 +1047,11 @@ svg { .text { display: block; - margin: 10px 0 0 10px; + margin: 10px 0 0 4px; } .warning { - @include themify($themes) { - color: themed("red-1"); - } + color: var(--danger); } // Advisor @@ -738,17 +1064,14 @@ svg { } .no-insight { - @include themify($themes) { - color: themed("grey-1"); - } - + color: var(--text-dim); margin: 20px; } .insight { font-size: 14px; - border-left: 5px solid; - padding-left: 5px; + border-left: 4px solid; + padding-left: 10px; margin: 20px 5px 20px 0; h3 { @@ -764,78 +1087,786 @@ svg { font-size: 12px; a:not(:first-child) { - @include themify($themes) { - border-left: 1px solid themed("grey-1"); - } + border-left: 1px solid var(--border-strong); margin-left: 10px; padding-left: 10px; } } } - [level="danger"] { - border-left-color: firebrick; + [level="danger"] { + border-left-color: var(--danger); + } + + [level="warning"] { + border-left-color: var(--warn); + } + + [level="info"] { + border-left-color: var(--accent); + } + } + + // Security + @mixin security-button { + @extend .button-small; + font-size: 13px; + margin: 16px 8px; + padding: 11px; + display: inline-block; + width: auto; + } + + #security-save { + @include security-button; + } + + #security-remove { + @include security-button; + } + + .badInput input { + border-color: var(--danger); + } + + &.fadein { + top: 10px; + animation: fadein 0.2s 1 ease-out; + } + + &.fadeout { + top: 110px; + animation: fadeout 0.2s 1 ease-in; + } + + &.show { + top: 10px; + } +} + +// ── Add-account pages (rendered inside #info) ─────────────────────────── +.page-title { + font-size: 18px; + font-weight: 800; + letter-spacing: -0.01em; + margin: 2px 0 16px; + color: var(--text); +} + +// Settings page (PreferencesPage) — matches design "08 OPTIONS" +.settings-page { + .settings-section-title { + font-size: 11px; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--text-faint); + margin: 18px 2px 8px; + } + + .settings-row { + display: flex; + align-items: center; + gap: 12px; + padding: 13px 14px; + margin-bottom: 8px; + border-radius: 13px; + background: var(--row); + + &.clickable { + cursor: pointer; + + &:hover { + background: var(--row-hover); + } + } + + &.disabled { + opacity: 0.5; + } + } + + .settings-row-text { + flex: 1; + min-width: 0; + } + + .settings-row-title { + font-size: 14px; + font-weight: 700; + color: var(--text); + } + + .settings-row-sub { + font-size: 12px; + color: var(--text-faint); + margin-top: 2px; + } + + .settings-select, + .settings-num { + flex: none; + padding: 8px 10px; + border-radius: 10px; + border: 1px solid var(--border-strong); + background: var(--app-bg); + color: var(--text); + font-family: var(--font-sans); + font-size: 13px; + outline: none; + + &:focus { + border-color: var(--accent); + } + } + + .settings-select { + cursor: pointer; + } + + .settings-num { + width: 64px; + text-align: center; + } + + .settings-action { + width: 100%; + margin-top: 16px; + padding: 13px; + border: 1px solid var(--border-strong); + border-radius: var(--radius-row); + background: var(--app-bg); + color: var(--text); + font-family: var(--font-sans); + font-size: 14px; + font-weight: 600; + cursor: pointer; + + &:hover { + background: var(--row); + } + } +} + +// Backup page (design "16 BACKUP") +.backup-page { + .backup-section-title { + font-size: 11px; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--text-faint); + margin: 18px 2px 8px; + } + + .backup-warning { + padding: 13px; + border-radius: 13px; + background: var(--warn-soft); + color: var(--text-dim); + font-size: 12px; + line-height: 1.5; + margin-bottom: 8px; + } + + .backup-list { + display: flex; + flex-direction: column; + gap: 8px; + } + + .backup-row { + display: flex; + align-items: center; + gap: 12px; + padding: 13px 14px; + border-radius: 13px; + background: var(--row); + color: var(--text); + text-decoration: none; + cursor: pointer; + + &:hover { + background: var(--row-hover); + } + } + + .backup-row.disabled { + cursor: default; + opacity: 0.55; + + &:hover { + background: var(--row); + } + + .backup-chevron { + display: none; + } + } + + .backup-ico { + width: 18px; + height: 18px; + flex: none; + color: var(--text-dim); + fill: none; + } + + .cloud-chip { + width: 30px; + height: 30px; + border-radius: 9px; + flex: none; + display: flex; + align-items: center; + justify-content: center; + + svg { + width: 17px; + height: 17px; + } + } + + .backup-row-text { + flex: 1; + min-width: 0; + } + + .backup-row-title { + font-size: 14px; + font-weight: 700; + color: var(--text); + } + + .backup-row-sub { + font-size: 12px; + color: var(--text-faint); + + &.connected { + color: var(--ok); + } + } + + .backup-chevron { + width: 18px; + height: 18px; + flex: none; + color: var(--text-faint); + fill: none; + } +} + +// Pill toggle +.pill { + flex: none; + width: 46px; + height: 27px; + border-radius: var(--radius-pill); + background: var(--border-strong); + position: relative; + transition: background 0.2s; + + .pill-knob { + position: absolute; + top: 2px; + left: 2px; + width: 23px; + height: 23px; + border-radius: var(--radius-pill); + background: #fff; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); + transition: transform 0.2s; + } + + &.on { + background: var(--accent); + + .pill-knob { + transform: translateX(19px); + } + } +} + +.method-card { + display: flex; + align-items: center; + gap: 14px; + padding: 15px; + margin-bottom: 11px; + border-radius: 16px; + background: var(--row); + border: 1px solid var(--border); + cursor: pointer; + + &:hover { + background: var(--row-hover); + } + + .method-ico { + width: 44px; + height: 44px; + border-radius: 13px; + flex: none; + display: flex; + align-items: center; + justify-content: center; + background: var(--app-bg); + border: 1px solid var(--border); + color: var(--text-dim); + + svg { + width: 22px; + height: 22px; + } + } + + .method-text { + flex: 1; + font-size: 15px; + font-weight: 700; + color: var(--text); + } + + .method-chevron { + width: 18px; + height: 18px; + color: var(--text-faint); + flex: none; + } +} + +.method-card--primary { + background: var(--accent-soft); + border-color: var(--accent); + + .method-ico { + background: var(--accent); + border-color: var(--accent); + color: var(--accent-fg); + } + + .method-chevron { + color: var(--accent); + } +} + +// Manual entry form (design "07 MANUAL ENTRY") +// Scoped under #info so its label/input rules beat the generic `#info label`. +#info .manual-form { + height: 100%; + display: flex; + flex-direction: column; + gap: 16px; + + .field-label { + display: block; + font-size: 12px; + font-weight: 700; + color: var(--text-dim); + letter-spacing: 0.02em; + margin-bottom: 7px; + } + + .field-input { + width: 100%; + padding: 13px; + border-radius: 12px; + border: 1px solid var(--border-strong); + background: var(--row); + color: var(--text); + font-family: var(--font-sans); + font-size: 14px; + outline: none; + + &:focus { + border-color: var(--accent); + background: var(--app-bg); + } + + &.mono { + font-family: var(--font-mono); + letter-spacing: 1px; + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } + } + + select.field-input { + cursor: pointer; + } + + .advanced summary { + font-size: 13px; + font-weight: 700; + color: var(--text-dim); + cursor: pointer; + padding: 4px 0; + } + + .advanced[open] summary { + margin-bottom: 12px; + } + + .advanced .field + .field { + margin-top: 12px; + } + + .add-submit { + margin-top: auto; + width: 100%; + padding: 15px; + border: none; + border-radius: 14px; + background: var(--accent); + color: var(--accent-fg); + font-family: var(--font-sans); + font-size: 15px; + font-weight: 700; + cursor: pointer; + + &:hover { + filter: brightness(1.06); + } + } +} + +// ── Cloud provider connected page (design "17b DROPBOX CONNECTED") ─────── +#info .cloud-page { + height: 100%; + display: flex; + flex-direction: column; + gap: 16px; + + .cloud-head { + display: flex; + align-items: center; + gap: 12px; + } + + .cloud-title { + flex: 1; + font-size: 18px; + font-weight: 800; + letter-spacing: -0.01em; + } + + .cloud-brand { + width: 30px; + height: 30px; + border-radius: 9px; + flex: none; + background: oklch(0.6 0.16 245); + display: flex; + align-items: center; + justify-content: center; + + svg { + width: 17px; + height: 17px; + } + } + + .cloud-account { + display: flex; + align-items: center; + gap: 13px; + padding: 15px; + border-radius: 15px; + background: var(--row); + border: 1px solid var(--border); + } + + .cloud-avatar { + width: 44px; + height: 44px; + border-radius: var(--radius-pill); + flex: none; + background: oklch(0.86 0.07 245); + color: oklch(0.42 0.16 245); + display: flex; + align-items: center; + justify-content: center; + font-weight: 800; + font-size: 18px; + } + + .cloud-account-text { + flex: 1; + min-width: 0; + } + + .cloud-email { + font-size: 14px; + font-weight: 700; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .cloud-status { + display: flex; + align-items: center; + gap: 5px; + font-size: 12px; + font-weight: 600; + color: var(--ok); + + .cloud-dot { + width: 7px; + height: 7px; + border-radius: var(--radius-pill); + background: var(--ok); + } + } + + .field-label { + display: block; + font-size: 12px; + font-weight: 700; + color: var(--text-dim); + margin-bottom: 7px; + } + + .cloud-select-wrap { + position: relative; + } + + .cloud-select { + width: 100%; + padding: 13px 36px 13px 13px; + border-radius: 12px; + border: 1px solid var(--border-strong); + background: var(--row); + color: var(--text); + font-family: var(--font-sans); + font-size: 14px; + font-weight: 600; + outline: none; + appearance: none; + cursor: pointer; + + &:focus { + border-color: var(--accent); + } + } + + .cloud-select-chevron { + position: absolute; + right: 13px; + top: 50%; + transform: translateY(-50%); + width: 14px; + height: 14px; + color: var(--text-faint); + pointer-events: none; + } + + .cloud-warning { + display: flex; + gap: 10px; + padding: 13px; + border-radius: 13px; + background: var(--warn-soft); + color: var(--text-dim); + + svg { + flex: none; + width: 17px; + height: 17px; + stroke: var(--warn); + margin-top: 1px; + } + + div { + font-size: 12px; + line-height: 1.5; + } + } + + .cloud-actions { + margin-top: auto; + display: flex; + flex-direction: column; + gap: 9px; + } + + .cloud-primary { + width: 100%; + padding: 14px; + border: none; + border-radius: 13px; + background: var(--accent); + color: var(--accent-fg); + font-family: var(--font-sans); + font-size: 14.5px; + font-weight: 700; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + + svg { + width: 17px; + height: 17px; + } + + &:hover { + filter: brightness(1.06); } + } - [level="warning"] { - border-left-color: darkorange; + .cloud-logout { + width: 100%; + padding: 12px; + border: 1px solid var(--border-strong); + border-radius: 13px; + background: transparent; + color: var(--text-dim); + font-family: var(--font-sans); + font-size: 13.5px; + font-weight: 700; + cursor: pointer; + + &:hover { + border-color: var(--danger); + color: var(--danger); } + } - [level="info"] { - border-left-color: #08c; + .cloud-connect { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 20px; + } + + .cloud-brand-lg { + width: 64px; + height: 64px; + border-radius: 18px; + flex: none; + background: oklch(0.6 0.16 245); + display: flex; + align-items: center; + justify-content: center; + + svg { + width: 32px; + height: 32px; } } - // Security - @mixin security-button($margin-left) { - @extend .button-small; - font-size: 12px; - margin: 20px 100px; - padding: 10px; - display: inline-block; - width: 80px; - margin-left: $margin-left; - margin-right: 0; + .cloud-connecting { + display: flex; + align-items: center; + gap: 10px; + font-size: 14px; + font-weight: 600; + color: var(--text-dim); } - #security-save { - @include security-button(40px); + .dropbox-spinner { + width: 20px; + height: 20px; + border-radius: var(--radius-pill); + border: 3px solid var(--ring-track); + border-top-color: var(--accent); + animation: spin 0.8s linear infinite; } - #security-remove { - @include security-button(30px); + // Google Drive branding (multicolor logo on a neutral chip; red-ish avatar) + &.drive { + .cloud-brand { + background: var(--row); + border: 1px solid var(--border); + } + + .cloud-avatar { + background: oklch(0.86 0.07 25); + color: oklch(0.45 0.18 25); + } } - .badInput { - @include themify($themes) { - input { - border-color: themed("red-1"); - } + // Connect prompt (design "17c CONNECT DRIVE") + .cloud-hero { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + } + + .cloud-hero-logo { + width: 78px; + height: 78px; + border-radius: 22px; + background: var(--row); + border: 1px solid var(--border); + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 22px; + + svg { + width: 40px; + height: 40px; } } - &.fadein { - top: 10px; - animation: fadein 0.2s 1 ease-out; + .cloud-hero-title { + font-size: 22px; + font-weight: 800; + letter-spacing: -0.02em; + margin-bottom: 10px; } - &.fadeout { - top: 110px; - animation: fadeout 0.2s 1 ease-in; + .cloud-hero-desc { + font-size: 13.5px; + line-height: 1.55; + color: var(--text-dim); + max-width: 270px; } - &.show { - top: 10px; + .cloud-foot { + display: flex; + flex-direction: column; + gap: 8px; + } + + .cloud-note { + display: flex; + align-items: center; + gap: 9px; + padding: 11px 13px; + border-radius: 12px; + background: var(--row); + color: var(--text-dim); + font-size: 12.5px; + + svg { + flex: none; + width: 15px; + height: 15px; + stroke: var(--ok); + } } } -// Menu +// ── Slide-in menu ─────────────────────────────────────────────────────── #menu { - width: 320px; - height: 480px; + width: $popup-w; + height: $popup-h; position: absolute; left: -1000px; top: 0; + background: var(--app-bg); &.slidein { left: 0; @@ -851,32 +1882,32 @@ svg { #menuBody { overflow-y: auto; - height: 442px; + height: $popup-h - $header-h; width: inherit; position: absolute; - @include themify($themes) { - background: themed("grey-background"); - } + background: var(--app-bg); + padding: 8px; } .menuList { - margin: 10px; - border-radius: 2px; - @include themify($themes) { - border: themed("grey-2") 1px solid; - background: themed("white-1"); - } + margin: 8px; + border-radius: var(--radius-row); + overflow: hidden; + background: var(--row); p { position: relative; - padding: 10px; - font-size: 16px; + padding: 13px 14px; + font-size: 14.5px; + font-weight: 600; cursor: pointer; display: grid; grid-template-columns: 30px auto; - @include themify($themes) { - border-bottom: themed("grey-2") 1px solid; - color: themed("grey-1"); + align-items: center; + color: var(--text-dim); + + &:not(:last-child) { + border-bottom: 1px solid var(--border); } span { @@ -884,76 +1915,60 @@ svg { align-items: center; svg { - @include themify($themes) { - fill: themed("grey-1"); - } - height: 16px; - width: 16px; + fill: var(--text-dim); + height: 17px; + width: 17px; } } &:hover { - @include themify($themes) { - background: themed("blue-menu"); - color: themed("black-1"); - } + background: var(--row-hover); + color: var(--text); svg { - @include themify($themes) { - fill: themed("black-1"); - } + fill: var(--text); } } - - &:last-child { - border-bottom: none; - } } } } #version { text-align: center; - margin: 10px; - @include themify($themes) { - color: themed("grey-1"); - } -} - -// Animations -@keyframes timer { - to { - stroke-dashoffset: -25.12; - } + margin: 16px 10px; + font-size: 12px; + color: var(--text-faint); + font-family: var(--font-mono); } -@keyframes twinkling { - 0% { - color: #eea59c; - } +// ── Animations ────────────────────────────────────────────────────────── +@keyframes glint { + 0%, 100% { - color: #dd4b39; + opacity: 1; + } + 50% { + opacity: 0.35; } } -@keyframes flash { - 0%, - 33% { +@keyframes toastUp { + from { opacity: 0; + transform: translate(-50%, 12px); } - 33.1%, - 100% { + to { opacity: 1; + transform: translate(-50%, 0); } } -@keyframes glint { - 0%, - 100% { - opacity: 1; +@keyframes sheetUp { + from { + transform: translateY(100%); } - 50% { - opacity: 0.15; + to { + transform: translateY(0); } } @@ -1019,351 +2034,415 @@ svg { } } -// Misc -@font-face { - font-family: "Droid Sans Mono"; - font-style: normal; - font-weight: 400; - src: url(DroidSansMono.woff2) format("woff2"); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, - U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000; -} - .gu-mirror { display: none; } +// ── Scrollbars ────────────────────────────────────────────────────────── ::-webkit-scrollbar { - width: 10px; - @include themify($themes) { - background: themed("grey-3"); - } + width: 8px; + height: 8px; } ::-webkit-scrollbar-thumb { - @include themify($themes) { - background-color: themed("grey-1"); - border: 2px solid themed("grey-3"); - } - border-radius: 5px; + background: var(--border-strong); + border-radius: 8px; } -// Dark overrides -.theme-dark { - #codes { - &:not(.edit) { - .code.timeout:not(.hotp) { - animation: glint 1s infinite; - } - } - } - - .message-box, - #info { - border: #ccc 1px solid; - box-shadow: 1px 1px 3px black; - } - - .entry .issuerEdit input { - color: #ccc; - } +::-webkit-scrollbar-track { + background: transparent; +} - ::-webkit-scrollbar { - background: #1e1e1e !important; - } +// ── Onboarding (first run) ────────────────────────────────────────────── +.onboarding { + height: $popup-h; + display: flex; + flex-direction: column; + padding: 34px 30px 30px; + background: var(--app-bg); + color: var(--text); - ::-webkit-scrollbar-thumb { - background-color: rgba(255, 255, 255, 0.5) !important; - border: 2px solid #1e1e1e !important; + .onboarding-hero { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; } -} -// Simple & Compact overrides -.theme-simple, -.theme-compact { - .header { - border-bottom: white 1px solid !important; + .onboarding-logo { + width: 78px; + height: 78px; + border-radius: 22px; + background: var(--accent); + color: var(--accent-fg); + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 10px 26px var(--accent-soft); + margin-bottom: 26px; - &::after { - content: ""; - display: block; - margin: 0 20px -1px 20px; - border-bottom: #eee 1px solid; + svg { + width: 40px; + height: 40px; } } - #search { - border-color: white !important; + .onboarding-title { + font-size: 24px; + font-weight: 800; + letter-spacing: -0.02em; + margin-bottom: 10px; } - .entry { - border-color: white !important; + .onboarding-sub { + font-size: 14px; + line-height: 1.5; + color: var(--text-dim); + max-width: 270px; } - &:not(.edit) { - .entry:hover { - background: #f8f8f8; + .onboarding-features { + display: flex; + flex-direction: column; + gap: 11px; + margin-bottom: 22px; + + .feature { + display: flex; + align-items: center; + gap: 11px; + font-size: 13.5px; + color: var(--text-dim); + + .dot { + width: 7px; + height: 7px; + border-radius: var(--radius-pill); + flex: none; + } } } - .control-group select { - border-color: white !important; - } - - .menuList { - border-color: white !important; + .onboarding-primary { + width: 100%; + padding: 15px; + border: none; + border-radius: 14px; + background: var(--accent); + color: var(--accent-fg); + font-family: var(--font-sans); + font-size: 15px; + font-weight: 700; + cursor: pointer; - p { - border-color: white !important; + &:hover { + filter: brightness(1.06); } } - ::-webkit-scrollbar { - background-color: white !important; - } + .onboarding-secondary { + width: 100%; + padding: 13px; + margin-top: 8px; + border: none; + border-radius: 14px; + background: transparent; + color: var(--text-dim); + font-family: var(--font-sans); + font-size: 13.5px; + font-weight: 600; + cursor: pointer; - ::-webkit-scrollbar-thumb { - border: 2px solid white !important; + &:hover { + color: var(--text); + } } +} - .button { - background: #f2f2f2 !important; - border: #f2f2f2 1px solid !important; - color: black !important; +// ── Loading (design "13b LOADING") ────────────────────────────────────── +.loading-page { + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 22px; - &:hover { - background: #f8f8f8 !important; - border: #f8f8f8 1px solid !important; - } + .loading-spinner { + width: 52px; + height: 52px; + border-radius: var(--radius-pill); + border: 4px solid var(--ring-track); + border-top-color: var(--accent); + animation: spin 0.8s linear infinite; } - #info { - box-shadow: none !important; + .loading-text { + font-size: 15px; + font-weight: 700; + color: var(--text-dim); + letter-spacing: 0.02em; } +} - .message-box { - border: #ccc 1px solid; - box-shadow: none !important; +@keyframes spin { + to { + transform: rotate(360deg); } } -// Compact overrides -.theme-compact { - #codes { - &:not(.edit) { - .entry { - padding: 0 10px; - margin: 0 0 0 10px; - } - } +// Inline "connecting…" indicator on the cloud backup sign-in pages. +.dropbox-connecting { + display: flex; + align-items: center; + gap: 8px; + margin: 10px 0 0 20px; + color: var(--text-dim); + + .dropbox-spinner { + width: 14px; + height: 14px; + border-radius: var(--radius-pill); + border: 2px solid var(--ring-track); + border-top-color: var(--accent); + animation: spin 0.8s linear infinite; } +} - .entry { - .sector { - bottom: 0; - } +// ── Manage password (design "14 MANAGE PASSWORD") ─────────────────────── +// Scoped under #info so its label rules beat the generic `#info label`. +#info .pw-page { + height: 100%; + display: flex; + flex-direction: column; + gap: 14px; + + .pw-warning { + display: flex; + gap: 10px; + padding: 13px; + border-radius: 13px; + background: var(--warn-soft); - .showqr, - .pin { - top: 0; + svg { + flex: none; + width: 18px; + height: 18px; + stroke: var(--warn); + margin-top: 1px; } - .code { - font-size: 32px; - margin-top: -5px; - letter-spacing: 0.5rem; + div { + font-size: 12px; + line-height: 1.5; + color: var(--text-dim); } } - a.entry { - border-bottom: #eee 1px solid !important; + .pw-field label { + display: block; + font-size: 12px; + font-weight: 700; + color: var(--text-dim); + margin-bottom: 7px; } - .issuer.account { - display: none; - } -} + .pw-input { + width: 100%; + padding: 13px; + border-radius: 12px; + border: 1px solid var(--border-strong); + background: var(--row); + color: var(--text); + font-family: var(--font-mono); + font-size: 14px; + letter-spacing: 2px; + outline: none; -// Accessibility overrides -.theme-accessibility { - select { - border-color: white; - color: white; - background-color: black; - } - .entry { - .issuerEdit { - input { - color: white; - background: black; - border: white 1px solid; - } + &:focus { + border-color: var(--accent); + background: var(--app-bg); } + } - .showqr, - .pin { - svg { - fill: yellow; - } + .pw-strength { + display: flex; + gap: 8px; + margin-top: 9px; - &:hover { - svg { - fill: yellow; - } - } + .seg { + flex: 1; + height: 5px; + border-radius: var(--radius-pill); + transition: background 0.3s; } } - #menu { - .menuList { - p:hover { - color: black; + .pw-save { + margin-top: auto; + width: 100%; + padding: 15px; + border: none; + border-radius: 14px; + background: var(--accent); + color: var(--accent-fg); + font-family: var(--font-sans); + font-size: 15px; + font-weight: 700; + cursor: pointer; - svg { - fill: black; - } - } + &:hover { + filter: brightness(1.06); } } - .header { - .icon { - svg { - fill: yellow; - } - - &#i-sync { - svg { - fill: white; - } - - &:hover { - svg { - fill: white; - } - } - } + .pw-remove { + width: 100%; + padding: 13px; + border: 1px solid var(--danger); + border-radius: 14px; + background: transparent; + color: var(--danger); + font-family: var(--font-sans); + font-size: 14px; + font-weight: 700; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; - &:hover { - svg { - fill: yellow; - } - } + svg { + width: 16px; + height: 16px; } - } - #codes { - &:not(.edit) { - .code.timeout:not(.hotp) { - animation: flash 1s infinite; - } + &:hover { + background: var(--row); } } +} - #info { - border: 1px solid white; - box-shadow: none; - } +// ── Unlock screen (design "13 LOCKED") ───────────────────────────────── +.lock-page { + height: 100%; + display: flex; + flex-direction: column; - #info .control-group input[type="checkbox"]::before { - background: none; - border: 2px solid white; - box-sizing: border-box; + .lock-hero { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; } - #info .control-group input[type="checkbox"]::after { - border: 2px solid black; - margin: -2px; - box-shadow: none; + .lock-icon { + width: 80px; + height: 80px; + border-radius: 24px; + background: var(--accent-soft); + color: var(--accent); + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 24px; + + svg { + width: 38px; + height: 38px; + } } - #info .control-group input[type="checkbox"]:checked::before { - background: white; + .lock-title { + font-size: 22px; + font-weight: 800; + letter-spacing: -0.02em; + margin-bottom: 8px; } - .input { - color: black; + .lock-desc { + font-size: 13.5px; + line-height: 1.5; + color: var(--text-dim); + max-width: 250px; + margin-bottom: 24px; } - #notification { - &.fadein { - opacity: 1; - animation: none; - } + .lock-input { + width: 100%; + padding: 14px; + border-radius: 13px; + border: 1px solid var(--border-strong); + background: var(--row); + color: var(--text); + font-family: var(--font-mono); + font-size: 16px; + letter-spacing: 3px; + text-align: center; + outline: none; - &.fadeout { - opacity: 0; - animation: none; + &:focus { + border-color: var(--accent); + background: var(--app-bg); } - } - ::-webkit-scrollbar { - background: black !important; - } - - ::-webkit-scrollbar-thumb { - background-color: white !important; - border: 2px solid black !important; + &.badInput { + border-color: var(--danger); + } } -} -// Flat overrides -.theme-flat { - .header { - color: black; - background: white; - border-bottom: #f5f4f7 1px solid; + .lock-error { + display: block; + color: var(--danger); + font-size: 12.5px; + margin-top: 10px; } - #codes { - background: #fcfbff; + .lock-unlock { + width: 100%; + padding: 15px; + border: none; + border-radius: 14px; + background: var(--accent); + color: var(--accent-fg); + font-family: var(--font-sans); + font-size: 15px; + font-weight: 700; + cursor: pointer; - .entry { - border: #f5f4f7 1px solid; - border-radius: 8px; + &:hover { + filter: brightness(1.06); } } +} - #menu { - #menuBody { - background: #fcfbff; - - .menuList { - margin: 10px; - border-radius: 8px; - border: #f5f4f7 1px solid; - background: white; - - p { - color: #727272; - - &:not(:last-child) { - border-bottom: #f5f4f7 1px solid; - } +// ── Compact density ───────────────────────────────────────────────────── +.theme-compact { + .entry { + padding: 7px 12px; + gap: 10px; - span svg { - fill: #727272; - } - } - } + .monogram { + width: 30px; + height: 30px; + font-size: 14px; + border-radius: 9px; } - #version { - bottom: 0px; - position: relative; - margin: 10px auto; - width: 100%; - color: #727272; - font-size: 0.9em; + .code { + font-size: 18px; } - #info { - border-radius: 8px; - border: #f5f4f7 1px solid; - - .control-group { - border-bottom: #f5f4f7 1px solid; - } + .account { + display: none; } } + + #codes { + gap: 4px; + } } diff --git a/scripts/build.sh b/scripts/build.sh index 3da00c0e2..98e93e27b 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -46,8 +46,6 @@ if ! [[ $REMOTE = *"https://github.com/Authenticator-Extension/Authenticator.git echo -e "Thanks for forking Authenticator! If you plan on redistributing your own version of Authenticator please generate your own API keys and put them in ./src/models/credentials.ts and ./manifest-chrome.json" echo "Clear this warning by commenting it out in ./scripts/build.sh" echo - read -rsp $'Press any key to continue...\n' -n1 key - echo fi echo "Compiling..." diff --git a/src/argon.ts b/src/argon.ts index ea27b3967..53a086c56 100644 --- a/src/argon.ts +++ b/src/argon.ts @@ -11,13 +11,14 @@ window.addEventListener("message", (event) => { switch (message.action) { case "hash": Argon.hash(message.value, message.salt).then((hash) => { - source.postMessage({ response: hash }, event.origin); + // echo the request id so the caller can match the reply to its request + source.postMessage({ id: message.id, response: hash }, event.origin); }); break; case "verify": Argon.compareHash(message.hash, message.value).then((result) => { - source.postMessage({ response: result }, event.origin); + source.postMessage({ id: message.id, response: result }, event.origin); }); break; @@ -50,8 +51,10 @@ class Argon { encoded: hash, }) .then(() => resolve(true)) - .catch((e: { message: string; code: number }) => { - console.error("Error decoding hash", e); + .catch(() => { + // verify() rejects on a wrong passphrase (the common case) as well as + // on a malformed hash; both just mean "cannot unlock", so report a + // non-match instead of logging an error for every mistyped password. resolve(false); }); }); diff --git a/src/background.ts b/src/background.ts index bb59a2f9d..c8f1fc723 100644 --- a/src/background.ts +++ b/src/background.ts @@ -14,60 +14,98 @@ import { getOTPAuthPerLineFromOPTAuthMigration } from "./models/migration"; import { isChrome, isFirefox } from "./browser"; import { UserSettings } from "./models/settings"; -let contentTab: chrome.tabs.Tab | undefined; - -chrome.runtime.onMessage.addListener(async (message, sender) => { - await UserSettings.updateItems(); +chrome.runtime.onMessage.addListener((message, sender) => { + // Only act on messages from our own extension pages / content scripts, never + // another extension. (No externally_connectable is set, so web pages can't + // reach here, but this is cheap defense-in-depth for the sensitive actions + // below — cache passphrase, cloud backup, lock.) + if (sender.id !== chrome.runtime.id) { + return; + } - if (message.action === "getCapture") { - if (!sender.tab) { - return; - } - const url = await getCapture(sender.tab); - if (contentTab && contentTab.id) { + // None of these handlers send a response, so do the async work fire-and-forget + // and DON'T return true. Returning true kept the message channel open waiting + // for a sendResponse that never came, so the sender's sendMessage promise + // rejected with "message channel closed before a response was received" + // (e.g. when a QR capture finds no code). + void (async () => { + await UserSettings.updateItems(); + + if (message.action === "getCapture") { + // Use sender.tab, not a module-level tab ref: in MV3 the service worker + // can be torn down between framing the QR region and this message + // arriving, leaving such a ref undefined -- so the capture reply was + // never sent back and the scan silently did nothing. sender.tab is the + // content script that asked, so it is always the right (and live) tab. + if (!sender.tab || sender.tab.id === undefined) { + return; + } + const url = await getCapture(sender.tab); message.info.url = url; - chrome.tabs.sendMessage(contentTab.id, { + chrome.tabs.sendMessage(sender.tab.id, { action: "sendCaptureUrl", info: message.info, }); + } else if (message.action === "getTotp") { + getTotp(message.info, sender.tab?.id, false, hostnameFromTab(sender.tab)); + } else if (message.action === "cachePassphrase") { + chrome.storage.session.set({ + cachedPassphrase: message.value, + cachedKeyId: message.keyId, + }); + chrome.alarms.clear("autolock"); + setAutolock(); + } else if (["dropbox", "drive", "onedrive"].indexOf(message.action) > -1) { + getBackupToken(message.action); + } else if (message.action === "lock") { + chrome.storage.session.set({ cachedPassphrase: null, cachedKeyId: null }); + } else if (message.action === "resetAutolock") { + chrome.alarms.clear("autolock"); + setAutolock(); + } else if (message.action === "updateContentTab") { + // Persist in session storage so the autolock alarm can still reach this + // tab to dismiss the capture overlay after the MV3 worker recycles. + chrome.storage.session.set({ captureTabId: message.data?.id }); + } else if (message.action === "updateContextMenu") { + updateContextMenu(); } - } else if (message.action === "getTotp") { - getTotp(message.info); - } else if (message.action === "cachePassphrase") { - chrome.storage.session.set({ - cachedPassphrase: message.value, - cachedKeyId: message.keyId, - }); - chrome.alarms.clear("autolock"); - setAutolock(); - } else if (["dropbox", "drive", "onedrive"].indexOf(message.action) > -1) { - getBackupToken(message.action); - } else if (message.action === "lock") { - chrome.storage.session.set({ cachedPassphrase: null, cachedKeyId: null }); - } else if (message.action === "resetAutolock") { - chrome.alarms.clear("autolock"); - setAutolock(); - } else if (message.action === "updateContentTab") { - contentTab = message.data; - } else if (message.action === "updateContextMenu") { - updateContextMenu(); - } - - // https://stackoverflow.com/a/56483156 - return true; + })(); }); chrome.alarms.onAlarm.addListener(() => { - chrome.storage.session.set({ cachedPassphrase: null, cachedKeyId: null }); - if (contentTab && contentTab.id) { - chrome.tabs.sendMessage(contentTab.id, { action: "stopCapture" }); - } - chrome.runtime.sendMessage({ action: "stopImport" }); - - // https://stackoverflow.com/a/56483156 - return true; + void (async () => { + await chrome.storage.session.set({ + cachedPassphrase: null, + cachedKeyId: null, + }); + // captureTabId is persisted in session storage (like the cached passphrase) + // so it survives MV3 service-worker recycling. The old module-level tab ref + // was undefined in a fresh worker, so a QR capture overlay left open stayed + // stuck on the page after autolock. Dismiss it if a tab is still recorded. + const { captureTabId } = await chrome.storage.session.get("captureTabId"); + if (typeof captureTabId === "number") { + chrome.tabs + .sendMessage(captureTabId, { action: "stopCapture" }) + .catch(() => undefined); + await chrome.storage.session.remove("captureTabId"); + } + chrome.runtime.sendMessage({ action: "stopImport" }).catch(() => undefined); + })(); }); +// The host of the tab a QR scan was initiated from, used to pre-bind a newly +// imported entry to the site the user scanned it on. +function hostnameFromTab(tab?: chrome.tabs.Tab): string | undefined { + if (!tab || !tab.url) { + return undefined; + } + try { + return new URL(tab.url).hostname.toLowerCase(); + } catch { + return undefined; + } +} + async function getCapture(tab: chrome.tabs.Tab) { const dataUrl = await chrome.tabs.captureVisibleTab(tab.windowId, { format: "png", @@ -76,11 +114,19 @@ async function getCapture(tab: chrome.tabs.Tab) { return dataUrl; } -async function getTotp(text: string, silent = false) { - if (!contentTab || !contentTab.id || !text) { +async function getTotp( + text: string, + tabId?: number, + silent = false, + host?: string +) { + // tabId is the content tab that initiated the scan (sender.tab.id). Relying on + // a module-level tab ref here broke scans whenever the MV3 service worker had + // been recycled (it was undefined -> silent return). + if (tabId === undefined || !text) { return false; } - const id = contentTab.id; + const id = tabId; if (text.indexOf("otpauth://") !== 0) { if (text.indexOf("otpauth-migration://") === 0) { @@ -92,11 +138,15 @@ async function getTotp(text: string, silent = false) { const getTotpPromises: Array> = []; for (const otpUrl of otpUrls) { - getTotpPromises.push(getTotp(otpUrl, true)); + getTotpPromises.push(getTotp(otpUrl, id, true, host)); } const getTotpResults = await Promise.allSettled(getTotpPromises); - const failedCount = getTotpResults.filter((res) => !res).length; + // allSettled entries are always-truthy {status,value} objects, so the + // old `!res` test was never true and every import reported success. + const failedCount = getTotpResults.filter( + (res) => res.status !== "fulfilled" || !res.value + ).length; if (failedCount === otpUrls.length) { !silent && chrome.tabs.sendMessage(id, { action: "migrationfail" }); return false; @@ -208,6 +258,7 @@ async function getTotp(text: string, silent = false) { account, hash, issuer, + host, secret, type, encrypted: false, @@ -241,23 +292,7 @@ async function getTotp(text: string, silent = false) { } function getBackupToken(service: string) { - if (isChrome && service === "drive") { - chrome.identity.getAuthToken( - { - interactive: true, - scopes: ["https://www.googleapis.com/auth/drive.file"], - }, - (value) => { - if (!value) { - return false; - } - UserSettings.items.driveToken = value; - UserSettings.commitItems(); - chrome.runtime.sendMessage({ action: "drivetoken", value }); - return true; - } - ); - } else { + { let authUrl = ""; let redirUrl = ""; if (service === "dropbox") { @@ -268,13 +303,10 @@ function getBackupToken(service: string) { "&redirect_uri=" + redirUrl; } else if (service === "drive") { - if (navigator.userAgent.indexOf("Edg") !== -1) { - redirUrl = encodeURIComponent("https://authenticator.cc/oauth-edge"); - } else if (isFirefox) { - redirUrl = encodeURIComponent(chrome.identity.getRedirectURL()); - } else { - redirUrl = encodeURIComponent("https://authenticator.cc/oauth"); - } + // The fork has no authenticator.cc redirect handler, and getAuthToken is + // blocked for new OAuth clients (custom-URI-scheme restriction), so Drive + // uses launchWebAuthFlow with the extension's own chromiumapp.org redirect. + redirUrl = encodeURIComponent(chrome.identity.getRedirectURL()); authUrl = "https://accounts.google.com/o/oauth2/v2/auth?response_type=code&access_type=offline&client_id=" + @@ -293,6 +325,10 @@ function getBackupToken(service: string) { { url: authUrl, interactive: true }, async (url) => { if (!url) { + // auth window was closed/cancelled — let an open popup reset its UI + chrome.runtime + .sendMessage({ action: `${service}authdone` }) + .catch(() => undefined); return; } let hashMatches = url.split("#"); @@ -322,6 +358,11 @@ function getBackupToken(service: string) { if (service === "dropbox") { UserSettings.items.dropboxToken = value; UserSettings.commitItems(); + // tell an open popup the connection finished so it can leave + // the sign-in view and stop the connecting indicator. + chrome.runtime + .sendMessage({ action: "dropboxauthdone" }) + .catch(() => undefined); uploadBackup("dropbox"); return; } @@ -364,6 +405,9 @@ function getBackupToken(service: string) { throw error; } + chrome.runtime + .sendMessage({ action: "driveauthdone" }) + .catch(() => undefined); uploadBackup("drive"); return success; } else if (service === "onedrive") { @@ -379,6 +423,19 @@ function getBackupToken(service: string) { Accept: "application/json", "Content-Type": "application/x-www-form-urlencoded", }, + // Microsoft's token endpoint requires the parameters in the + // request body; without it the exchange always failed and + // OneDrive sign-in never completed. + body: + "client_id=" + + getCredentials().onedrive.client_id + + "&client_secret=" + + getCredentials().onedrive.client_secret + + "&code=" + + value + + "&redirect_uri=" + + redirUrl + + "&grant_type=authorization_code", } ); @@ -476,7 +533,7 @@ chrome.commands.onCommand.addListener(async (command: string) => { files: ["/css/content.css"], }); - contentTab = tab; + chrome.storage.session.set({ captureTabId: tab.id }); chrome.tabs.sendMessage(tab.id, { action: "capture" }); } break; @@ -493,16 +550,16 @@ chrome.commands.onCommand.addListener(async (command: string) => { files: ["/css/content.css"], }); - contentTab = tab; - const siteName = await getSiteName(); const entries = await EntryStorage.get(); - const matchedEntries = getMatchedEntries(siteName, entries); + // strict=true: autofill pastes a live OTP, so don't trust the page title + const matchedEntries = getMatchedEntries(siteName, entries, true); if (matchedEntries && matchedEntries.length === 1) { const entry = matchedEntries[0]; const encryption = new Encryption(cachedPassphrase, cachedKeyId); - entry.applyEncryption(encryption); + // applyEncryption is async now; await so entry.code is decrypted + await entry.applyEncryption(encryption); if ( entry.code !== CodeState.Encrypted && diff --git a/src/components/Import.vue b/src/components/Import.vue index e224a82a2..f6e51b71f 100644 --- a/src/components/Import.vue +++ b/src/components/Import.vue @@ -1,38 +1,95 @@
-
-
- - - - - - +
+
+
+ +

{{ i18n.import_backup }}

-
-

- {{ i18n.otp_backup_inform }} - {{ - i18n.otp_backup_learn - }} -

+

+ {{ i18n.otp_backup_inform }} + {{ + i18n.otp_backup_learn + }} +

+ +
+ + + +
+ +
+
-
{{ i18n.import_error_password }} @@ -40,12 +97,12 @@
diff --git a/src/components/Import/QrImport.vue b/src/components/Import/QrImport.vue index 6edd0702d..f3ca4a249 100644 --- a/src/components/Import/QrImport.vue +++ b/src/components/Import/QrImport.vue @@ -13,7 +13,7 @@
diff --git a/src/components/Popup/AddAccountPage.vue b/src/components/Popup/AddAccountPage.vue index 20e82e9bc..5c098f742 100644 --- a/src/components/Popup/AddAccountPage.vue +++ b/src/components/Popup/AddAccountPage.vue @@ -1,57 +1,91 @@ diff --git a/src/components/Popup/DropboxPage.vue b/src/components/Popup/DropboxPage.vue index 030d2d6ca..ccf346522 100644 --- a/src/components/Popup/DropboxPage.vue +++ b/src/components/Popup/DropboxPage.vue @@ -1,45 +1,129 @@ diff --git a/src/components/Popup/EnterPasswordPage.vue b/src/components/Popup/EnterPasswordPage.vue index ebe533e86..24a7032ad 100644 --- a/src/components/Popup/EnterPasswordPage.vue +++ b/src/components/Popup/EnterPasswordPage.vue @@ -1,23 +1,43 @@ diff --git a/src/components/Popup/MainBody.vue b/src/components/Popup/MainBody.vue index 4e8c4e92d..7bb78cce7 100644 --- a/src/components/Popup/MainBody.vue +++ b/src/components/Popup/MainBody.vue @@ -1,11 +1,29 @@ diff --git a/src/components/Popup/MainHeader.vue b/src/components/Popup/MainHeader.vue index f97d77817..36f465a88 100644 --- a/src/components/Popup/MainHeader.vue +++ b/src/components/Popup/MainHeader.vue @@ -1,7 +1,33 @@ diff --git a/src/components/Popup/OneDrivePage.vue b/src/components/Popup/OneDrivePage.vue index 8c93fc782..9fcc175cd 100644 --- a/src/components/Popup/OneDrivePage.vue +++ b/src/components/Popup/OneDrivePage.vue @@ -1,7 +1,10 @@ diff --git a/src/components/common/ButtonLink.vue b/src/components/common/ButtonLink.vue index 6a8753caf..98de8940c 100644 --- a/src/components/common/ButtonLink.vue +++ b/src/components/common/ButtonLink.vue @@ -9,9 +9,9 @@ diff --git a/src/components/common/FileInput.vue b/src/components/common/FileInput.vue index 9103990f0..25d31b1ff 100644 --- a/src/components/common/FileInput.vue +++ b/src/components/common/FileInput.vue @@ -4,16 +4,17 @@
diff --git a/src/components/common/SelectInput.vue b/src/components/common/SelectInput.vue index b249ddf9e..64a7a6bff 100644 --- a/src/components/common/SelectInput.vue +++ b/src/components/common/SelectInput.vue @@ -3,9 +3,9 @@
diff --git a/src/content.ts b/src/content.ts index 5ac1805cf..d4c5c5695 100644 --- a/src/content.ts +++ b/src/content.ts @@ -61,9 +61,10 @@ if (!document.getElementById("__ga_grayLayout__")) { // invalid command, ignore it break; } - - // https://stackoverflow.com/a/56483156 - return true; + // Only "capture" responds, and it does so synchronously, so don't return + // true. Returning true kept the channel open waiting for a response that + // never came for the other actions (e.g. sendCaptureUrl on a non-QR image), + // making the background sender's sendMessage promise reject. }); } @@ -98,6 +99,10 @@ function showGrayLayout() { event.preventDefault(); return; }; + // Belt-and-suspenders: explicitly refuse native drag (e.g. dragging over an + // image/link under the overlay), which otherwise shows the no-drop cursor. + grayLayout.ondragstart = () => false; + grayLayout.style.userSelect = "none"; } grayLayout.style.display = "block"; } @@ -112,6 +117,10 @@ function grayLayoutDown(event: MouseEvent) { return; } + // Stop the browser from starting a native text-selection / image drag, which + // shows the "no-drop" cursor and steals the gesture from our drag-select. + event.preventDefault(); + sessionStorage.setItem("captureBoxPositionLeft", event.clientX.toString()); sessionStorage.setItem("captureBoxPositionTop", event.clientY.toString()); captureBox.style.left = event.clientX + "px"; @@ -132,6 +141,12 @@ function grayLayoutMove(event: MouseEvent) { event.preventDefault(); return; } + // Only redraw while the left button is actually held. Without this the box + // tracked every bare pointer move (before the first click and after release), + // so the selection felt jumpy and "undraggable". + if (event.buttons !== 1) { + return; + } const captureBox = document.getElementById("__ga_captureBox__"); if (!captureBox) { return; @@ -223,22 +238,37 @@ async function qrDecode( ) as HTMLCanvasElement; const qr = new Image(); qr.onload = () => { + // A failed/empty capture yields a 0x0 image; bail with feedback rather than + // dividing by it and extracting a 0-size region. + if (!qr.width || !qr.height) { + alert(chrome.i18n.getMessage("errorqr")); + return; + } const devicePixelRatio = qr.width / window.innerWidth; canvas.width = qr.width; canvas.height = qr.height; - canvas.getContext("2d")?.drawImage(qr, 0, 0); - const imageData = canvas - .getContext("2d") - ?.getImageData( - left * devicePixelRatio, - top * devicePixelRatio, - width * devicePixelRatio, - height * devicePixelRatio - ); + // willReadFrequently: we call getImageData below; silences a Chrome perf hint + const ctx = canvas.getContext("2d", { willReadFrequently: true }); + if (!ctx) { + return; + } + ctx.drawImage(qr, 0, 0); + // Clamp the selection to the captured image and require a non-empty area. + // A click or 1px drag makes a zero/out-of-bounds region, and getImageData + // then throws an uncaught IndexSizeError that kills the scan silently. + const sx = Math.max(0, Math.floor(left * devicePixelRatio)); + const sy = Math.max(0, Math.floor(top * devicePixelRatio)); + const sw = Math.min(qr.width - sx, Math.floor(width * devicePixelRatio)); + const sh = Math.min(qr.height - sy, Math.floor(height * devicePixelRatio)); + if (sw <= 0 || sh <= 0) { + alert(chrome.i18n.getMessage("errorqr")); + return; + } + const imageData = ctx.getImageData(sx, sy, sw, sh); if (imageData) { canvas.width = imageData.width; canvas.height = imageData.height; - canvas.getContext("2d")?.putImageData(imageData, 0, 0); + ctx?.putImageData(imageData, 0, 0); const qrReader = new QRCode(); qrReader.callback = ( @@ -255,7 +285,8 @@ async function qrDecode( ) => { let qrRes = ""; if (error) { - console.error(error); + // qrcode-reader reports "no finder patterns" for any non-QR region; + // that's expected, so don't log it as an error -- fall back to jsQR. const jsQrCode = jsQR( imageData.data, imageData.width, @@ -279,18 +310,29 @@ async function qrDecode( qrReader.decode(imageData); } }; + qr.onerror = () => { + alert(chrome.i18n.getMessage("errorqr")); + }; qr.src = url; } +// Skip inputs the user can't see (hidden honeypots, off-screen fields) so the +// code doesn't land in the wrong box. checkVisibility is guarded for older +// browsers that don't support it. (#1273, #1136) +function isVisibleInput(input: HTMLInputElement) { + return typeof input.checkVisibility !== "function" || input.checkVisibility(); +} + function pasteCode(code: string) { const _inputBoxes = document.getElementsByTagName("input"); const inputBoxes: HTMLInputElement[] = []; for (let i = 0; i < _inputBoxes.length; i++) { if ( - _inputBoxes[i].type === "text" || - _inputBoxes[i].type === "number" || - _inputBoxes[i].type === "tel" || - _inputBoxes[i].type === "password" + (_inputBoxes[i].type === "text" || + _inputBoxes[i].type === "number" || + _inputBoxes[i].type === "tel" || + _inputBoxes[i].type === "password") && + isVisibleInput(_inputBoxes[i]) ) { inputBoxes.push(_inputBoxes[i]); } diff --git a/src/definitions/module-interface.d.ts b/src/definitions/module-interface.d.ts index 494266315..007de404b 100644 --- a/src/definitions/module-interface.d.ts +++ b/src/definitions/module-interface.d.ts @@ -37,6 +37,7 @@ interface MenuState { smartFilter: boolean; enableContextMenu: boolean; theme: string; + onboardingComplete: boolean; backupDisabled: boolean; storageArea: "sync" | "local"; } diff --git a/src/definitions/otp.d.ts b/src/definitions/otp.d.ts index c55fb1486..6259ef9be 100644 --- a/src/definitions/otp.d.ts +++ b/src/definitions/otp.d.ts @@ -2,6 +2,7 @@ interface OTPEntryInterface { type: number; // OTPType index: number; issuer: string; + host: string; secret: string | null; account: string; hash: string; @@ -16,7 +17,7 @@ interface OTPEntryInterface { create(): Promise; update(): Promise; next(): Promise; - applyEncryption(encryption: EncryptionInterface): void; + applyEncryption(encryption: EncryptionInterface): Promise; changeEncryption(encryption: EncryptionInterface): void; delete(): Promise; generate(): void; @@ -24,9 +25,9 @@ interface OTPEntryInterface { } interface EncryptionInterface { - getEncryptedString(data: string): string; - decryptSecretString(entry: string): string | null; - decryptEncSecret(entry: OTPEntryInterface): RawOTPStorage | null; + getEncryptedString(data: string): Promise; + decryptSecretString(entry: string): Promise; + decryptEncSecret(entry: OTPEntryInterface): Promise; getEncryptionStatus(): boolean; updateEncryptionPassword(password: string): void; getEncryptionKeyId(): string; @@ -41,6 +42,7 @@ interface RawOTPStorage { hash: string; index: number; issuer?: string; + host?: string; secret: string; type: string; counter?: number; diff --git a/src/definitions/shims-vue.d.ts b/src/definitions/shims-vue.d.ts index bac18c752..e4c904ad6 100644 --- a/src/definitions/shims-vue.d.ts +++ b/src/definitions/shims-vue.d.ts @@ -1,11 +1,20 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ declare module "*.vue" { - import Vue from "vue"; - export default Vue; + import { DefineComponent } from "vue"; + const component: DefineComponent< + Record, + Record, + any + >; + export default component; } declare module "*.svg" { - import { ComponentOptions } from "vue"; - const a: ComponentOptions; - export default a; + import { DefineComponent } from "vue"; + const component: DefineComponent< + Record, + Record, + any + >; + export default component; } diff --git a/src/definitions/vue.d.ts b/src/definitions/vue.d.ts index 236017a15..6dfd192b0 100644 --- a/src/definitions/vue.d.ts +++ b/src/definitions/vue.d.ts @@ -1,11 +1,10 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { Store } from "vuex"; -declare module "vue/types/vue" { - interface Vue { +declare module "@vue/runtime-core" { + interface ComponentCustomProperties { // Only in Popup $store: Store; - $dragula: any; // Only in Import $entries: OTPEntryInterface[]; $encryption: EncryptionInterface; diff --git a/src/definitions/vue2-dragula.d.ts b/src/definitions/vue2-dragula.d.ts deleted file mode 100644 index 9d122aa79..000000000 --- a/src/definitions/vue2-dragula.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -declare module "vue2-dragula" { - import { PluginFunction, VueConstructor } from "vue"; - - const Vue2Dragula: PluginFunction; -} diff --git a/src/import.ts b/src/import.ts index 21bebdabb..57c404d64 100644 --- a/src/import.ts +++ b/src/import.ts @@ -1,20 +1,21 @@ -import Vue from "vue"; +import { createApp } from "vue"; import ImportView from "./components/Import.vue"; import CommonComponents from "./components/common/index"; import { loadI18nMessages } from "./store/i18n"; -import { Encryption } from "./models/encryption"; +import { Encryption, decryptString } from "./models/encryption"; import { EntryStorage } from "./models/storage"; import { getOTPAuthPerLineFromOPTAuthMigration } from "./models/migration"; -import * as CryptoJS from "crypto-js"; +import { argonHash, argonVerify } from "./models/password"; async function init() { + const app = createApp(ImportView); // i18n - Vue.prototype.i18n = await loadI18nMessages(); + app.config.globalProperties.i18n = await loadI18nMessages(); // Load common components globally for (const component of CommonComponents) { - Vue.component(component.name, component.component); + app.component(component.name, component.component); } // Load entries to global @@ -31,12 +32,10 @@ async function init() { } } - Vue.prototype.$entries = entries; - Vue.prototype.$encryption = encryption; + app.config.globalProperties.$entries = entries; + app.config.globalProperties.$encryption = encryption; - const instance = new Vue({ - render: (h) => h(ImportView), - }).$mount("#import"); + const instance = app.mount("#import"); // Set title try { @@ -90,13 +89,24 @@ export async function decryptBackupData( continue; } + // decryptString is prefix-aware (new AES-GCM or legacy AES-CBC backups) + const decryptedJson = await decryptString( + unknownStorageItem.data, + decryptKey + ); + if (!decryptedJson) { + // a single corrupt/undecryptable entry must not abort the whole import + continue; + } + let decryptedData; + try { + decryptedData = JSON.parse(decryptedJson); + } catch { + continue; + } storageItem = { ...unknownStorageItem, - ...JSON.parse( - CryptoJS.AES.decrypt(unknownStorageItem.data, decryptKey).toString( - CryptoJS.enc.Utf8 - ) - ), + ...decryptedData, encrypted: false, }; } else { @@ -109,15 +119,15 @@ export async function decryptBackupData( continue; } if (storageItem.encrypted && passphrase) { - try { - storageItem.secret = CryptoJS.AES.decrypt( - storageItem.secret, - passphrase - ).toString(CryptoJS.enc.Utf8); - storageItem.encrypted = false; - } catch (error) { + const decryptedSecret = await decryptString( + storageItem.secret, + passphrase + ); + if (!decryptedSecret) { continue; } + storageItem.secret = decryptedSecret; + storageItem.encrypted = false; } // storageItem.secret may be empty after decrypt with wrong // passphrase @@ -143,47 +153,17 @@ async function findAndUnlockKey( return null; } - const rawHash = await new Promise((resolve: (value: string) => void) => { - const iframe = document.getElementById("argon-sandbox"); - const message = { - action: "hash", - value: password, - salt: key.salt, - }; - if (iframe) { - window.addEventListener("message", (response) => { - resolve(response.data.response); - }); - // @ts-expect-error bad typings - iframe.contentWindow.postMessage(message, "*"); - } - }); + const rawHash = await argonHash(password, key.salt); // https://passlib.readthedocs.io/en/stable/lib/passlib.hash.argon2.html#format-algorithm - const possibleHash = rawHash.split("$")[5]; + const possibleHash = rawHash ? rawHash.split("$")[5] : ""; if (!possibleHash) { throw new Error("argon2 did not return a hash!"); } // verify user password by comparing their password hash with the // hash of their password's hash - const isCorrectPassword = await new Promise( - (resolve: (value: string) => void) => { - const iframe = document.getElementById("argon-sandbox"); - const message = { - action: "verify", - value: possibleHash, - hash: key.hash, - }; - if (iframe) { - window.addEventListener("message", (response) => { - resolve(response.data.response); - }); - // @ts-expect-error bad typings - iframe.contentWindow.postMessage(message, "*"); - } - } - ); + const isCorrectPassword = await argonVerify(possibleHash, key.hash); if (!isCorrectPassword) { return null; @@ -200,7 +180,13 @@ export async function getEntryDataFromOTPAuthPerLine(importCode: string) { for (let item of lines) { item = item.trim(); if (item.startsWith("otpauth-migration:")) { - const migrationData = getOTPAuthPerLineFromOPTAuthMigration(item); + let migrationData: string[] = []; + try { + migrationData = getOTPAuthPerLineFromOPTAuthMigration(item); + } catch (error) { + // one malformed migration payload must not abort the whole batch + console.warn("Failed to parse migration payload", error); + } for (const line of migrationData) { lines.push(line); } @@ -256,10 +242,10 @@ export async function getEntryDataFromOTPAuthPerLine(importCode: string) { parameter[0].toLowerCase() === "period" ) { period = Number(parameter[1]); - period = - isNaN(period) || period < 0 || period > 60 || 60 % period !== 0 - ? undefined - : period; + // accept any positive integer period; the old "> 60" / "60 % period" + // checks silently dropped valid periods (45, 60, 90, 120...) so those + // OTPs fell back to 30s and produced wrong codes (#1271, #1508) + period = !Number.isInteger(period) || period < 1 ? undefined : period; } else if (parameter[0].toLowerCase() === "digits") { digits = Number(parameter[1]); digits = isNaN(digits) ? 6 : digits; diff --git a/src/models/advisor.ts b/src/models/advisor.ts index c729fde24..8cf238cc9 100644 --- a/src/models/advisor.ts +++ b/src/models/advisor.ts @@ -14,7 +14,7 @@ export class AdvisorInsight implements AdvisorInsightInterface { constructor(insight: AdvisorInsightInterface) { this.id = insight.id; - this.level = insight.level as InsightLevel; + this.level = insight.level; this.levelText = chrome.i18n.getMessage(insight.level); this.description = insight.description; this.link = insight.link; diff --git a/src/models/backup.ts b/src/models/backup.ts index 5484c5727..5384b479f 100644 --- a/src/models/backup.ts +++ b/src/models/backup.ts @@ -2,6 +2,7 @@ import { getCredentials } from "./credentials"; import { Encryption } from "./encryption"; import { UserSettings } from "./settings"; import { EntryStorage } from "./storage"; +import { cloudBackupAllowed } from "../utils"; export class Dropbox implements BackupProvider { private async getToken() { @@ -10,6 +11,11 @@ export class Dropbox implements BackupProvider { } async upload(encryption: Encryption) { + // Never let a cloud backup carry plaintext secrets: require a master + // password (which encrypts the export) before uploading anything. + if (!cloudBackupAllowed(encryption)) { + return false; + } await UserSettings.updateItems(); if (UserSettings.items.dropboxEncrypted === undefined) { @@ -25,50 +31,43 @@ export class Dropbox implements BackupProvider { const url = "https://content.dropboxapi.com/2/files/upload"; const token = await this.getToken(); - return new Promise( - (resolve: (value: boolean) => void, reject: (reason: Error) => void) => { - if (!token) { - return resolve(false); - } - try { - const xhr = new XMLHttpRequest(); - const now = new Date().toISOString().slice(0, 10).replace(/-/g, ""); - const apiArg = { - path: `/${now}.json`, - mode: "add", - autorename: true, - }; - xhr.open("POST", url); - xhr.setRequestHeader("Authorization", "Bearer " + token); - xhr.setRequestHeader("Content-type", "application/octet-stream"); - xhr.setRequestHeader("Dropbox-API-Arg", JSON.stringify(apiArg)); - xhr.onreadystatechange = () => { - if (xhr.readyState === 4) { - if (xhr.status === 401) { - UserSettings.items.dropboxToken = undefined; - UserSettings.items.dropboxRevoked = true; - UserSettings.commitItems(); - return resolve(false); - } - try { - const res = JSON.parse(xhr.responseText); - if (res.name) { - resolve(true); - } else { - resolve(false); - } - } catch (error) { - reject(error as Error); - } - } - return; - }; - xhr.send(backup); - } catch (error) { - return reject(error as Error); - } - } - ); + if (!token) { + return false; + } + const now = new Date().toISOString().slice(0, 10).replace(/-/g, ""); + const apiArg = { + path: `/${now}.json`, + mode: "add", + autorename: true, + }; + // fetch (not XMLHttpRequest) because upload runs in the MV3 background + // service worker, where XHR is not defined. + const res = await fetch(url, { + method: "POST", + headers: { + Authorization: "Bearer " + token, + "Content-Type": "application/octet-stream", + "Dropbox-API-Arg": JSON.stringify(apiArg), + }, + body: backup, + }); + if (res.status === 401) { + UserSettings.items.dropboxToken = undefined; + UserSettings.items.dropboxRevoked = true; + UserSettings.commitItems(); + return false; + } + if (!res.ok) { + // a non-2xx (5xx, HTML error page, ...) is a failed upload, not + // something to JSON.parse and maybe misread as success. Surface + // Dropbox's body so the actual cause (bad path / arg / scope) is visible. + const detail = await res.text(); + throw new Error( + "Dropbox upload failed: HTTP " + res.status + " " + detail + ); + } + const body = await res.json(); + return Boolean(body.name); } async getUser() { await UserSettings.updateItems(); @@ -88,8 +87,9 @@ export class Dropbox implements BackupProvider { UserSettings.items.dropboxRevoked = true; UserSettings.commitItems(); resolve( - "Error: Response was 401. You will be logged out the next time you open Authenticator." + "Error: Response was 401. You will be logged out the next time you open OTPilot." ); + return; } try { const res = JSON.parse(xhr.responseText); @@ -114,54 +114,20 @@ export class Dropbox implements BackupProvider { export class Drive implements BackupProvider { private async getToken() { await UserSettings.updateItems(); - if ( - !UserSettings.items.driveToken || - (await new Promise( - ( - resolve: (value: boolean) => void, - reject: (reason: Error) => void - ) => { - const xhr = new XMLHttpRequest(); - xhr.open("GET", "https://www.googleapis.com/drive/v3/files"); - xhr.setRequestHeader( - "Authorization", - "Bearer " + UserSettings.items.driveToken - ); - xhr.onreadystatechange = async () => { - if (xhr.readyState === 4) { - try { - const res = JSON.parse(xhr.responseText); - if (res.error) { - if (res.error.code === 401) { - if ( - navigator.userAgent.indexOf("Chrome") !== -1 && - navigator.userAgent.indexOf("OPR") === -1 && - navigator.userAgent.indexOf("Edg") === -1 - ) { - // Clear invalid token from - // chrome://identity-internals/ - await chrome.identity.removeCachedAuthToken({ - token: UserSettings.items.driveToken as string, - }); - } - UserSettings.items.driveToken = undefined; - UserSettings.commitItems(); - resolve(true); - } - } else { - resolve(false); - } - } catch (error) { - console.error(error); - reject(error as Error); - } - } - return; - }; - xhr.send(); - } - )) - ) { + let needsRefresh = !UserSettings.items.driveToken; + if (UserSettings.items.driveToken) { + // validate the cached access token; a 401 means it expired/was revoked. + // fetch (not XHR) so this works in the MV3 service worker too. + const res = await fetch("https://www.googleapis.com/drive/v3/files", { + headers: { Authorization: "Bearer " + UserSettings.items.driveToken }, + }); + if (res.status === 401) { + UserSettings.items.driveToken = undefined; + UserSettings.commitItems(); + needsRefresh = true; + } + } + if (needsRefresh) { await this.refreshToken(); } return UserSettings.items.driveToken; @@ -169,80 +135,44 @@ export class Drive implements BackupProvider { private async refreshToken() { await UserSettings.updateItems(); - - if ( - navigator.userAgent.indexOf("Chrome") !== -1 && - navigator.userAgent.indexOf("OPR") === -1 && - navigator.userAgent.indexOf("Edg") === -1 - ) { - return new Promise((resolve: (value: boolean) => void) => { - return chrome.identity.getAuthToken( - { - interactive: false, - scopes: ["https://www.googleapis.com/auth/drive.file"], - }, - (token) => { - UserSettings.items.driveToken = token; - if (!token) { - UserSettings.items.driveRevoked = true; - } - UserSettings.commitItems(); - resolve(Boolean(token)); - } - ); - }); - } else { - return new Promise( - ( - resolve: (value: boolean) => void, - reject: (reason: Error) => void - ) => { - const xhr = new XMLHttpRequest(); - xhr.open( - "POST", - "https://www.googleapis.com/oauth2/v4/token?client_id=" + - getCredentials().drive.client_id + - "&client_secret=" + - getCredentials().drive.client_secret + - "&refresh_token=" + - UserSettings.items.driveRefreshToken + - "&grant_type=refresh_token" - ); - xhr.setRequestHeader("Accept", "application/json"); - xhr.onreadystatechange = () => { - if (xhr.readyState === 4) { - if (xhr.status === 401) { - UserSettings.items.driveRefreshToken = undefined; - UserSettings.items.driveRevoked = true; - UserSettings.commitItems(); - return resolve(false); - } - try { - const res = JSON.parse(xhr.responseText); - if (res.error) { - if (res.error === "invalid_grant") { - UserSettings.items.driveRefreshToken = undefined; - UserSettings.items.driveRevoked = true; - UserSettings.commitItems(); - } - console.error(res.error_description); - resolve(false); - } else { - UserSettings.items.driveToken = res.access_token; - UserSettings.commitItems(); - resolve(true); - } - } catch (error) { - console.error(error); - reject(error as Error); - } - } - return; - }; - xhr.send(); - } - ); + if (!UserSettings.items.driveRefreshToken) { + UserSettings.items.driveRevoked = true; + UserSettings.commitItems(); + return; + } + // Refresh-token flow via fetch. getAuthToken is no longer usable (Google + // blocks custom-URI-scheme OAuth clients for new apps) and fetch works in + // both the popup and the MV3 service worker (XHR is undefined there). + const res = await fetch("https://www.googleapis.com/oauth2/v4/token", { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/x-www-form-urlencoded", + }, + body: + `client_id=${getCredentials().drive.client_id}` + + `&client_secret=${getCredentials().drive.client_secret}` + + `&refresh_token=${UserSettings.items.driveRefreshToken}` + + `&grant_type=refresh_token`, + }); + if (res.status === 401) { + UserSettings.items.driveRefreshToken = undefined; + UserSettings.items.driveRevoked = true; + UserSettings.commitItems(); + return; + } + const data = await res.json(); + if (data.error) { + if (data.error === "invalid_grant") { + UserSettings.items.driveRefreshToken = undefined; + UserSettings.items.driveRevoked = true; + UserSettings.commitItems(); + } + console.error(data.error_description); + return; } + UserSettings.items.driveToken = data.access_token; + UserSettings.commitItems(); } private async getFolder() { @@ -252,105 +182,70 @@ export class Drive implements BackupProvider { } await UserSettings.updateItems(); if (UserSettings.items.driveFolder) { - await new Promise( - ( - resolve: (value: boolean) => void, - reject: (reason: Error) => void - ) => { - const xhr = new XMLHttpRequest(); - xhr.open( - "GET", - "https://www.googleapis.com/drive/v3/files/" + - UserSettings.items.driveFolder + - "?fields=trashed" - ); - xhr.setRequestHeader("Authorization", "Bearer " + token); - xhr.setRequestHeader("Accept", "application/json"); - xhr.onreadystatechange = () => { - if (xhr.readyState === 4) { - if (xhr.status === 401) { - UserSettings.items.driveToken = undefined; - UserSettings.commitItems(); - return resolve(false); - } - try { - const res = JSON.parse(xhr.responseText); - if (res.error) { - if (res.error.code === 404) { - UserSettings.items.driveFolder = undefined; - UserSettings.commitItems(); - resolve(true); - } else { - console.error(res.error.message); - resolve(false); - } - } else if (res.trashed) { - UserSettings.items.driveFolder = undefined; - UserSettings.commitItems(); - resolve(true); - } else { - resolve(true); - } - } catch (error) { - console.error(error); - reject(error as Error); - } - } - return; - }; - xhr.send(); + const res = await fetch( + "https://www.googleapis.com/drive/v3/files/" + + UserSettings.items.driveFolder + + "?fields=trashed", + { + headers: { + Authorization: "Bearer " + token, + Accept: "application/json", + }, } ); + if (res.status === 401) { + UserSettings.items.driveToken = undefined; + UserSettings.commitItems(); + return false; + } + const data = await res.json(); + if (data.error) { + if (data.error.code === 404) { + UserSettings.items.driveFolder = undefined; + UserSettings.commitItems(); + } else { + console.error(data.error.message); + return false; + } + } else if (data.trashed) { + UserSettings.items.driveFolder = undefined; + UserSettings.commitItems(); + } } if (!UserSettings.items.driveFolder) { - await new Promise( - ( - resolve: (value: boolean) => void, - reject: (reason: Error) => void - ) => { - // create folder - const xhr = new XMLHttpRequest(); - xhr.open("POST", "https://www.googleapis.com/drive/v3/files/"); - xhr.setRequestHeader("Authorization", "Bearer " + token); - xhr.setRequestHeader("Accept", "application/json"); - xhr.setRequestHeader("Content-Type", "application/json"); - xhr.onreadystatechange = () => { - if (xhr.readyState === 4) { - if (xhr.status === 401) { - UserSettings.items.driveToken = undefined; - UserSettings.commitItems(); - return resolve(false); - } - try { - const res = JSON.parse(xhr.responseText); - if (!res.error) { - UserSettings.items.driveFolder = res.id; - UserSettings.commitItems(); - resolve(true); - } else { - console.error(res.error.message); - resolve(false); - } - } catch (error) { - console.error(error); - reject(error as Error); - } - } - return; - }; - xhr.send( - JSON.stringify({ - name: "Authenticator Backups", - mimeType: "application/vnd.google-apps.folder", - }) - ); - } - ); + const res = await fetch("https://www.googleapis.com/drive/v3/files/", { + method: "POST", + headers: { + Authorization: "Bearer " + token, + Accept: "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name: "Authenticator Backups", + mimeType: "application/vnd.google-apps.folder", + }), + }); + if (res.status === 401) { + UserSettings.items.driveToken = undefined; + UserSettings.commitItems(); + return false; + } + const data = await res.json(); + if (!data.error) { + UserSettings.items.driveFolder = data.id; + UserSettings.commitItems(); + } else { + console.error(data.error.message); + return false; + } } return UserSettings.items.driveFolder; } async upload(encryption: Encryption) { + if (!cloudBackupAllowed(encryption)) { + return false; + } await UserSettings.updateItems(); if (UserSettings.items.driveEncrypted === undefined) { UserSettings.items.driveEncrypted = true; @@ -367,69 +262,51 @@ export class Drive implements BackupProvider { return false; } const folderId = await this.getFolder(); - return new Promise( - (resolve: (value: boolean) => void, reject: (reason: Error) => void) => { - if (!token || !folderId) { - return resolve(false); - } - try { - const xhr = new XMLHttpRequest(); - const now = new Date().toISOString().slice(0, 10).replace(/-/g, ""); - xhr.open( - "POST", - "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart" - ); - xhr.setRequestHeader("Authorization", "Bearer " + token); - xhr.setRequestHeader( - "Content-type", - "multipart/related; boundary=segment_marker" - ); - xhr.onreadystatechange = () => { - if (xhr.readyState === 4) { - if (xhr.status === 401) { - UserSettings.items.driveToken = undefined; - UserSettings.commitItems(); - return resolve(false); - } - try { - const res = JSON.parse(xhr.responseText); - if (!res.error) { - resolve(true); - } else { - console.error(res.error.message); - resolve(false); - } - } catch (error) { - reject(error as Error); - } - } - return; - }; - const requestDataPrototype = [ - "--segment_marker", - "Content-Type: application/json; charset=UTF-8", - "", - JSON.stringify({ - name: `${now}.json`, - parents: [UserSettings.items.driveFolder], - }), - "", - "--segment_marker", - "Content-Type: application/octet-stream", - "", - backup, - "--segment_marker--", - ]; - let requestData = ""; - requestDataPrototype.forEach((line) => { - requestData = requestData + line + "\n"; - }); - xhr.send(requestData); - } catch (error) { - return reject(error as Error); - } + if (!folderId) { + return false; + } + const now = new Date().toISOString().slice(0, 10).replace(/-/g, ""); + const requestData = + [ + "--segment_marker", + "Content-Type: application/json; charset=UTF-8", + "", + JSON.stringify({ name: `${now}.json`, parents: [folderId] }), + "", + "--segment_marker", + "Content-Type: application/octet-stream", + "", + backup, + "--segment_marker--", + ].join("\n") + "\n"; + // fetch (not XMLHttpRequest) because upload runs in the MV3 service worker. + const res = await fetch( + "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart", + { + method: "POST", + headers: { + Authorization: "Bearer " + token, + "Content-Type": "multipart/related; boundary=segment_marker", + }, + body: requestData, } ); + if (res.status === 401) { + UserSettings.items.driveToken = undefined; + UserSettings.items.driveRevoked = true; + UserSettings.commitItems(); + return false; + } + if (!res.ok) { + // a non-2xx is a failed upload, not something to JSON.parse as success + return false; + } + const data = await res.json(); + if (!data.error) { + return true; + } + console.error(data.error.message); + return false; } async getUser() { @@ -452,8 +329,9 @@ export class Drive implements BackupProvider { UserSettings.items.driveToken = undefined; UserSettings.commitItems(); resolve( - "Error: Response was 401. You will be logged out the next time you open Authenticator." + "Error: Response was 401. You will be logged out the next time you open OTPilot." ); + return; } try { const res = JSON.parse(xhr.responseText); @@ -503,6 +381,8 @@ export class OneDrive implements BackupProvider { UserSettings.items.oneDriveToken = undefined; UserSettings.commitItems(); resolve(true); + } else { + resolve(false); } } else { resolve(false); @@ -580,9 +460,13 @@ export class OneDrive implements BackupProvider { } async upload(encryption: Encryption) { + if (!cloudBackupAllowed(encryption)) { + return false; + } await UserSettings.updateItems(); if (UserSettings.items.oneDriveEncrypted === undefined) { UserSettings.items.oneDriveEncrypted = true; + UserSettings.commitItems(); } const exportData = await EntryStorage.backupGetExport( encryption, @@ -612,7 +496,14 @@ export class OneDrive implements BackupProvider { xhr.onreadystatechange = () => { if (xhr.readyState === 4) { if (xhr.status === 401) { - UserSettings.removeItem("oneDriveToken"); + UserSettings.items.oneDriveToken = undefined; + UserSettings.items.oneDriveRevoked = true; + UserSettings.commitItems(); + return resolve(false); + } + if (xhr.status < 200 || xhr.status >= 300) { + // a non-2xx is a failed upload; don't fall through and risk + // misreading the body as success return resolve(false); } try { @@ -655,8 +546,9 @@ export class OneDrive implements BackupProvider { UserSettings.items.oneDriveToken = undefined; UserSettings.commitItems(); resolve( - "Error: Response was 401. You will be logged out the next time you open Authenticator." + "Error: Response was 401. You will be logged out the next time you open OTPilot." ); + return; } try { const res = JSON.parse(xhr.responseText); diff --git a/src/models/credentials.ts b/src/models/credentials.ts index 4c16f5621..d5c1b4e69 100644 --- a/src/models/credentials.ts +++ b/src/models/credentials.ts @@ -5,7 +5,7 @@ export function getCredentials() { client_secret: "", // Google client secret }, dropbox: { - client_id: "", // Dropbox client ID + client_id: "lmii0f2hnnztmvv", // Dropbox client ID }, onedrive: { client_id: "", // Microsoft Identity client ID diff --git a/src/models/encryption.ts b/src/models/encryption.ts index ebca10a85..aa074e06c 100644 --- a/src/models/encryption.ts +++ b/src/models/encryption.ts @@ -1,68 +1,150 @@ import * as CryptoJS from "crypto-js"; +// New ciphertext is authenticated AES-GCM via WebCrypto, tagged with this +// prefix so it is self-describing. Legacy ciphertext is crypto-js AES-CBC +// (OpenSSL "Salted__" framing, base64 starting "U2FsdGVkX1"); it stays readable +// for old storage and backups and upgrades to GCM the next time it is written. +const GCM_PREFIX = "v4:"; + +function bytesToBase64(bytes: Uint8Array): string { + let binary = ""; + for (let i = 0; i < bytes.length; i++) { + binary += String.fromCharCode(bytes[i]); + } + return btoa(binary); +} + +function base64ToBytes(base64: string): Uint8Array { + const binary = atob(base64); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i); + } + return bytes; +} + +async function deriveKey(password: string): Promise { + const raw = await crypto.subtle.digest( + "SHA-256", + new TextEncoder().encode(password) + ); + return crypto.subtle.importKey("raw", raw, "AES-GCM", false, [ + "encrypt", + "decrypt", + ]); +} + +// Prefix-aware decrypt shared by the Encryption class and the backup importer. +// GCM throws on a wrong key / tampered data (auth tag); we surface that as null +// like the legacy path. Returns null on any failure. +export async function decryptString( + data: string, + password: string +): Promise { + if (data.startsWith(GCM_PREFIX)) { + try { + const combined = base64ToBytes(data.slice(GCM_PREFIX.length)); + const iv = combined.slice(0, 12); + const ciphertext = combined.slice(12); + const key = await deriveKey(password); + const plaintext = await crypto.subtle.decrypt( + { name: "AES-GCM", iv }, + key, + ciphertext + ); + return new TextDecoder().decode(plaintext); + } catch (error) { + return null; + } + } + // legacy AES-CBC (crypto-js) + try { + const decrypted = CryptoJS.AES.decrypt(data, password).toString( + CryptoJS.enc.Utf8 + ); + return decrypted || null; + } catch (error) { + return null; + } +} + export class Encryption implements EncryptionInterface { private password: string; private keyId: string; + private keyPromise?: Promise; constructor(hash: string, keyId: string) { this.password = hash; this.keyId = keyId; + // Derive eagerly. These instances get stored in the reactive Vuex store + // (state.encryption / entry.encryption); if getKey() lazily assigned + // this.keyPromise later, that write would mutate reactive state during an + // await -- outside a mutation handler -- and trip Vuex strict mode. + if (hash) { + this.keyPromise = deriveKey(hash); + } } - getEncryptedString(data: string): string { + // Derive a 256-bit AES-GCM key from the (high-entropy argon2) saltedHash. + // Cached per instance. + private getKey(): Promise { + if (!this.keyPromise) { + this.keyPromise = deriveKey(this.password); + } + return this.keyPromise; + } + + async getEncryptedString(data: string): Promise { if (!this.password) { return data; - } else { - return CryptoJS.AES.encrypt(data, this.password).toString(); } + const key = await this.getKey(); + const iv = crypto.getRandomValues(new Uint8Array(12)); + const ciphertext = await crypto.subtle.encrypt( + { name: "AES-GCM", iv }, + key, + new TextEncoder().encode(data) + ); + const combined = new Uint8Array(iv.length + ciphertext.byteLength); + combined.set(iv, 0); + combined.set(new Uint8Array(ciphertext), iv.length); + return GCM_PREFIX + bytesToBase64(combined); } - decryptSecretString(secret: string) { - try { - const decryptedSecret = CryptoJS.AES.decrypt( - secret, - this.password - ).toString(CryptoJS.enc.Utf8); - - if (!decryptedSecret) { - return null; - } - - if (decryptedSecret.length < 8) { - return null; - } - - if ( - !/^[a-z2-7]+=*$/i.test(decryptedSecret) && - !/^[0-9a-f]+$/i.test(decryptedSecret) && - !/^blz-/.test(decryptedSecret) && - !/^bliz-/.test(decryptedSecret) && - !/^stm-/.test(decryptedSecret) - ) { - return null; - } - - return decryptedSecret; - } catch (error) { + async decryptSecretString(secret: string): Promise { + const decryptedSecret = await decryptString(secret, this.password); + if (!decryptedSecret) { return null; } - } - decryptEncSecret(entry: OTPEntryInterface) { - try { - if (!entry.encData) { - return null; - } + if (decryptedSecret.length < 8) { + return null; + } - const decryptedData = CryptoJS.AES.decrypt( - entry.encData, - this.password - ).toString(CryptoJS.enc.Utf8); + if ( + !/^[a-z2-7]+=*$/i.test(decryptedSecret) && + !/^[0-9a-f]+$/i.test(decryptedSecret) && + !/^blz-/.test(decryptedSecret) && + !/^bliz-/.test(decryptedSecret) && + !/^stm-/.test(decryptedSecret) + ) { + return null; + } - if (!decryptedData) { - return null; - } + return decryptedSecret; + } + async decryptEncSecret( + entry: OTPEntryInterface + ): Promise { + if (!entry.encData) { + return null; + } + const decryptedData = await decryptString(entry.encData, this.password); + if (!decryptedData) { + return null; + } + try { return JSON.parse(decryptedData); } catch (error) { return null; @@ -70,11 +152,12 @@ export class Encryption implements EncryptionInterface { } getEncryptionStatus(): boolean { - return this.password ? true : false; + return Boolean(this.password); } updateEncryptionPassword(password: string) { this.password = password; + this.keyPromise = undefined; } setEncryptionKeyId(id: string): void { diff --git a/src/models/key-utilities.ts b/src/models/key-utilities.ts index 31bcb0ba5..2b2c4f819 100644 --- a/src/models/key-utilities.ts +++ b/src/models/key-utilities.ts @@ -21,21 +21,6 @@ export class KeyUtilities { return Number(`0x${s}`); } - private static hex2str(hex: string) { - let str = ""; - for (let i = 0; i < hex.length; i += 2) { - str += String.fromCharCode(this.hex2dec(hex.substr(i, 2))); - } - return str; - } - - private static leftpad(str: string, len: number, pad: string): string { - if (len + 1 >= str.length) { - str = new Array(len + 1 - str.length).join(pad) + str; - } - return str; - } - private static base32tohex(base32: string): string { const base32chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; let bits = ""; @@ -48,7 +33,7 @@ export class KeyUtilities { padding++; } else { const val = base32chars.indexOf(base32.charAt(i).toUpperCase()); - bits += this.leftpad(val.toString(2), 5, "0"); + bits += val.toString(2).padStart(5, "0"); } } @@ -167,7 +152,7 @@ export class KeyUtilities { counter = Math.floor(epoch / period); } - const time = this.leftpad(this.dec2hex(counter), 16, "0"); + const time = this.dec2hex(counter).padStart(16, "0"); if (key.length % 2 === 1) { if (key.substr(-1) === "0") { diff --git a/src/models/migration.ts b/src/models/migration.ts index 9d00c63e8..4a222fe2d 100644 --- a/src/models/migration.ts +++ b/src/models/migration.ts @@ -85,7 +85,19 @@ export function getOTPAuthPerLineFromOPTAuthMigration(migrationUri: string) { return []; } - const base64Data = decodeURIComponent(migrationUri.split("data=")[1]); + const dataPart = migrationUri.split("data=")[1]; + if (!dataPart) { + return []; + } + + let base64Data: string; + try { + base64Data = decodeURIComponent(dataPart); + } catch { + // malformed percent-encoding in the migration URI + return []; + } + const wordArrayData = CryptoJS.enc.Base64.parse(base64Data); const byteData = wordArrayToByteArray(wordArrayData); const lines: string[] = []; @@ -94,31 +106,72 @@ export function getOTPAuthPerLineFromOPTAuthMigration(migrationUri: string) { if (byteData[offset] !== 10) { break; } + // Every length byte below is attacker-controlled; validate that each field + // stays inside the buffer before reading, so truncated/garbage payloads + // can't fabricate entries from out-of-bounds (undefined) bytes. + if (offset + 4 > byteData.length) { + break; + } const lineLength = byteData[offset + 1]; const secretStart = offset + 4; const secretLength = byteData[offset + 3]; + const secretEnd = secretStart + secretLength; + if (secretEnd + 2 > byteData.length) { + break; + } + const accountStart = secretEnd + 2; + const accountLength = byteData[secretEnd + 1]; + const accountEnd = accountStart + accountLength; + if (accountEnd + 2 > byteData.length) { + break; + } + const isserStart = accountEnd + 2; + const isserLength = byteData[accountEnd + 1]; + const isserEnd = isserStart + isserLength; + // need bytes up to the type field (isserEnd + 5) + if (isserEnd + 5 >= byteData.length) { + break; + } + const secretBytes = subBytesArray(byteData, secretStart, secretLength); const secret = byteArray2Base32(secretBytes); - const accountStart = secretStart + secretLength + 2; - const accountLength = byteData[secretStart + secretLength + 1]; const accountBytes = subBytesArray(byteData, accountStart, accountLength); const account = byteArray2String(accountBytes); - const isserStart = accountStart + accountLength + 2; - const isserLength = byteData[accountStart + accountLength + 1]; const issuerBytes = subBytesArray(byteData, isserStart, isserLength); const issuer = byteArray2String(issuerBytes); - const algorithm = ["SHA1", "SHA1", "SHA256", "SHA512", "MD5"][ - byteData[isserStart + isserLength + 1] - ]; - const digits = [6, 6, 8][byteData[isserStart + isserLength + 3]]; - const type = ["totp", "hotp", "totp"][ - byteData[isserStart + isserLength + 5] + // index 4 is MD5, which KeyUtilities cannot generate (it would fall back + // to SHA1 and emit wrong codes); map it to undefined so the entry is + // skipped below instead of imported silently. + const algorithm = ["SHA1", "SHA1", "SHA256", "SHA512", undefined][ + byteData[isserEnd + 1] ]; + const digits = [6, 6, 8][byteData[isserEnd + 3]]; + const type = ["totp", "hotp", "totp"][byteData[isserEnd + 5]]; + + // Skip rather than emit otpauth://undefined/...&algorithm=undefined, which + // would silently import an entry that generates wrong codes. + if ( + !secret || + algorithm === undefined || + digits === undefined || + type === undefined + ) { + offset += lineLength + 2; + continue; + } + let line = `otpauth://${type}/${account}?secret=${secret}&issuer=${issuer}&algorithm=${algorithm}&digits=${digits}`; if (type === "hotp") { let counter = 1; - if (isserStart + isserLength + 7 <= lineLength) { - counter = byteData[isserStart + isserLength + 7]; + // counter byte must sit inside this entry ([offset, offset+lineLength+2)) + // and inside the buffer; the old `<= lineLength` compared an absolute + // index against a relative length, so it never read the counter for any + // entry after the first and could read past the buffer on the first. + if ( + isserEnd + 7 < offset + lineLength + 2 && + isserEnd + 7 < byteData.length + ) { + counter = byteData[isserEnd + 7]; } line += `&counter=${counter}`; } diff --git a/src/models/otp.ts b/src/models/otp.ts index cdfa38dba..747e6662d 100644 --- a/src/models/otp.ts +++ b/src/models/otp.ts @@ -47,10 +47,28 @@ export class OTPUtil { } } +// Upstream encoded an autofill-bound host inside the issuer field as +// "Name::host". Split that into the dedicated host field on read so old +// entries keep working and get normalized on the next save. +function migrateLegacyHost( + issuer: string, + host: string +): { issuer: string; host: string } { + if (!host && issuer.includes("::")) { + const parts = issuer.split("::"); + return { + issuer: parts[0], + host: (parts[1] || "").replace(/^\.+/, "").toLowerCase(), + }; + } + return { issuer, host }; +} + export class OTPEntry implements OTPEntryInterface { type: OTPType; index: number; issuer: string; + host: string; secret: string | null; account: string; hash: string; @@ -63,7 +81,7 @@ export class OTPEntry implements OTPEntryInterface { encData?: string; encSecret?: string; keyId?: string; - code = "••••••"; + code = "••••••"; constructor( entry: @@ -72,6 +90,7 @@ export class OTPEntry implements OTPEntryInterface { encrypted: boolean; index: number; issuer?: string; + host?: string; secret: string; type: OTPType; counter?: number; @@ -102,6 +121,7 @@ export class OTPEntry implements OTPEntryInterface { // defaults this.type = OTPType.totp; this.issuer = ""; + this.host = ""; this.account = ""; this.counter = 0; this.period = 30; @@ -123,6 +143,11 @@ export class OTPEntry implements OTPEntryInterface { } else { this.issuer = ""; } + { + const migrated = migrateLegacyHost(this.issuer, entry.host || ""); + this.issuer = migrated.issuer; + this.host = migrated.host; + } if (entry.account) { this.account = entry.account; } else { @@ -182,14 +207,14 @@ export class OTPEntry implements OTPEntryInterface { return; } - applyEncryption(encryption: EncryptionInterface) { + async applyEncryption(encryption: EncryptionInterface) { if (!encryption || !encryption.getEncryptionStatus()) { return; } if (this.encSecret) { // v2 encryption - this.secret = encryption.decryptSecretString(this.encSecret); + this.secret = await encryption.decryptSecretString(this.encSecret); if (this.secret) { this.encSecret = ""; } @@ -197,7 +222,7 @@ export class OTPEntry implements OTPEntryInterface { } // check if its a rawotpstorage - const decryptedData = encryption.decryptEncSecret(this); + const decryptedData = await encryption.decryptEncSecret(this); if (decryptedData === null) { return; } @@ -220,6 +245,12 @@ export class OTPEntry implements OTPEntryInterface { this.counter = decryptedData.counter || 0; this.digits = decryptedData.digits || 6; this.issuer = decryptedData.issuer || ""; + this.host = decryptedData.host || ""; + { + const migrated = migrateLegacyHost(this.issuer, this.host); + this.issuer = migrated.issuer; + this.host = migrated.host; + } this.period = decryptedData.period || 30; this.pinned = decryptedData.pinned || false; this.secret = decryptedData.secret; @@ -256,16 +287,17 @@ export class OTPEntry implements OTPEntryInterface { } generate() { - const offset = UserSettings.items ? UserSettings.items.offset : 0; if (!UserSettings.items) { - // browser storage is async, so we need to wait for it to load - // and re-generate the code - // don't change the code to async, it will break the mutation - // for Accounts store to export data + // browser storage is async, so wait for it to load and re-generate. + // don't change this to async: it would break the Accounts store + // mutation that exports data. Return so we don't first generate a + // code with offset 0 and then race the reload over it. UserSettings.updateItems().then(() => { this.generate(); }); + return; } + const offset = UserSettings.items.offset; if (!this.secret && !this.encData) { this.code = CodeState.Invalid; diff --git a/src/models/password.ts b/src/models/password.ts index 2d3bb3102..4ca2fb06e 100644 --- a/src/models/password.ts +++ b/src/models/password.ts @@ -1,55 +1,58 @@ import { BrowserStorage, isOldKey } from "./storage"; -export async function argonHash( - value: string, - salt: string -): Promise { - const iframe = document.getElementById("argon-sandbox"); - const message = { - action: "hash", - value, - salt, - }; - - if (!iframe) { +// Single argon-sandbox round-trip. Every request carries a unique id; the +// listener only accepts the reply that comes from our sandbox iframe and +// matches that id, then removes itself. This replaces the old per-call +// anonymous listeners that were never removed and resolved on whatever message +// arrived first (a correctness hazard when calls overlapped). A timeout turns a +// hung sandbox into a rejection instead of an indefinite wait. +function callArgonSandbox(message: { + action: "hash" | "verify"; + [key: string]: unknown; +}): Promise { + const iframe = document.getElementById( + "argon-sandbox" + ) as HTMLIFrameElement | null; + if (!iframe || !iframe.contentWindow) { throw new Error("argon-sandbox missing!"); } + const sandbox = iframe.contentWindow; + const id = crypto.randomUUID(); - const argonPromise: Promise = new Promise((resolve) => { - window.addEventListener("message", (response) => { - resolve(response.data.response); - }); - // @ts-expect-error bad typings - iframe.contentWindow.postMessage(message, "*"); + return new Promise((resolve, reject) => { + const handler = (event: MessageEvent) => { + if (event.source !== sandbox || !event.data || event.data.id !== id) { + return; + } + window.removeEventListener("message", handler); + clearTimeout(timer); + resolve(event.data.response); + }; + const timer = setTimeout(() => { + window.removeEventListener("message", handler); + reject(new Error("argon2 sandbox timed out")); + }, 60000); + window.addEventListener("message", handler); + // The sandbox iframe has an opaque origin, so "*" is required to post to it; + // the event.source check above is what authenticates the reply. + sandbox.postMessage({ ...message, id }, "*"); }); +} - return argonPromise; +export async function argonHash( + value: string, + salt: string +): Promise { + return (await callArgonSandbox({ action: "hash", value, salt })) as + | string + | undefined; } export async function argonVerify( value: string, hash: string ): Promise { - const iframe = document.getElementById("argon-sandbox"); - const message = { - action: "verify", - value, - hash, - }; - - if (!iframe) { - throw new Error("argon-sandbox missing!"); - } - - const argonPromise: Promise = new Promise((resolve) => { - window.addEventListener("message", (response) => { - resolve(response.data.response); - }); - // @ts-expect-error bad typings - iframe.contentWindow.postMessage(message, "*"); - }); - - return argonPromise; + return (await callArgonSandbox({ action: "verify", value, hash })) as boolean; } // Verify a password using keys in BrowserStorage diff --git a/src/models/settings.ts b/src/models/settings.ts index aac7a84b0..6d71fbd16 100644 --- a/src/models/settings.ts +++ b/src/models/settings.ts @@ -28,6 +28,7 @@ interface UserSettingsData { autolock?: number; enableContextMenu?: boolean; encodedPhrase?: string; + onboardingComplete?: boolean; smartFilter?: boolean; theme?: string; zoom?: number; @@ -184,6 +185,7 @@ type BooleanOption = | "oneDriveBusiness" | "oneDriveEncrypted" | "oneDriveRevoked" + | "onboardingComplete" | "smartFilter"; type NumberOption = "autolock" | "lastRemindingBackupTime" | "offset" | "zoom"; @@ -201,6 +203,7 @@ function isBooleanOption(key: string): key is BooleanOption { "oneDriveBusiness", "oneDriveEncrypted", "oneDriveRevoked", + "onboardingComplete", "smartFilter", ].includes(key); } diff --git a/src/models/storage.ts b/src/models/storage.ts index 4b3699fb2..0c183052a 100644 --- a/src/models/storage.ts +++ b/src/models/storage.ts @@ -63,7 +63,6 @@ export class BrowserStorage { } } - // TODO: promise this static async get() { const storageLocation = await this.getStorageLocation(); const removeOtherData = function (items: Record): void { @@ -129,10 +128,22 @@ export class BrowserStorage { static async set(data: object) { const storageLocation = await this.getStorageLocation(); - if (storageLocation === StorageLocation.Local) { - await chrome.storage.local.set(data); - } else if (storageLocation === StorageLocation.Sync) { - await chrome.storage.sync.set(data); + try { + if (storageLocation === StorageLocation.Local) { + await chrome.storage.local.set(data); + } else if (storageLocation === StorageLocation.Sync) { + await chrome.storage.sync.set(data); + } + } catch (error) { + // Usually the sync quota (MAX_ITEMS / QUOTA_BYTES_PER_ITEM). Re-throw with + // a clear message instead of failing as a silent unhandled rejection that + // leaves the entry visible in the UI but never persisted. + console.error("Storage write failed", storageLocation, error); + throw new Error( + storageLocation === StorageLocation.Sync + ? "Browser sync storage is full. Switch to local storage in Preferences." + : "Storage write failed: " + String(error) + ); } return; } @@ -192,10 +203,10 @@ function isKey(key: unknown): key is Key { } export class EntryStorage { - private static getOTPStorageFromEntry( + private static async getOTPStorageFromEntry( entry: OTPEntry, unencrypted?: boolean - ): OTPStorage { + ): Promise { let secret: string; if (!entry.secret && entry.encData && entry.keyId) { return { @@ -242,6 +253,10 @@ export class EntryStorage { storageItem.issuer = entry.issuer; } + if (entry.host) { + storageItem.host = entry.host; + } + if (entry.account) { storageItem.account = entry.account; } @@ -266,7 +281,7 @@ export class EntryStorage { entry.encryption?.getEncryptionStatus() && entry.encryption.getEncryptionKeyId() ) { - const encData = entry.encryption.getEncryptedString( + const encData = await entry.encryption.getEncryptedString( JSON.stringify(storageItem) ); return { @@ -355,7 +370,7 @@ export class EntryStorage { } } - static getExport(data: OTPEntryInterface[], encrypted?: boolean) { + static async getExport(data: OTPEntryInterface[], encrypted?: boolean) { try { const exportData: { [hash: string]: OTPStorage } = {}; for (const entry of data) { @@ -364,7 +379,10 @@ export class EntryStorage { continue; } - exportData[entry.hash] = this.getOTPStorageFromEntry(entry, !encrypted); + exportData[entry.hash] = await this.getOTPStorageFromEntry( + entry, + !encrypted + ); } return exportData; } catch (error) { @@ -407,6 +425,10 @@ export class EntryStorage { delete entry.issuer; } + if (!entry.host) { + delete entry.host; + } + if (!entry.account) { delete entry.account; } @@ -424,7 +446,9 @@ export class EntryStorage { if (!encrypted) { // decrypt the data to export if (entry.encrypted) { - const decryptedSecret = encryption.decryptSecretString(entry.secret); + const decryptedSecret = await encryption.decryptSecretString( + entry.secret + ); if (decryptedSecret !== entry.secret && decryptedSecret !== null) { entry.secret = decryptedSecret; entry.encrypted = false; @@ -463,12 +487,22 @@ export class EntryStorage { continue; } + // type and algorithm are stored as their names (e.g. "hotp", "SHA256"), + // not numbers, so parseInt would yield NaN and silently fall back to the + // default. Map the name back to the enum instead. Fixes imported HOTP / + // Steam entries becoming TOTP (Authenticator-Extension/Authenticator#1292, + // #405) and SHA256/SHA512 reverting to SHA1 (#1442, #1294, #1089). + const typeFromName = OTPType[data[hash].type as keyof typeof OTPType]; const rawAlgorithm = data[hash].algorithm; + const algorithmFromName = rawAlgorithm + ? OTPAlgorithm[rawAlgorithm.toUpperCase() as keyof typeof OTPAlgorithm] + : undefined; const entryData: { account: string; encrypted: false; index: number; issuer: string; + host: string; secret: string; type: OTPType; counter: number; @@ -478,18 +512,20 @@ export class EntryStorage { algorithm: OTPAlgorithm; pinned: boolean; } = { - type: (parseInt(data[hash].type) as OTPType) || OTPType[OTPType.totp], + type: typeof typeFromName === "number" ? typeFromName : OTPType.totp, index: data[hash].index || 0, issuer: data[hash].issuer || "", + host: data[hash].host || "", account: data[hash].account || "", encrypted: false, secret: data[hash].secret, counter: data[hash].counter || 0, period: data[hash].period || 30, digits: data[hash].digits || 6, - algorithm: rawAlgorithm - ? (parseInt(rawAlgorithm) as OTPAlgorithm) - : OTPAlgorithm.SHA1, + algorithm: + typeof algorithmFromName === "number" + ? algorithmFromName + : OTPAlgorithm.SHA1, pinned: data[hash].pinned || false, hash: data[hash].hash || hash, }; @@ -551,7 +587,7 @@ export class EntryStorage { } const entry = new OTPEntry(entryData, encryption); - _data[entryData.hash] = this.getOTPStorageFromEntry(entry); + _data[entryData.hash] = await this.getOTPStorageFromEntry(entry); } _data = this.ensureUniqueIndex(_data); await BrowserStorage.set(_data); @@ -559,7 +595,7 @@ export class EntryStorage { static async add(entry: OTPEntry) { await BrowserStorage.set({ - [entry.hash]: this.getOTPStorageFromEntry(entry), + [entry.hash]: await this.getOTPStorageFromEntry(entry), }); } @@ -568,7 +604,7 @@ export class EntryStorage { if (!Object.prototype.hasOwnProperty.call(_data, entry.hash)) { throw new Error("Entry to change does not exist."); } - const storageItem = this.getOTPStorageFromEntry(entry); + const storageItem = await this.getOTPStorageFromEntry(entry); _data[entry.hash] = storageItem; _data = this.ensureUniqueIndex(_data); await BrowserStorage.set(_data); @@ -576,10 +612,10 @@ export class EntryStorage { static async set(entries: OTPEntry[]) { let _data = await BrowserStorage.get(); - entries.forEach((entry) => { - const storageItem = this.getOTPStorageFromEntry(entry); + for (const entry of entries) { + const storageItem = await this.getOTPStorageFromEntry(entry); _data[entry.hash] = storageItem; - }); + } _data = this.ensureUniqueIndex(_data); await BrowserStorage.set(_data); } @@ -652,6 +688,7 @@ export class EntryStorage { hash: entryData.hash, index: entryData.index, issuer: entryData.issuer, + host: entryData.host, secret: entryData.secret, type, counter: entryData.counter, diff --git a/src/options.ts b/src/options.ts index f9cc047e7..b7fd31701 100644 --- a/src/options.ts +++ b/src/options.ts @@ -1,14 +1,12 @@ -import Vue from "vue"; +import { createApp } from "vue"; import OptionsView from "./components/Options.vue"; import { loadI18nMessages } from "./store/i18n"; async function init() { + const app = createApp(OptionsView); // i18n - Vue.prototype.i18n = await loadI18nMessages(); - - new Vue({ - render: (h) => h(OptionsView), - }).$mount("#options"); + app.config.globalProperties.i18n = await loadI18nMessages(); + app.mount("#options"); } init(); diff --git a/src/permissions.ts b/src/permissions.ts index d0636485c..373bd2451 100644 --- a/src/permissions.ts +++ b/src/permissions.ts @@ -1,6 +1,6 @@ // Vue -import Vue from "vue"; -import Vuex from "vuex"; +import { createApp } from "vue"; +import { createStore } from "vuex"; // Components import PermissionsView from "./components/Permissions.vue"; @@ -11,28 +11,24 @@ import { loadI18nMessages } from "./store/i18n"; import { Permissions } from "./store/Permissions"; async function init() { - // i18n - Vue.prototype.i18n = await loadI18nMessages(); - - // Load modules - Vue.use(Vuex); - - // Load common components globally - for (const component of CommonComponents) { - Vue.component(component.name, component.component); - } - // State - const store = new Vuex.Store({ + const store = createStore({ + strict: process.env.NODE_ENV !== "production", modules: { permissions: await new Permissions().getModule(), }, }); - const instance = new Vue({ - render: (h) => h(PermissionsView), - store, - }).$mount("#permissions"); + const app = createApp(PermissionsView); + app.use(store); + // i18n + app.config.globalProperties.i18n = await loadI18nMessages(); + // Load common components globally + for (const component of CommonComponents) { + app.component(component.name, component.component); + } + + const instance = app.mount("#permissions"); // Set title try { diff --git a/src/popup.ts b/src/popup.ts index 173bc033e..819db833f 100644 --- a/src/popup.ts +++ b/src/popup.ts @@ -1,7 +1,6 @@ // Vue -import Vue from "vue"; -import Vuex from "vuex"; -import { Vue2Dragula } from "vue2-dragula"; +import { createApp, h, ComponentPublicInstance } from "vue"; +import { createStore } from "vuex"; // Components import Popup from "./components/Popup.vue"; @@ -34,20 +33,10 @@ async function init() { await migrateLocalStorageToBrowserStorage(); await UserSettings.updateItems(); - // Add globals - Vue.prototype.i18n = await loadI18nMessages(); - - // Load modules - Vue.use(Vuex); - Vue.use(Vue2Dragula); - - // Load common components globally - for (const component of CommonComponents) { - Vue.component(component.name, component.component); - } - // State - const store = new Vuex.Store({ + const store = createStore({ + // catch out-of-mutation state changes during development (no prod cost) + strict: process.env.NODE_ENV !== "production", modules: { accounts: await new Accounts().getModule(), advisor: await new Advisor().getModule(), @@ -61,9 +50,8 @@ async function init() { }); // Render - const instance = new Vue({ - render: (h) => h(Popup), - store, + const app = createApp({ + render: () => h(Popup), mounted() { // Update time based entries' codes this.$store.commit("accounts/updateCodes"); @@ -71,7 +59,17 @@ async function init() { this.$store.commit("accounts/updateCodes"); }, 1000); }, - }).$mount("#authenticator"); + }); + + app.use(store); + // Add globals + app.config.globalProperties.i18n = await loadI18nMessages(); + // Load common components globally + for (const component of CommonComponents) { + app.component(component.name, component.component); + } + + const instance = app.mount("#authenticator"); // Prompt for password if needed if (instance.$store.state.accounts.shouldShowPassphrase) { @@ -147,7 +145,9 @@ async function init() { if (!searchInput || !searchDiv) { return; } - searchDiv.style.display = "block"; + // force-show before Vue re-renders so focus() lands; must match the + // search box's flex layout (display:block would break it — #1) + searchDiv.style.display = "flex"; searchInput.focus(); } }, @@ -200,7 +200,15 @@ async function init() { init(); -async function runScheduledBackup(clientTime: number, instance: Vue) { +async function runScheduledBackup( + clientTime: number, + instance: ComponentPublicInstance +) { + // A scheduled cloud backup without a master password would upload plaintext + // secrets; skip it entirely. The UI prompts the user to set a password first. + if (!instance.$store.state.accounts.defaultEncryption) { + return; + } if (instance.$store.state.backup.dropboxToken) { chrome.permissions.contains( { origins: ["https://*.dropboxapi.com/*"] }, @@ -228,7 +236,8 @@ async function runScheduledBackup(clientTime: number, instance: Vue) { UserSettings.removeItem("dropboxRevoked"); } } catch (error) { - // ignore + // a failed scheduled backup shouldn't be completely silent + console.error("Scheduled backup failed", error); } } instance.$store.commit( @@ -270,7 +279,8 @@ async function runScheduledBackup(clientTime: number, instance: Vue) { UserSettings.removeItem("driveRevoked"); } } catch (error) { - // ignore + // a failed scheduled backup shouldn't be completely silent + console.error("Scheduled backup failed", error); } } instance.$store.commit( @@ -312,7 +322,8 @@ async function runScheduledBackup(clientTime: number, instance: Vue) { UserSettings.removeItem("oneDriveRevoked"); } } catch (error) { - // ignore + // a failed scheduled backup shouldn't be completely silent + console.error("Scheduled backup failed", error); } } instance.$store.commit( diff --git a/src/qrdebug.ts b/src/qrdebug.ts index 713c92def..ebceca1ef 100644 --- a/src/qrdebug.ts +++ b/src/qrdebug.ts @@ -12,9 +12,8 @@ chrome.runtime.onMessage.addListener((message, sender) => { message.info.windowWidth ); } - - // https://stackoverflow.com/a/56483156 - return true; + // no response is sent, so don't return true (which leaves the sender's + // message channel open and rejects it on close) }); function getQrDebug( diff --git a/src/store/Accounts.ts b/src/store/Accounts.ts index 57f6a47e6..bfbf1ced1 100644 --- a/src/store/Accounts.ts +++ b/src/store/Accounts.ts @@ -7,8 +7,11 @@ import { getSiteName, getMatchedEntriesHash } from "../utils"; import { isChromium } from "../browser"; import { StorageLocation, UserSettings } from "../models/settings"; import { DataType } from "../models/otp"; +import { argonHash, argonVerify } from "../models/password"; const LegacyEncryption = "LegacyEncryption"; +// uuidv4 shape; entries whose hash is not a uuidv4 get their hash regenerated +const UUIDV4_REGEX = /[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}/i; export class Accounts implements Module { async getModule() { const cachedKeyInfo = await this.getCachedKeyInfo(); @@ -80,17 +83,20 @@ export class Accounts implements Module { stopFilter(state: AccountsState) { state.filter = false; }, + startFilter(state: AccountsState) { + state.filter = true; + }, showSearch(state: AccountsState) { state.showSearch = true; }, updateCodes(state: AccountsState) { let second = new Date().getSeconds(); if (UserSettings.items.offset) { - // prevent second from negative - second += Number(UserSettings.items.offset) + 60; + second += Number(UserSettings.items.offset); } - second = second % 60; + // positive modulo so any offset (incl. < -60) stays in 0..59 (#1310) + second = ((second % 60) + 60) % 60; state.second = second; let currentlyEncrypted = false; @@ -110,49 +116,35 @@ export class Accounts implements Module { state.sectorOffset = -second; } - // if (second > 25) { - // app.class.timeout = true; - // } else { - // app.class.timeout = false; - // } - // if (second < 1) { - // const entries = app.entries as OTP[]; - // for (let i = 0; i < entries.length; i++) { - // if (entries[i].type !== OTPType.hotp && - // entries[i].type !== OTPType.hhex) { - // entries[i].generate(); - // } - // } - // } - const entries = state.entries as OTPEntryInterface[]; - for (let i = 0; i < entries.length; i++) { - if ( - entries[i].type !== OTPType.hotp && - entries[i].type !== OTPType.hhex - ) { - entries[i].generate(); + for (const entry of state.entries) { + if (entry.type !== OTPType.hotp && entry.type !== OTPType.hhex) { + entry.generate(); } } }, loadCodes(state: AccountsState, newCodes: OTPEntryInterface[]) { state.entries = newCodes; }, - moveCode(state: AccountsState, opts: { from: number; to: number }) { - state.entries.splice( - opts.to, - 0, - state.entries.splice(opts.from, 1)[0] - ); - - for (let i = 0; i < state.entries.length; i++) { - if (state.entries[i].index !== i) { - state.entries[i].index = i; - } - } + // vuedraggable hands back the reordered (pinned-first) displayed array; + // re-index by position and store it. The `entries` getter re-derives + // the pinned-first view from this, so drags within a group stick. + reorderEntries(state: AccountsState, reordered: OTPEntryInterface[]) { + reordered.forEach((entry, i) => { + entry.index = i; + }); + state.entries = reordered; }, pinEntry(state: AccountsState, entry: OTPEntryInterface) { state.entries[entry.index].pinned = !entry.pinned; }, + // Re-anchor the timer-circle animation phase to the current second. + // Reordering (pin/drag) re-attaches the entry's DOM node, which restarts + // its CSS animation; without re-syncing it would resume from the stale + // load-time sectorOffset and the countdown circle would drift until the + // popup is reopened. + resyncSector(state: AccountsState) { + state.sectorOffset = -state.second; + }, updateExport( state: AccountsState, exportData: { [k: string]: OTPEntryInterface } @@ -182,18 +174,59 @@ export class Accounts implements Module { initComplete(state: AccountsState) { state.initComplete = true; }, + // --- mutations routed for Vuex strict mode (state must only change here) + removeEntry(state: AccountsState, hash: string) { + const index = state.entries.findIndex((entry) => entry.hash === hash); + if (index > -1) { + state.entries.splice(index, 1); + } + }, + addEntry(state: AccountsState, entry: OTPEntryInterface) { + state.entries.unshift(entry); + }, + setDefaultEncryption(state: AccountsState, keyId: string) { + state.defaultEncryption = keyId; + }, + setEncryption( + state: AccountsState, + payload: { keyId: string; encryption: EncryptionInterface } + ) { + state.encryption.set(payload.keyId, payload.encryption); + }, + setEntryField( + state: AccountsState, + payload: { + entry: OTPEntryInterface; + field: "issuer" | "account" | "host"; + value: string; + } + ) { + payload.entry[payload.field] = payload.value; + }, + applyEntryEncryption( + state: AccountsState, + payload: { entry: OTPEntryInterface; encryption: EncryptionInterface } + ) { + payload.entry.changeEncryption(payload.encryption); + }, + regenEntryHash(state: AccountsState, entry: OTPEntryInterface) { + entry.genUUID(); + }, + // in-memory part of OTPEntry.next(); persistence (entry.update()) stays + // in the action since mutations must be synchronous + advanceHotpCounter(state: AccountsState, entry: OTPEntryInterface) { + entry.generate(); + if (entry.secret !== null) { + entry.counter++; + } + }, }, actions: { deleteCode: async ( state: ActionContext, hash: string ) => { - const index = state.state.entries.findIndex( - (entry) => entry.hash === hash - ); - if (index > -1) { - state.state.entries.splice(index, 1); - } + state.commit("removeEntry", hash); state.commit( "updateExport", await EntryStorage.getExport(state.state.entries) @@ -207,7 +240,7 @@ export class Accounts implements Module { state: ActionContext, entry: OTPEntryInterface ) => { - state.state.entries.unshift(entry); + state.commit("addEntry", entry); state.commit( "updateExport", await EntryStorage.getExport(state.state.entries) @@ -235,23 +268,7 @@ export class Accounts implements Module { // --- handle v2 encryption // decrypt using key const key = CryptoJS.AES.decrypt(encKeys.enc, password).toString(); - const isCorrectPassword = await new Promise( - (resolve: (value: string) => void) => { - const iframe = document.getElementById("argon-sandbox"); - const message = { - action: "verify", - value: key, - hash: encKeys.hash, - }; - if (iframe) { - window.addEventListener("message", (response) => { - resolve(response.data.response); - }); - // @ts-expect-error - bad typings - iframe.contentWindow.postMessage(message, "*"); - } - } - ); + const isCorrectPassword = await argonVerify(key, encKeys.hash); if (!isCorrectPassword) { state.commit("wrongPassword"); @@ -261,19 +278,19 @@ export class Accounts implements Module { return; } - state.state.encryption.set( - LegacyEncryption, - new Encryption(key, LegacyEncryption) - ); + state.commit("setEncryption", { + keyId: LegacyEncryption, + encryption: new Encryption(key, LegacyEncryption), + }); migrationNeeded = true; } else if (encKeys.length === 0) { // --- handle v1 encryption // verify current password - state.state.encryption.set( - LegacyEncryption, - new Encryption(password, LegacyEncryption) - ); + state.commit("setEncryption", { + keyId: LegacyEncryption, + encryption: new Encryption(password, LegacyEncryption), + }); await state.dispatch("updateEntries"); if (state.getters.currentlyEncrypted) { @@ -289,58 +306,29 @@ export class Accounts implements Module { // --- handle v3 encryption // TODO: let user reconcile multiple keys from sync conflicts for (const key of encKeys) { - const rawHash = await new Promise( - (resolve: (value: string) => void) => { - const iframe = document.getElementById("argon-sandbox"); - const message = { - action: "hash", - value: password, - salt: key.salt, - }; - if (iframe) { - window.addEventListener("message", (response) => { - resolve(response.data.response); - }); - // @ts-expect-error bad typings - iframe.contentWindow.postMessage(message, "*"); - } - } - ); + const rawHash = await argonHash(password, key.salt); // https://passlib.readthedocs.io/en/stable/lib/passlib.hash.argon2.html#format-algorithm - const possibleHash = rawHash.split("$")[5]; + const possibleHash = rawHash ? rawHash.split("$")[5] : ""; if (!possibleHash) { throw new Error("argon2 did not return a hash!"); } // verify user password by comparing their password hash with the // hash of their password's hash - const isCorrectPassword = await new Promise( - (resolve: (value: string) => void) => { - const iframe = document.getElementById("argon-sandbox"); - const message = { - action: "verify", - value: possibleHash, - hash: key.hash, - }; - if (iframe) { - window.addEventListener("message", (response) => { - resolve(response.data.response); - }); - // @ts-expect-error bad typings - iframe.contentWindow.postMessage(message, "*"); - } - } + const isCorrectPassword = await argonVerify( + possibleHash, + key.hash ); // TODO: there is a serious bug here. If two keys have the same password, // then only one of them will be used for decryption. if (isCorrectPassword) { - state.state.encryption.set( - key.id, - new Encryption(possibleHash, key.id) - ); - state.state.defaultEncryption = key.id; + state.commit("setEncryption", { + keyId: key.id, + encryption: new Encryption(possibleHash, key.id), + }); + state.commit("setDefaultEncryption", key.id); saltedHash = possibleHash; } @@ -383,8 +371,11 @@ export class Accounts implements Module { version: 3, }; const newEncryption = new Encryption(saltedHash, key.id); - state.state.encryption.set(key.id, newEncryption); - state.state.defaultEncryption = key.id; + state.commit("setEncryption", { + keyId: key.id, + encryption: newEncryption, + }); + state.commit("setDefaultEncryption", key.id); const toRemove: string[] = []; for (const entry of state.state.entries) { @@ -392,15 +383,14 @@ export class Accounts implements Module { continue; } - await entry.changeEncryption(newEncryption); + state.commit("applyEntryEncryption", { + entry, + encryption: newEncryption, + }); // if not uuidv4 regen - if ( - /[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}/i.test( - entry.hash - ) - ) { - entry.genUUID(); + if (UUIDV4_REGEX.test(entry.hash)) { + state.commit("regenEntryHash", entry); toRemove.push(entry.hash); } } @@ -436,7 +426,10 @@ export class Accounts implements Module { entry.encryption?.getEncryptionKeyId() !== state.state.defaultEncryption ) { - await entry.changeEncryption(defaultEncryption); + state.commit("applyEntryEncryption", { + entry, + encryption: defaultEncryption, + }); needUpdateStorage = true; } } @@ -454,7 +447,7 @@ export class Accounts implements Module { }); } - state.commit("style/hideInfo", true, { root: true }); + state.dispatch("style/hideInfo", true, { root: true }); return; }, changePassphrase: async ( @@ -491,24 +484,20 @@ export class Accounts implements Module { version: 3, }; - const linkedKeys = new Map(); + const linkedKeys = new Set(); for (const entry of state.state.entries) { - await entry.changeEncryption(new Encryption(saltedHash, key.id)); + state.commit("applyEntryEncryption", { + entry, + encryption: new Encryption(saltedHash, key.id), + }); // if not uuidv4 regen - if ( - /[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}/i.test( - entry.hash - ) - ) { + if (UUIDV4_REGEX.test(entry.hash)) { removeKeys.push(entry.hash); - entry.genUUID(); + state.commit("regenEntryHash", entry); } if (entry.encryption?.getEncryptionKeyId()) { - linkedKeys.set( - entry.encryption.getEncryptionKeyId(), - undefined - ); + linkedKeys.add(entry.encryption.getEncryptionKeyId()); } } @@ -529,11 +518,11 @@ export class Accounts implements Module { await BrowserStorage.remove(removeKeys); } - state.state.encryption.set( - key.id, - new Encryption(saltedHash, key.id) - ); - state.state.defaultEncryption = key.id; + state.commit("setEncryption", { + keyId: key.id, + encryption: new Encryption(saltedHash, key.id), + }); + state.commit("setDefaultEncryption", key.id); await state.dispatch("updateEntries"); @@ -549,7 +538,10 @@ export class Accounts implements Module { }); } else { for (const entry of state.state.entries) { - await entry.changeEncryption(new Encryption("", "")); + state.commit("applyEntryEncryption", { + entry, + encryption: new Encryption("", ""), + }); } await EntryStorage.set(state.state.entries); @@ -560,7 +552,7 @@ export class Accounts implements Module { if (keyId) { await BrowserStorage.remove(keyId); } - state.state.defaultEncryption = ""; + state.commit("setDefaultEncryption", ""); await state.dispatch("updateEntries"); @@ -595,6 +587,15 @@ export class Accounts implements Module { state.commit("loadCodes", entries); state.commit("updateCodes"); + + // show the search box on load too, not only after clearing a smart + // filter, so it appears for large lists / after unlocking (#1496, #1400) + if ( + state.state.entries.length >= 10 && + !(state.getters.shouldFilter && state.state.filter) + ) { + state.commit("showSearch"); + } state.commit( "updateExport", await EntryStorage.getExport(state.state.entries) @@ -633,9 +634,9 @@ export class Accounts implements Module { ) { UserSettings.items.storageLocation = StorageLocation.Local; await chrome.storage.sync.clear(); - await chrome.storage.local.set({ - UserSettings: UserSettings.items, - }); + // commitItems() strips functions and routes by storageLocation, + // matching the local=>sync branch (was a raw local.set bypass). + await UserSettings.commitItems(); return "updateSuccess"; } else { throw " All data not transferred successfully."; @@ -676,10 +677,10 @@ export class Accounts implements Module { } private async getCachedKeyInfo() { - const { - cachedPassphrase, - cachedKeyId, - } = await chrome.storage.session.get(); + const { cachedPassphrase, cachedKeyId } = await chrome.storage.session.get([ + "cachedPassphrase", + "cachedKeyId", + ]); return { cachedPassphrase, cachedKeyId }; } @@ -694,22 +695,15 @@ async function genHash(value: string) { const randomValues = window.crypto.getRandomValues(new Uint16Array(8)); let salt = ""; for (const byte of randomValues) { - salt += byte.toString(16); + // zero-pad each 16-bit value to 4 hex chars; without padding leading zeros + // were dropped, giving variable-length salts and collisions (e.g. 0x0001 + // and 0x0010 both contributing "1"/"10" ambiguously). + salt += byte.toString(16).padStart(4, "0"); } - return new Promise((resolve: (value: string) => void) => { - const iframe = document.getElementById("argon-sandbox"); - const message = { - action: "hash", - value: value, - salt, - }; - if (iframe) { - window.addEventListener("message", (response) => { - resolve(response.data.response); - }); - // @ts-expect-error bad typings - iframe.contentWindow.postMessage(message, "*"); - } - }); + const hash = await argonHash(value, salt); + if (!hash) { + throw new Error("argon2 did not return a hash!"); + } + return hash; } diff --git a/src/store/Advisor.ts b/src/store/Advisor.ts index a9514d4e8..1e7d01fe1 100644 --- a/src/store/Advisor.ts +++ b/src/store/Advisor.ts @@ -1,3 +1,4 @@ +import { ActionContext } from "vuex"; import { EntryStorage } from "../models/storage"; import { InsightLevel, AdvisorInsight } from "../models/advisor"; import { StorageLocation, UserSettings } from "../models/settings"; @@ -19,7 +20,9 @@ const insightsData: AdvisorInsightInterface[] = [ validation: async () => { await UserSettings.updateItems(); const hasEncryptedEntry = await EntryStorage.hasEncryptionKey(); - return hasEncryptedEntry && !Number(UserSettings.items.autolock); + // an unset autolock falls back to the 30-min default (see setAutolock), + // so only warn when the user has explicitly disabled it with 0 (#1281) + return hasEncryptedEntry && Number(UserSettings.items.autolock) === 0; }, }, { @@ -58,6 +61,13 @@ const insightsData: AdvisorInsightInterface[] = [ }, ]; +// advisorIgnoreList may be stored as a JSON string (legacy) or an array; +// normalise to an array. +function parseIgnoreList(): string[] { + const raw = UserSettings.items.advisorIgnoreList; + return typeof raw === "string" ? JSON.parse(raw || "[]") : raw || []; +} + export class Advisor implements Module { async getModule() { await UserSettings.updateItems(); @@ -67,26 +77,41 @@ export class Advisor implements Module { ignoreList: UserSettings.items.advisorIgnoreList || [], }, mutations: { - dismissInsight: async (state: AdvisorState, insightId: string) => { + // sync state changes only (these used to be async mutations that + // assigned state after an await, which Vuex strict mode forbids) + pushIgnore(state: AdvisorState, insightId: string) { state.ignoreList.push(insightId); - UserSettings.items.advisorIgnoreList = state.ignoreList; + }, + setIgnoreList(state: AdvisorState, list: string[]) { + state.ignoreList = list; + }, + setInsights(state: AdvisorState, insights: AdvisorInsightInterface[]) { + state.insights = insights; + }, + }, + actions: { + dismissInsight: async ( + context: ActionContext, + insightId: string + ) => { + context.commit("pushIgnore", insightId); + UserSettings.items.advisorIgnoreList = context.state.ignoreList; UserSettings.commitItems(); - state.insights = await this.getInsights(); + context.commit("setInsights", await this.getInsights()); }, - clearIgnoreList: async (state: AdvisorState) => { - state.ignoreList = []; + clearIgnoreList: async ( + context: ActionContext + ) => { + context.commit("setIgnoreList", []); UserSettings.items.advisorIgnoreList = undefined; UserSettings.commitItems(); - state.insights = await this.getInsights(); + context.commit("setInsights", await this.getInsights()); }, - updateInsight: async (state: AdvisorState) => { - state.insights = await this.getInsights(); - state.ignoreList = - typeof UserSettings.items.advisorIgnoreList === "string" - ? JSON.parse(UserSettings.items.advisorIgnoreList || "[]") - : UserSettings.items.advisorIgnoreList || []; + updateInsight: async (context: ActionContext) => { + context.commit("setInsights", await this.getInsights()); + context.commit("setIgnoreList", parseIgnoreList()); }, }, namespaced: true, @@ -95,10 +120,7 @@ export class Advisor implements Module { private async getInsights() { await UserSettings.updateItems(); - const advisorIgnoreList: string[] = - typeof UserSettings.items.advisorIgnoreList === "string" - ? JSON.parse(UserSettings.items.advisorIgnoreList || "[]") - : UserSettings.items.advisorIgnoreList || []; + const advisorIgnoreList = parseIgnoreList(); const filteredInsightsData: AdvisorInsightInterface[] = []; diff --git a/src/store/Backup.ts b/src/store/Backup.ts index 790ed719a..b424b7b78 100644 --- a/src/store/Backup.ts +++ b/src/store/Backup.ts @@ -2,13 +2,15 @@ import { UserSettings } from "../models/settings"; export class Backup implements Module { async getModule() { - UserSettings.updateItems(); + await UserSettings.updateItems(); return { state: { - dropboxEncrypted: UserSettings.items.dropboxEncrypted === true, - driveEncrypted: UserSettings.items.driveEncrypted === true, - oneDriveEncrypted: UserSettings.items.oneDriveEncrypted === true, + // default to encrypted when unset, matching backup.ts upload behaviour + // (an unset preference uploads encrypted, so the UI must reflect that) + dropboxEncrypted: UserSettings.items.dropboxEncrypted !== false, + driveEncrypted: UserSettings.items.driveEncrypted !== false, + oneDriveEncrypted: UserSettings.items.oneDriveEncrypted !== false, dropboxToken: Boolean(UserSettings.items.dropboxToken), driveToken: Boolean(UserSettings.items.driveToken), oneDriveToken: Boolean(UserSettings.items.oneDriveToken), diff --git a/src/store/Menu.ts b/src/store/Menu.ts index 48487d6ed..26e2d27c4 100644 --- a/src/store/Menu.ts +++ b/src/store/Menu.ts @@ -1,7 +1,20 @@ -import { isSafari } from "../browser"; import { UserSettings } from "../models/settings"; import { ManagedStorage } from "../models/storage"; +// Map any stored theme (incl. the retired normal/simple/flat) to a current one. +function normalizeTheme(value?: string): string { + switch (value) { + case "dark": + case "auto": + case "accessibility": + case "compact": + case "light": + return value; + default: + return "light"; + } +} + export class Menu implements Module { async getModule() { await UserSettings.updateItems(); @@ -13,7 +26,8 @@ export class Menu implements Module { useAutofill: UserSettings.items.autofill === true, smartFilter: UserSettings.items.smartFilter === true, enableContextMenu: UserSettings.items.enableContextMenu === true, - theme: UserSettings.items.theme || (isSafari ? "flat" : "normal"), + theme: normalizeTheme(UserSettings.items.theme), + onboardingComplete: UserSettings.items.onboardingComplete === true, autolock: Number(UserSettings.items.autolock) || 30, backupDisabled: await ManagedStorage.get("disableBackup", false), exportDisabled: await ManagedStorage.get("disableExport", false), @@ -53,6 +67,11 @@ export class Menu implements Module { UserSettings.items.theme = theme; UserSettings.commitItems(); }, + setOnboardingComplete(state: MenuState, complete: boolean) { + state.onboardingComplete = complete; + UserSettings.items.onboardingComplete = complete; + UserSettings.commitItems(); + }, setAutolock(state: MenuState, autolock: number) { state.autolock = autolock; UserSettings.items.autolock = autolock; @@ -69,8 +88,8 @@ export class Menu implements Module { private resize(zoom: number) { if (zoom !== 100) { - document.body.style.marginBottom = 480 * (zoom / 100 - 1) + "px"; - document.body.style.marginRight = 320 * (zoom / 100 - 1) + "px"; + document.body.style.marginBottom = 580 * (zoom / 100 - 1) + "px"; + document.body.style.marginRight = 360 * (zoom / 100 - 1) + "px"; document.body.style.transform = "scale(" + zoom / 100 + ")"; } } diff --git a/src/store/Notification.ts b/src/store/Notification.ts index c68d81290..8fce1c5fc 100644 --- a/src/store/Notification.ts +++ b/src/store/Notification.ts @@ -13,12 +13,11 @@ export class Notification implements Module { alert: (state: NotificationState, message: string) => { state.message.unshift(message); }, - closeAlert: (state: NotificationState) => { - state.messageIdle = false; + setMessageIdle: (state: NotificationState, value: boolean) => { + state.messageIdle = value; + }, + shiftMessage: (state: NotificationState) => { state.message.shift(); - setTimeout(() => { - state.messageIdle = true; - }, 200); }, setConfirm: (state: NotificationState, message: string) => { state.confirmMessage = message; @@ -28,13 +27,26 @@ export class Notification implements Module { }, }, actions: { + // was a mutation, but the deferred reset (setTimeout) mutated state + // outside the handler, which Vuex strict mode forbids + closeAlert: ({ commit }: ActionContext) => { + commit("setMessageIdle", false); + commit("shiftMessage"); + setTimeout(() => { + commit("setMessageIdle", true); + }, 200); + }, confirm: async ( state: ActionContext, message: string ) => { return new Promise((resolve: (value: boolean) => void) => { state.commit("setConfirm", message); - window.addEventListener("confirm", (event) => { + // Named handler so it can be removed once it fires; the old + // anonymous listener was added on every dispatch and never removed, + // accumulating and re-firing on later confirms. + const handler = (event: Event) => { + window.removeEventListener("confirm", handler); state.commit("setConfirm", ""); if (!this.isCustomEvent(event)) { resolve(false); @@ -42,7 +54,8 @@ export class Notification implements Module { } resolve(event.detail); return; - }); + }; + window.addEventListener("confirm", handler); }); }, ephermalMessage: ( @@ -50,7 +63,7 @@ export class Notification implements Module { message: string ) => { state.commit("setNotification", message); - state.commit("style/showNotification", null, { root: true }); + state.dispatch("style/showNotification", null, { root: true }); }, }, namespaced: true, diff --git a/src/store/Permissions.ts b/src/store/Permissions.ts index a8d20ebca..ea2268c16 100644 --- a/src/store/Permissions.ts +++ b/src/store/Permissions.ts @@ -1,3 +1,4 @@ +import { ActionContext } from "vuex"; import { Permission } from "../models/permission"; import { UserSettings } from "../models/settings"; @@ -150,8 +151,15 @@ export class Permissions implements Module { permissions: await this.getPermissions(), }, mutations: { + setPermissions(state: PermissionsState, permissions: Permission[]) { + state.permissions = permissions; + }, + }, + actions: { + // was an async mutation; assigning state after an await violates Vuex + // strict mode, so the async work lives in an action now revokePermission: async ( - state: PermissionsState, + context: ActionContext, permissionId: string ) => { const permissionObject = this.getPermissionById(permissionId); @@ -163,17 +171,15 @@ export class Permissions implements Module { ).filter((result) => !result.valid); if (validationResults.length > 0) { - const messages = await Promise.all( - validationResults.map( - async (result) => "• " + (await result).message - ) + const messages = validationResults.map( + (result) => "• " + result.message ); alert(messages.join("\n")); return; } await this.revokePermission(permissionId); - state.permissions = await this.getPermissions(); + context.commit("setPermissions", await this.getPermissions()); }, }, namespaced: true, @@ -251,11 +257,11 @@ export class Permissions implements Module { ); } } + + // nothing matched -> nothing to remove + resolve(); } ); - - // Timeout for remove permissions failed - setTimeout(resolve, 100); }); } } diff --git a/src/store/Qr.ts b/src/store/Qr.ts index 1f77682cb..3251206f8 100644 --- a/src/store/Qr.ts +++ b/src/store/Qr.ts @@ -1,12 +1,21 @@ +interface QrData { + src: string; + issuer: string; + account: string; + monogram: string; + monoBg: string; + monoFg: string; +} + export class Qr implements Module { getModule() { return { state: { - qr: "", + qr: null as QrData | null, }, mutations: { - setQr(state: { qr: string }, url: string) { - state.qr = `url(${url})`; + setQr(state: { qr: QrData | null }, data: QrData) { + state.qr = data; }, }, namespaced: true, diff --git a/src/store/Style.ts b/src/store/Style.ts index a70345691..b4b0bed0c 100644 --- a/src/store/Style.ts +++ b/src/store/Style.ts @@ -1,3 +1,5 @@ +import { ActionContext } from "vuex"; + export class Style implements Module { getModule() { return { @@ -18,17 +20,19 @@ export class Style implements Module { }, }, mutations: { + // generic synchronous flag setter so the animation actions below can + // schedule their deferred resets through a mutation (Vuex strict mode + // forbids the setTimeout callbacks mutating state directly) + setStyleFlag( + state: StyleState, + payload: { key: keyof StyleState["style"]; value: boolean } + ) { + state.style[payload.key] = payload.value; + }, showMenu(state: StyleState) { state.style.slidein = true; state.style.slideout = false; }, - hideMenu(state: StyleState) { - state.style.slidein = false; - state.style.slideout = true; - setTimeout(() => { - state.style.slideout = false; - }, 200); - }, showInfo(state: StyleState, noAnimate?: boolean) { if (noAnimate) { state.style.show = true; @@ -37,45 +41,62 @@ export class Style implements Module { state.style.fadeout = false; } }, - hideInfo(state: StyleState, noAnimate?: boolean) { + showQr(state: StyleState) { + state.style.qrfadein = true; + state.style.qrfadeout = false; + }, + toggleEdit(state: StyleState) { + state.style.isEditing = !state.style.isEditing; + }, + toggleHotpDisabled(state: StyleState) { + state.style.hotpDisabled = !state.style.hotpDisabled; + }, + }, + actions: { + // these end an animation by resetting a flag after a delay, which has + // to be committed (not mutated directly) under strict mode + hideMenu({ commit }: ActionContext) { + commit("setStyleFlag", { key: "slidein", value: false }); + commit("setStyleFlag", { key: "slideout", value: true }); + setTimeout(() => { + commit("setStyleFlag", { key: "slideout", value: false }); + }, 200); + }, + hideInfo( + { commit }: ActionContext, + noAnimate?: boolean + ) { if (noAnimate) { - state.style.show = false; + commit("setStyleFlag", { key: "show", value: false }); } else { - state.style.fadein = false; - state.style.fadeout = true; + commit("setStyleFlag", { key: "fadein", value: false }); + commit("setStyleFlag", { key: "fadeout", value: true }); } setTimeout(() => { - state.style.fadeout = false; + commit("setStyleFlag", { key: "fadeout", value: false }); }, 200); }, - showQr(state: StyleState) { - state.style.qrfadein = true; - state.style.qrfadeout = false; - }, - hideQr(state: StyleState) { - state.style.qrfadein = false; - state.style.qrfadeout = true; + hideQr({ commit }: ActionContext) { + commit("setStyleFlag", { key: "qrfadein", value: false }); + commit("setStyleFlag", { key: "qrfadeout", value: true }); setTimeout(() => { - state.style.qrfadeout = false; + commit("setStyleFlag", { key: "qrfadeout", value: false }); }, 200); }, - showNotification(state: StyleState) { - state.style.notificationFadein = true; - state.style.notificationFadeout = false; + showNotification({ commit }: ActionContext) { + commit("setStyleFlag", { key: "notificationFadein", value: true }); + commit("setStyleFlag", { key: "notificationFadeout", value: false }); setTimeout(() => { - state.style.notificationFadein = false; - state.style.notificationFadeout = true; + commit("setStyleFlag", { key: "notificationFadein", value: false }); + commit("setStyleFlag", { key: "notificationFadeout", value: true }); setTimeout(() => { - state.style.notificationFadeout = false; + commit("setStyleFlag", { + key: "notificationFadeout", + value: false, + }); }, 200); }, 1000); }, - toggleEdit(state: StyleState) { - state.style.isEditing = !state.style.isEditing; - }, - toggleHotpDisabled(state: StyleState) { - state.style.hotpDisabled = !state.style.hotpDisabled; - }, }, getters: { // Returns true if menu or info screen shown diff --git a/src/store/i18n.ts b/src/store/i18n.ts index 983f97c50..f1b127b5f 100644 --- a/src/store/i18n.ts +++ b/src/store/i18n.ts @@ -1,34 +1,13 @@ export async function loadI18nMessages() { - return new Promise( - ( - resolve: (value: { [key: string]: string }) => void, - reject: (reason: Error) => void - ) => { - try { - const xhr = new XMLHttpRequest(); - xhr.overrideMimeType("application/json"); - xhr.onreadystatechange = () => { - if (xhr.readyState === 4) { - const i18nMessage: I18nMessage = JSON.parse(xhr.responseText); - const i18nData: { [key: string]: string } = {}; - for (const key of Object.keys(i18nMessage)) { - i18nData[key] = chrome.i18n.getMessage(key); - } - return resolve(i18nData); - } - return; - }; - xhr.open("GET", chrome.runtime.getURL("/_locales/en/messages.json")); - xhr.send(); - } catch (error) { - if (typeof error === "string" || error === undefined) { - return reject(Error(error)); - } else if (error instanceof Error) { - return reject(error); - } else { - return reject(Error(String(error))); - } - } - } + // bundled extension resource; only the keys are used -- each value is + // resolved through chrome.i18n.getMessage for the active locale. + const response = await fetch( + chrome.runtime.getURL("/_locales/en/messages.json") ); + const i18nMessage: I18nMessage = await response.json(); + const i18nData: { [key: string]: string } = {}; + for (const key of Object.keys(i18nMessage)) { + i18nData[key] = chrome.i18n.getMessage(key); + } + return i18nData; } diff --git a/src/syncTime.ts b/src/syncTime.ts index 7e46074cc..b21578ccd 100644 --- a/src/syncTime.ts +++ b/src/syncTime.ts @@ -21,14 +21,16 @@ export async function syncTimeWithGoogle() { return resolve("updateFailure"); } const serverTime = new Date(date).getTime(); + if (isNaN(serverTime)) { + // unparseable date header — report a failure, not a huge offset + return resolve("updateFailure"); + } const clientTime = new Date().getTime(); const offset = Math.round((serverTime - clientTime) / 1000); if (Math.abs(offset) <= 300) { // within 5 minutes - UserSettings.items.offset = Math.round( - (serverTime - clientTime) / 1000 - ); + UserSettings.items.offset = offset; UserSettings.commitItems(); return resolve("updateSuccess"); } else { diff --git a/src/test/components/Popup/AddAccountPage.test.ts b/src/test/components/Popup/AddAccountPage.test.ts new file mode 100644 index 000000000..ea30f1814 --- /dev/null +++ b/src/test/components/Popup/AddAccountPage.test.ts @@ -0,0 +1,84 @@ +import "mocha"; +import * as chai from "chai"; +import * as sinon from "sinon"; +import * as sinonChai from "sinon-chai"; + +import { mount } from "@vue/test-utils"; +import { toRaw } from "vue"; +import { createStore, Store } from "vuex"; +import CommonComponents from "../../../components/common/index"; + +import AddAccountPage from "../../../components/Popup/AddAccountPage.vue"; +import { EntryStorage } from "../../../models/storage"; +import { OTPType, OTPAlgorithm } from "../../../models/otp"; +import { loadI18nMessages } from "../../../store/i18n"; + +chai.should(); +chai.use(sinonChai); +mocha.setup("bdd"); + +describe("AddAccountPage", () => { + let i18n: { [key: string]: string }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const components: { [name: string]: any } = {}; + + before(async () => { + i18n = await loadI18nMessages(); + for (const component of CommonComponents) { + components[component.name] = component.component; + } + }); + + // a minimal stand-in for a real encryption instance held in the Map + const fakeEncryption = { getEncryptionStatus: () => true }; + const addCode = sinon.fake(); + + const storeOpts = { + modules: { + accounts: { + state: { + OTPType, + OTPAlgorithm, + encryption: new Map([["key-1", fakeEncryption]]), + defaultEncryption: "key-1", + }, + actions: { addCode }, + namespaced: true, + }, + style: { + actions: { hideInfo: sinon.fake() }, + mutations: { toggleEdit: () => undefined }, + namespaced: true, + }, + notification: { + mutations: { alert: () => undefined }, + namespaced: true, + }, + }, + }; + let store: Store; + + beforeEach(() => { + addCode.resetHistory(); + // don't touch real storage when the entry is created + sinon.stub(EntryStorage, "add").resolves(); + store = createStore(storeOpts); + }); + + it("should construct the new entry with the default encryption instance", async () => { + const wrapper = mount(AddAccountPage, { + global: { plugins: [store], mocks: { i18n }, components }, + }); + + wrapper.vm.newAccount.secret = "aaaaaaaaaaaaaaaa"; // valid base32, >= 16 chars + await wrapper.vm.addNewAccount(); + + addCode.should.have.been.calledOnce; + // regression: encryption Map must be read with .get(), not [] — bracket + // indexing returns undefined and the secret would be stored UNENCRYPTED. + // toRaw: the Map value comes back as a reactive proxy, and chai's `.should` + // getter chokes on Vue's __v_isRef probe, so compare the raw target. + const entry = addCode.lastCall.args[1]; + chai.assert.strictEqual(toRaw(entry.encryption), fakeEncryption); + }); +}); diff --git a/src/test/components/Popup/EnterPasswordPage.test.ts b/src/test/components/Popup/EnterPasswordPage.test.ts index ab1c16489..5374cd5be 100644 --- a/src/test/components/Popup/EnterPasswordPage.test.ts +++ b/src/test/components/Popup/EnterPasswordPage.test.ts @@ -3,8 +3,8 @@ import * as chai from "chai"; import * as sinon from "sinon"; import * as sinonChai from "sinon-chai"; -import { mount, createLocalVue } from "@vue/test-utils"; -import Vuex, { Store } from "vuex"; +import { mount } from "@vue/test-utils"; +import { createStore, Store } from "vuex"; import CommonComponents from "../../../components/common/index"; import EnterPasswordPage from "../../../components/Popup/EnterPasswordPage.vue"; @@ -13,18 +13,20 @@ import { loadI18nMessages } from "../../../store/i18n"; const should = chai.should(); chai.use(sinonChai); mocha.setup("bdd"); -const localVue = createLocalVue(); describe("EnterPasswordPage", () => { + let i18n: { [key: string]: string }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const components: { [name: string]: any } = {}; + before(async () => { - localVue.prototype.i18n = await loadI18nMessages(); - localVue.use(Vuex); + i18n = await loadI18nMessages(); for (const component of CommonComponents) { - localVue.component(component.name, component.component); + components[component.name] = component.component; } }); - let storeOpts = { + const storeOpts = { modules: { accounts: { actions: { @@ -39,14 +41,20 @@ describe("EnterPasswordPage", () => { }; let store: Store; + const mountPage = (attach = false) => + mount(EnterPasswordPage, { + global: { plugins: [store], mocks: { i18n }, components }, + ...(attach ? { attachTo: document.body } : {}), + }); + beforeEach(() => { // TODO: find a nicer var storeOpts.modules.accounts.actions.applyPassphrase.resetHistory(); - store = new Vuex.Store(storeOpts); + store = createStore(storeOpts); }); it("should apply password when button is clicked", async () => { - const wrapper = mount(EnterPasswordPage, { store, localVue }); + const wrapper = mountPage(); const passwordInput = wrapper.find("input"); const passwordButton = wrapper.find("button"); @@ -60,7 +68,7 @@ describe("EnterPasswordPage", () => { }); it("should apply password when enter is pressed", async () => { - const wrapper = mount(EnterPasswordPage, { store, localVue }); + const wrapper = mountPage(); const passwordInput = wrapper.find("input"); @@ -73,11 +81,7 @@ describe("EnterPasswordPage", () => { }); it("should autofocus password input", () => { - const wrapper = mount(EnterPasswordPage, { - store, - localVue, - attachToDocument: true, - }); + const wrapper = mountPage(true); const passwordInput = wrapper.find("input"); @@ -85,7 +89,9 @@ describe("EnterPasswordPage", () => { }); it("should not show incorrect password message", () => { - const wrapper = mount(EnterPasswordPage, { store, localVue }); + // isVisible() reads getComputedStyle, which only reflects v-show's + // display:none for elements attached to the live document, so attach. + const wrapper = mountPage(true); const errorText = wrapper.find("label.warning"); @@ -98,7 +104,7 @@ describe("EnterPasswordPage", () => { }); it("should show incorrect password message", () => { - const wrapper = mount(EnterPasswordPage, { store, localVue }); + const wrapper = mountPage(); const errorText = wrapper.find("label.warning"); diff --git a/src/test/components/Popup/MenuPage.test.ts b/src/test/components/Popup/MenuPage.test.ts index f8c169b1c..91df8faae 100644 --- a/src/test/components/Popup/MenuPage.test.ts +++ b/src/test/components/Popup/MenuPage.test.ts @@ -2,34 +2,23 @@ import "mocha"; import * as chai from "chai"; import { assert } from "chai"; import * as sinonChai from "sinon-chai"; -import { createLocalVue, mount, Wrapper } from "@vue/test-utils"; -import Vuex, { Store } from "vuex"; +import * as sinon from "sinon"; +import { mount, VueWrapper } from "@vue/test-utils"; +import { createStore, Store } from "vuex"; -import { loadI18nMessages } from "../../../store/i18n"; import MenuPage from "../../../components/Popup/MenuPage.vue"; -import { Style } from "../../../store/Style"; -import { Accounts } from "../../../store/Accounts"; -import { Backup } from "../../../store/Backup"; -import { CurrentView } from "../../../store/CurrentView"; -import { Menu } from "../../../store/Menu"; -import { Notification } from "../../../store/Notification"; -import { Qr } from "../../../store/Qr"; - -import chrome from "sinon-chrome"; - chai.should(); chai.use(sinonChai); mocha.setup("bdd"); -const localVue = createLocalVue(); describe("MenuPage", () => { - before(async () => { - localVue.prototype.i18n = await loadI18nMessages(); - localVue.use(Vuex); - }); + // chrome.i18n.getMessage returns "" in the test extension, so titles bound + // to i18n.* render empty. Use a fixed map; only `feedback` is asserted on + // (the feedback button is found via *[title='Feedback']). + const i18n: { [key: string]: string } = { feedback: "Feedback" }; - let storeOpts = { + const storeOpts = { menu: { state: { version: "1.2.3", @@ -40,133 +29,33 @@ describe("MenuPage", () => { let store: Store<{}>; - let wrapper: Wrapper; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let wrapper: VueWrapper; - before(() => { - // mock the chrome global object - global.chrome.tabs.create = chrome.tabs.create; - global.chrome.storage.managed.get = chrome.storage.managed.get; - }); + const mountMenu = () => + mount(MenuPage, { + global: { plugins: [store], mocks: { i18n } }, + }); beforeEach(async () => { - store = new Vuex.Store({ + store = createStore({ modules: storeOpts, }); - wrapper = mount(MenuPage, { - store, - localVue, - }); + wrapper = mountMenu(); }); - const clickMenuPageButtonByTitle = async ( - wrapper: Wrapper, - title: string - ) => wrapper.find(`*[title='${title}']`).trigger("click"); - describe("feedback button", () => { - // mocks the user agent for testing purposes - const mockUserAgent = (userAgent: string) => { - Object.defineProperty(global, "navigator", { - value: { - userAgent, - }, - configurable: true, - enumerable: true, - writable: true, - }); - }; - - beforeEach(() => { - wrapper = mount(MenuPage, { - store, - localVue, - }); - }); - - it("should open a new tab to the Chrome help page when the feedback button is clicked and the user agent is Chrome", async () => { - mockUserAgent( - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)" - ); - await clickMenuPageButtonByTitle(wrapper, "Feedback"); - assert.ok( - chrome.tabs.create.withArgs({ url: "https://otp.ee/chromeissues" }) - .calledOnce, - "Tab create should be called with the Chrome URL" - ); - }); - - it("should open a new tab to the Edge help page when the feedback button is clicked and the user agent is Edge", async () => { - mockUserAgent( - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.74 Safari/537.36 Edg/79.0.309.43" - ); - await clickMenuPageButtonByTitle(wrapper, "Feedback"); - assert.ok( - chrome.tabs.create.withArgs({ url: "https://otp.ee/edgeissues" }) - .calledOnce, - "Tab create should be called with the Edge URL" - ); - }); - - it("should open a new tab to the Firefox help page when the feedback button is clicked and the user agent is Firefox", async () => { - mockUserAgent( - "Mozilla/5.0 (Windows NT x.y; rv:10.0) Gecko/20100101 Firefox/10.0" - ); - await clickMenuPageButtonByTitle(wrapper, "Feedback"); + it("opens the GitHub issues page", async () => { + const openStub = sinon.stub(window, "open"); + await wrapper.find("*[title='Feedback']").trigger("click"); assert.ok( - chrome.tabs.create.withArgs({ url: "https://otp.ee/firefoxissues" }) - .calledOnce, - "Tab create should be called with the Firefox URL" + openStub.calledWith( + "https://github.com/Hank076/Authenticator/issues", + "_blank" + ), + "window.open should be called with the GitHub issues URL" ); - }); - - it("should open a new tab to the Chrome help page when the feedback button is clicked and the user agent is unknown", async () => { - mockUserAgent("Unknown"); - await clickMenuPageButtonByTitle(wrapper, "Feedback"); - assert.ok( - chrome.tabs.create.withArgs({ url: "https://otp.ee/chromeissues" }) - .called, - "Tab create should be called with the Chrome URL" - ); - }); - - describe("feedbackURL is set", () => { - beforeEach(async () => { - try { - chrome.storage.managed.get.yieldsAsync({ - feedbackURL: "https://authenticator.cc", - }); - - store = new Vuex.Store({ - modules: { - backup: await new Backup().getModule(), - currentView: new CurrentView().getModule(), - notification: new Notification().getModule(), - qr: new Qr().getModule(), - style: new Style().getModule(), - menu: await new Menu().getModule(), - accounts: await new Accounts().getModule(), - }, - }); - - wrapper = mount(MenuPage, { - store, - localVue, - }); - } catch (e) { - console.error(e); - // Doesn't show up in mocha? - throw e; - } - }); - - it("should open a new tab to the page specified in ManagedStorage", async () => { - await clickMenuPageButtonByTitle(wrapper, "Feedback"); - assert.ok( - chrome.tabs.create.withArgs({ url: "https://authenticator.cc" }) - .called, - "Tab create should be called with the feedback URL" - ); - }); + openStub.restore(); }); }); diff --git a/src/test/utils.test.ts b/src/test/utils.test.ts new file mode 100644 index 000000000..585839019 --- /dev/null +++ b/src/test/utils.test.ts @@ -0,0 +1,151 @@ +import "mocha"; +import { expect } from "chai"; +import { getMatchedEntries, cloudBackupAllowed } from "../utils"; +import { EntryStorage } from "../models/storage"; +import { OTPEntry, OTPType } from "../models/otp"; + +// getSiteName() returns [title, nameFromDomain, hostname]. autofill paths call +// getMatchedEntries(siteName, entries, strict=true). These tests pin the strict +// (host-bound) matching contract: a live OTP is only ever offered for a page +// whose real host matches the entry's bound host. +function site( + hostname: string, + nameFromDomain = "", + title = "" +): Array { + return [title, nameFromDomain, hostname]; +} + +function entry(issuer: string, host?: string): OTPEntryInterface { + return ({ issuer, host } as unknown) as OTPEntryInterface; +} + +describe("getMatchedEntries strict (host-bound autofill)", () => { + it("matches when the bound host exactly equals the page host", () => { + const entries = [entry("MyBank", "accounts.google.com")]; + const matched = getMatchedEntries( + site("accounts.google.com", "google"), + entries, + true + ); + expect(matched).to.be.an("array").with.lengthOf(1); + }); + + it("matches a subdomain of the bound host", () => { + const entries = [entry("MyBank", "google.com")]; + const matched = getMatchedEntries( + site("accounts.google.com", "google"), + entries, + true + ); + expect(matched).to.be.an("array").with.lengthOf(1); + }); + + it("does NOT match when the bound host differs, even if the issuer name coincides with the page", () => { + // issuer "Google" used to match the host "google" by name; with host + // binding the mismatching bound host (mybank.com) must win and refuse. + const entries = [entry("Google", "mybank.com")]; + const matched = getMatchedEntries( + site("accounts.google.com", "google"), + entries, + true + ); + expect(matched).to.deep.equal([]); + }); + + it("does NOT match an attacker suffix of the bound host", () => { + const entries = [entry("MyBank", "google.com")]; + const matched = getMatchedEntries( + site("google.com.attacker.com", "attacker"), + entries, + true + ); + expect(matched).to.deep.equal([]); + }); + + it("does NOT autofill an entry that has no bound host", () => { + // No host binding: even though the issuer name matches the page, strict + // autofill must refuse to inject a live code. + const entries = [entry("Google", undefined)]; + const matched = getMatchedEntries( + site("google.com", "google"), + entries, + true + ); + expect(matched).to.deep.equal([]); + }); + + it("migrates a legacy issuer::host binding for strict matching", () => { + // Upstream encoded the bound host in the issuer field as "Name::host". + const entries = [entry("Google::google.com", undefined)]; + const matched = getMatchedEntries( + site("accounts.google.com", "google"), + entries, + true + ); + expect(matched).to.be.an("array").with.lengthOf(1); + }); +}); + +describe("getMatchedEntries loose (display filtering, unchanged)", () => { + it("still matches by issuer name against the real host", () => { + const entries = [entry("Google", undefined)]; + const matched = getMatchedEntries( + site("google.com", "google"), + entries, + false + ); + expect(matched).to.be.an("array").with.lengthOf(1); + }); +}); + +// A cloud backup must never carry plaintext secrets off the device. Uploading +// is only permitted once a master password is set, so the export is encrypted +// before it leaves for Dropbox / Drive / OneDrive. cloudBackupAllowed only +// reads getEncryptionStatus(), so a tiny stub stands in for a real Encryption. +describe("cloudBackupAllowed (no plaintext cloud upload without a password)", () => { + const withPassword = ({ + getEncryptionStatus: () => true, + } as unknown) as EncryptionInterface; + const withoutPassword = ({ + getEncryptionStatus: () => false, + } as unknown) as EncryptionInterface; + + it("blocks cloud upload when no master password is set", () => { + expect(cloudBackupAllowed(withoutPassword)).to.equal(false); + }); + + it("allows cloud upload once a master password is set", () => { + expect(cloudBackupAllowed(withPassword)).to.equal(true); + }); + + it("blocks cloud upload when no encryption instance is provided", () => { + expect(cloudBackupAllowed(undefined)).to.equal(false); + }); +}); + +// The bound host must survive a storage round-trip (save -> reload). Without a +// master password an entry is stored as plaintext and rebuilt via the explicit +// field list in EntryStorage.get(), which used to drop the host field. +describe("EntryStorage preserves the bound host across reload", () => { + it("keeps entry.host after add() and get()", async () => { + const entry = new OTPEntry({ + type: OTPType.totp, + index: 0, + issuer: "MyBank", + host: "accounts.example.com", + account: "user", + encrypted: false, + secret: "AAAAAAAAAAAAAAAA", + }); + try { + await EntryStorage.add(entry); + const reloaded = (await EntryStorage.get()).find( + (e) => e.hash === entry.hash + ); + expect(reloaded && reloaded.host).to.equal("accounts.example.com"); + } finally { + await EntryStorage.delete(entry); + } + }); +}); diff --git a/src/utils.ts b/src/utils.ts index 508f06503..5488c4bd5 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -34,32 +34,32 @@ export async function getSiteName() { // ip address if (/^\d+\.\d+\.\d+\.\d+$/.test(hostname)) { nameFromDomain = hostname; - } - - // local network - if (hostname.indexOf(".") === -1) { - nameFromDomain = hostname; - } + } else { + // local network + if (hostname.indexOf(".") === -1) { + nameFromDomain = hostname; + } - const hostLevelUnits = hostname.split("."); + const hostLevelUnits = hostname.split("."); - if (hostLevelUnits.length === 2) { - nameFromDomain = hostLevelUnits[0]; - } + if (hostLevelUnits.length === 2) { + nameFromDomain = hostLevelUnits[0]; + } - // www.example.com - // example.com.cn - if (hostLevelUnits.length > 2) { + // www.example.com // example.com.cn - if ( - ["com", "net", "org", "edu", "gov", "co"].indexOf( - hostLevelUnits[hostLevelUnits.length - 2] - ) !== -1 - ) { - nameFromDomain = hostLevelUnits[hostLevelUnits.length - 3]; - } else { - // www.example.com - nameFromDomain = hostLevelUnits[hostLevelUnits.length - 2]; + if (hostLevelUnits.length > 2) { + // example.com.cn + if ( + ["com", "net", "org", "edu", "gov", "co"].indexOf( + hostLevelUnits[hostLevelUnits.length - 2] + ) !== -1 + ) { + nameFromDomain = hostLevelUnits[hostLevelUnits.length - 3]; + } else { + // www.example.com + nameFromDomain = hostLevelUnits[hostLevelUnits.length - 2]; + } } } @@ -68,9 +68,14 @@ export async function getSiteName() { return [title, nameFromDomain, hostname]; } +// `strict` is used by autofill, which pastes a live OTP into the page: it drops +// the page-controlled match and anchors the host match to a real domain +// boundary, so a hostile page can't claim another origin's code. Display +// filtering passes strict=false and stays loose. export function getMatchedEntries( siteName: Array<string | null>, - entries: OTPEntryInterface[] + entries: OTPEntryInterface[], + strict = false ) { if (siteName.length < 2) { return false; @@ -79,7 +84,7 @@ export function getMatchedEntries( const matched = []; for (const entry of entries) { - if (isMatchedEntry(siteName, entry)) { + if (isMatchedEntry(siteName, entry, strict)) { matched.push(entry); } } @@ -99,36 +104,58 @@ export function getMatchedEntriesHash( return false; } +// True when `host` is exactly `bound` or a subdomain of it, so that +// "google.com.attacker.com" does NOT match the bound host "google.com". +function hostMatchesDomain(host: string, bound: string) { + host = host.toLowerCase(); + bound = bound.toLowerCase().replace(/^\.+/, ""); + return host === bound || host.endsWith("." + bound); +} + function isMatchedEntry( siteName: Array<string | null>, - entry: OTPEntryInterface + entry: OTPEntryInterface, + strict = false ) { - if (!entry.issuer) { - return false; + const siteTitle = siteName[0] || ""; + const siteNameFromHost = siteName[1] || ""; + const siteHost = siteName[2] || ""; + + // The bound host lives in entry.host; fall back to the legacy "issuer::host" + // encoding for entries not yet migrated (e.g. raw storage objects). + const issuerParts = (entry.issuer || "").split("::"); + let boundHost = entry.host || ""; + if (!boundHost && issuerParts.length > 1 && issuerParts[1]) { + boundHost = issuerParts[1]; + } + boundHost = boundHost.replace(/^\.+/, "").toLowerCase(); + + // strict (autofill): only ever inject a live code when the page's real host + // matches an explicitly bound host. No bound host => never autofill, so a + // hostile page can't harvest a code the user didn't mean for it. + if (strict) { + if (!boundHost) { + return false; + } + return Boolean(siteHost && hostMatchesDomain(siteHost, boundHost)); } - const issuerHostMatches = entry.issuer.split("::"); - const issuer = issuerHostMatches[0].replace(/[^0-9a-z]/gi, "").toLowerCase(); + // loose (display filtering): bound host match, else issuer-name heuristics. + if (boundHost && siteHost && hostMatchesDomain(siteHost, boundHost)) { + return true; + } + const issuer = issuerParts[0].replace(/[^0-9a-z]/gi, "").toLowerCase(); if (!issuer) { return false; } - const siteTitle = siteName[0] || ""; - const siteNameFromHost = siteName[1] || ""; - const siteHost = siteName[2] || ""; - - if (issuerHostMatches.length > 1) { - if (siteHost && siteHost.indexOf(issuerHostMatches[1]) !== -1) { - return true; - } - } - // site title should be more detailed - // so we use siteTitle.indexOf(issuer) + // The page-controlled <title> is only a weak hint, kept for display filtering. if (siteTitle && siteTitle.indexOf(issuer) !== -1) { return true; } + // siteNameFromHost is derived from the real hostname, not page-controlled. if (siteNameFromHost && issuer.indexOf(siteNameFromHost) !== -1) { return true; } @@ -136,6 +163,30 @@ function isMatchedEntry( return false; } +// Normalize a user- or page-provided host into a bare lowercase hostname so it +// can be compared with hostMatchesDomain. Accepts a full URL or a bare host. +export function normalizeHost(input: string): string { + const trimmed = input.trim().toLowerCase(); + if (!trimmed) { + return ""; + } + try { + return new URL(trimmed.includes("://") ? trimmed : "https://" + trimmed) + .hostname; + } catch { + return trimmed.replace(/^\.+/, "").replace(/\/.*$/, ""); + } +} + +// A cloud backup must never carry plaintext secrets off the device. Uploading +// is only allowed once a master password is set, so the export is encrypted +// before it leaves for Dropbox / Drive / OneDrive. Lives here (a leaf module) +// rather than in backup.ts so tests can exercise it without dragging in the +// storage <-> otp import cycle. +export function cloudBackupAllowed(encryption?: EncryptionInterface): boolean { + return Boolean(encryption && encryption.getEncryptionStatus()); +} + export async function getCurrentTab() { const currentWindow = await chrome.windows.getCurrent(); const queryOptions = { active: true, windowId: currentWindow.id }; diff --git a/tsconfig.json b/tsconfig.json index 3adffa296..d6120b32c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -26,6 +26,6 @@ "node_modules" ], "vueCompilerOptions": { - "target": 2 + "target": 3 } } diff --git a/view/options.html b/view/options.html index 20c33c749..fea6e0596 100644 --- a/view/options.html +++ b/view/options.html @@ -3,6 +3,7 @@ <head> <meta charset="utf-8" /> + <link rel="stylesheet" href="../css/options.css" /> </head> <body> diff --git a/webpack.config.js b/webpack.config.js index 3eb5c2828..6ee725fc2 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,4 +1,5 @@ const path = require("path"); +const webpack = require("webpack"); const { VueLoaderPlugin } = require("vue-loader"); const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin"); @@ -39,7 +40,7 @@ module.exports = { }, { test: /\.svg$/, - loader: 'vue-svg-loader' + use: ['vue-loader', 'vue-svg-loader'] }, { test: /\.(png|jpe?g|gif)$/, @@ -54,13 +55,15 @@ module.exports = { }, plugins: [ new VueLoaderPlugin(), - new ForkTsCheckerWebpackPlugin({ - typescript: { - extensions: { - vue: true - } - } - }) + // Vue 3 esm-bundler feature flags (better tree-shaking, silences the warning) + new webpack.DefinePlugin({ + __VUE_OPTIONS_API__: "true", + __VUE_PROD_DEVTOOLS__: "false", + __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: "false" + }), + // .vue type checking is done by vue-tsc (npm run typecheck), not here; + // fork-ts-checker's vue extension needs Vue 2's vue-template-compiler. + new ForkTsCheckerWebpackPlugin() ], resolve: { extensions: [ diff --git a/webpack.prod.js b/webpack.prod.js index e7bfebabc..28791bbbf 100644 --- a/webpack.prod.js +++ b/webpack.prod.js @@ -3,4 +3,7 @@ const common = require('./webpack.config.js'); module.exports = merge(common, { mode: 'production', + // The base config enables full source maps for development; don't ship them + // (and the .map files build.sh copies) in production release artifacts. + devtool: false, });