Compare commits

..

325 Commits

Author SHA1 Message Date
Leendert de Borst
4030387ead Show disabled email claim amount in user edit page in admin 2025-11-06 16:36:57 +01:00
Leendert de Borst
0240f008ce Update android-autofill.tsx 2025-11-06 12:45:33 +01:00
Leendert de Borst
bad4f46a82 Bump Android app version to include new autofill fixes (#1332) 2025-11-06 12:30:41 +01:00
Leendert de Borst
8ec5fab5e0 Improve android autofill matching logic for common usecases (#1332) 2025-11-06 12:30:40 +01:00
Leendert de Borst
85bbb0ab78 Add new tests to all autofill credential match/filter logic methods (#1332) 2025-11-06 12:30:40 +01:00
Leendert de Borst
343b1baedb Tweak android autofill matching logic so all tests pass (#1332) 2025-11-06 12:30:40 +01:00
Leendert de Borst
fb5d4dfeca Improve Android autofill matching to prevent android packages resulting in false positives (#1332) 2025-11-06 12:30:40 +01:00
Leendert de Borst
661f0574c5 Add show search title option to Android autofill (#1332) 2025-11-06 12:30:40 +01:00
Leendert de Borst
a4a1c0b097 Update Android autofill to properly detect email type fiels (#1332) 2025-11-06 12:30:40 +01:00
Leendert de Borst
02eae4c04f Merge branch 'main' of https://github.com/aliasvault/aliasvault
* 'main' of https://github.com/aliasvault/aliasvault:
  Update Android credential provider label (#1332)
  Add Android build script (#1332)
  Bump app build number for unlock screen animation fix (#1332)
  Update unlock loading animation position (#1332)
  Update GitHub workflow Android gradlew memory (#1332)
  Update build-and-submit scripts (#1332)
  Add iOS fastlane CLI build and submit script (#1332)
  Bump version to 0.24.0 stable (#1332)
  New Crowdin updates (#1323)
2025-11-06 12:26:39 +01:00
Leendert de Borst
d7d9d2d99f Update Android credential provider label (#1332) 2025-11-05 22:34:27 +01:00
Leendert de Borst
40b368bc7e Add Android build script (#1332) 2025-11-05 22:34:27 +01:00
Leendert de Borst
360ce0c9eb Bump app build number for unlock screen animation fix (#1332) 2025-11-05 22:34:27 +01:00
Leendert de Borst
074b2e48fa Update unlock loading animation position (#1332) 2025-11-05 22:34:27 +01:00
Leendert de Borst
ae4ea3cb80 Update GitHub workflow Android gradlew memory (#1332) 2025-11-05 22:34:27 +01:00
Leendert de Borst
a8a51f65c3 Update build-and-submit scripts (#1332) 2025-11-05 22:34:27 +01:00
Leendert de Borst
b5264eae69 Add iOS fastlane CLI build and submit script (#1332) 2025-11-05 22:34:27 +01:00
Leendert de Borst
d380ce7946 Bump version to 0.24.0 stable (#1332) 2025-11-05 22:34:27 +01:00
Leendert de Borst
75797fe829 New Crowdin updates (#1323)
* New translations en.json (French)
Update translations from Crowdin [ci skip]

* New translations en.json (Spanish)
Update translations from Crowdin [ci skip]

* New translations en.json (Catalan)
Update translations from Crowdin [ci skip]

* New translations en.json (German)
Update translations from Crowdin [ci skip]

* New translations en.json (Finnish)
Update translations from Crowdin [ci skip]

* New translations en.json (Hebrew)
Update translations from Crowdin [ci skip]

* New translations en.json (Italian)
Update translations from Crowdin [ci skip]

* New translations en.json (Dutch)
Update translations from Crowdin [ci skip]

* New translations en.json (Polish)
Update translations from Crowdin [ci skip]

* New translations en.json (Russian)
Update translations from Crowdin [ci skip]

* New translations en.json (Swedish)
Update translations from Crowdin [ci skip]

* New translations en.json (Turkish)
Update translations from Crowdin [ci skip]

* New translations en.json (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations en.json (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations en.json (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations home.en.resx (Dutch)
Update translations from Crowdin [ci skip]

* New translations general.en.resx (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations en.json (Dutch)
Update translations from Crowdin [ci skip]

* New translations en.json (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations en.json (Finnish)
Update translations from Crowdin [ci skip]

* New translations en.json (Dutch)
Update translations from Crowdin [ci skip]

* New translations sharedresources.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations en.json (Polish)
Update translations from Crowdin [ci skip]

* New translations passwordsettingspopup.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations addedit.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations delete.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations general.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations validationmessages.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations en.json (Polish)
Update translations from Crowdin [ci skip]

* New translations en.json (Polish)
Update translations from Crowdin [ci skip]

* New translations showrecoverycodes.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations unlock.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations apps.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations general.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations importexport.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations en.json (Polish)
Update translations from Crowdin [ci skip]

* New translations home.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations topmenu.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations addedit.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations clipboardcountdownbar.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations en.json (Polish)
Update translations from Crowdin [ci skip]

* New translations login.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations totpcodes.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations totpviewer.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations recentemails.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations quickvaultunlocksection.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations twofactorauthenticationsection.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations createnewidentitywidget.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations unlock.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations addedit.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations addedit.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations home.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations view.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations home.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations errorvaultdecrypt.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations welcome.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations enable2fa.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations en.json (Polish)
Update translations from Crowdin [ci skip]

* New translations disable2fa.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations strings.xml (Polish)
Update translations from Crowdin [ci skip]

* New translations strings.xml (Russian)
Update translations from Crowdin [ci skip]

* New translations pendingmigrations.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (Polish)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (Russian)
Update translations from Crowdin [ci skip]

* New translations topmenu.en.resx (French)
Update translations from Crowdin [ci skip]

* New translations topmenu.en.resx (Spanish)
Update translations from Crowdin [ci skip]

* New translations topmenu.en.resx (Catalan)
Update translations from Crowdin [ci skip]

* New translations topmenu.en.resx (German)
Update translations from Crowdin [ci skip]

* New translations topmenu.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations topmenu.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations topmenu.en.resx (Italian)
Update translations from Crowdin [ci skip]

* New translations topmenu.en.resx (Dutch)
Update translations from Crowdin [ci skip]

* New translations topmenu.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations topmenu.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations topmenu.en.resx (Swedish)
Update translations from Crowdin [ci skip]

* New translations topmenu.en.resx (Turkish)
Update translations from Crowdin [ci skip]

* New translations topmenu.en.resx (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations topmenu.en.resx (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations topmenu.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations topmenu.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations en.json (Polish)
Update translations from Crowdin [ci skip]

* New translations login.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations totpcodes.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations emailmodal.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations recentemails.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations topmenu.en.resx (Dutch)
Update translations from Crowdin [ci skip]

* New translations topmenu.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations unlock.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations home.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations apps.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations sync.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations enable2fa.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations disable2fa.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations en.json (Polish)
Update translations from Crowdin [ci skip]

* New translations en.json (Polish)
Update translations from Crowdin [ci skip]

* New translations pendingmigrations.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations en.json (Polish)
Update translations from Crowdin [ci skip]

* New translations twofactorauthenticationsection.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations topmenu.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations security.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations en.json (Polish)
Update translations from Crowdin [ci skip]

* New translations disable2fa.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations en.json (Polish)
Update translations from Crowdin [ci skip]

* New translations topmenu.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations unlock.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations sync.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations en.json (Polish)
Update translations from Crowdin [ci skip]

* New translations totpcodes.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations editemailformrow.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations en.json (Polish)
Update translations from Crowdin [ci skip]

* New translations en.json (Polish)
Update translations from Crowdin [ci skip]

* New translations totpcodes.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations importservices.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations footer.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations start.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations en.json (Polish)
Update translations from Crowdin [ci skip]

* New translations strings.xml (Polish)
Update translations from Crowdin [ci skip]

* New translations en.json (Polish)
Update translations from Crowdin [ci skip]

* New translations en.json (Polish)
Update translations from Crowdin [ci skip]

* New translations en.json (Hebrew)
Update translations from Crowdin [ci skip]

* New translations topmenu.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations en.json (Hebrew)
Update translations from Crowdin [ci skip]

* New translations en.json (Polish)
Update translations from Crowdin [ci skip]

* New translations quickvaultunlocksection.en.resx (French)
Update translations from Crowdin [ci skip]

* New translations quickvaultunlocksection.en.resx (Spanish)
Update translations from Crowdin [ci skip]

* New translations quickvaultunlocksection.en.resx (Catalan)
Update translations from Crowdin [ci skip]

* New translations quickvaultunlocksection.en.resx (German)
Update translations from Crowdin [ci skip]

* New translations quickvaultunlocksection.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations quickvaultunlocksection.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations quickvaultunlocksection.en.resx (Italian)
Update translations from Crowdin [ci skip]

* New translations quickvaultunlocksection.en.resx (Dutch)
Update translations from Crowdin [ci skip]

* New translations quickvaultunlocksection.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations quickvaultunlocksection.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations quickvaultunlocksection.en.resx (Swedish)
Update translations from Crowdin [ci skip]

* New translations quickvaultunlocksection.en.resx (Turkish)
Update translations from Crowdin [ci skip]

* New translations quickvaultunlocksection.en.resx (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations quickvaultunlocksection.en.resx (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations quickvaultunlocksection.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations unlock.en.resx (French)
Update translations from Crowdin [ci skip]

* New translations unlock.en.resx (Spanish)
Update translations from Crowdin [ci skip]

* New translations unlock.en.resx (Catalan)
Update translations from Crowdin [ci skip]

* New translations unlock.en.resx (German)
Update translations from Crowdin [ci skip]

* New translations unlock.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations unlock.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations unlock.en.resx (Italian)
Update translations from Crowdin [ci skip]

* New translations unlock.en.resx (Dutch)
Update translations from Crowdin [ci skip]

* New translations unlock.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations unlock.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations unlock.en.resx (Swedish)
Update translations from Crowdin [ci skip]

* New translations unlock.en.resx (Turkish)
Update translations from Crowdin [ci skip]

* New translations unlock.en.resx (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations unlock.en.resx (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations unlock.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations quickvaultunlocksection.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations unlock.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations quickvaultunlocksection.en.resx (Dutch)
Update translations from Crowdin [ci skip]

* New translations unlock.en.resx (Dutch)
Update translations from Crowdin [ci skip]

* New translations quickvaultunlocksection.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations unlock.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations en.json (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations quickvaultunlocksection.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations topmenu.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations quickvaultunlocksection.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations unlock.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations addedit.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations home.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations view.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations en.json (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations strings.xml (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations clipboardcountdownbar.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations view.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations strings.xml (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations strings.xml (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations en.json (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations en.json (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations en.json (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations login.en.resx (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations apierrors.en.resx (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations en.json (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations strings.xml (Chinese Simplified)
Update translations from Crowdin [ci skip]
2025-11-03 22:13:49 +01:00
Leendert de Borst
3fd279e032 Update F-Droid README.md 2025-11-03 21:24:23 +01:00
Leendert de Borst
df50a1ad47 Update fdroid test build version 2025-11-03 21:17:32 +01:00
Leendert de Borst
5d96c44ea9 Update fdroid docker compose config 2025-11-03 18:31:23 +01:00
Leendert de Borst
e7baadda9f Add fdroid build script helpers 2025-11-03 17:55:01 +01:00
Leendert de Borst
376d38ef07 Add f-droid local build scripts for debugging 2025-11-03 17:46:59 +01:00
Leendert de Borst
97d8d4d15d Remove react-native-keyboard-controller package as it conflicts with F-droid build 2025-11-02 22:16:09 +01:00
Leendert de Borst
4010631d73 Remove credential play services as we don't support < Android 14 autofill 2025-11-02 21:05:09 +01:00
Leendert de Borst
03d8e15eeb Improve iOS quick passkey autofill to work on iOS 18+ 2025-11-02 20:41:13 +01:00
Leendert de Borst
7f01e2a9a0 Bump app build versions 2025-11-02 00:13:02 +01:00
Leendert de Borst
d0334e9033 Add version artifacts for build processes 2025-11-01 22:59:34 +01:00
Leendert de Borst
0aa99572e3 Fix iOS dependency tree 2025-11-01 22:39:54 +01:00
Leendert de Borst
51f666d238 Update activity_loading.xml 2025-11-01 20:11:24 +01:00
Leendert de Borst
fc60426e0f Tweak alert dialogs and app startup migration 2025-11-01 20:09:38 +01:00
Leendert de Borst
520a6ef4b2 Update credential_provider_config.xml 2025-11-01 19:43:23 +01:00
Leendert de Borst
deacb9ada9 Bump version to 0.24.0-beta 2025-11-01 19:26:49 +01:00
Leendert de Borst
25383dd615 Update Android native loading view to be off-center like iOS 2025-11-01 19:24:37 +01:00
Leendert de Borst
6daed9b31b Update ARCHITECTURE.md 2025-11-01 18:07:18 +01:00
Leendert de Borst
8c40c786f7 Add passkey operations to security diagram 2025-11-01 17:52:49 +01:00
Leendert de Borst
a5025d3262 Update security architecture diagram 2025-11-01 16:53:58 +01:00
Leendert de Borst
c932a24f21 Browser extension webauthn tweaks 2025-11-01 15:41:17 +01:00
Leendert de Borst
0ebc75dcea Update project.pbxproj, add missing static files 2025-11-01 13:42:41 +01:00
Leendert de Borst
0d62b4af55 Improve webauthn popup close robustness 2025-11-01 13:42:22 +01:00
Leendert de Borst
9de879a387 Prevent WebAuthn interception during prefetch for Safari 2025-11-01 12:36:56 +01:00
Leendert de Borst
519fe9ba30 Fix browser extension linting 2025-11-01 09:04:07 +01:00
Leendert de Borst
6aaca60049 Update WebAuthn implementation to be compatible with Firefox 2025-11-01 09:02:53 +01:00
Leendert de Borst
17a248d0d7 Update browser extension passkey title/subtitle to match mobile apps 2025-11-01 08:39:00 +01:00
Leendert de Borst
c8b42aecc1 Update kotlin insert query for passkeys 2025-11-01 08:31:25 +01:00
Leendert de Borst
577c452c88 Tweak add edit popup button margins for iOS 26+ 2025-11-01 08:03:28 +01:00
Leendert de Borst
6a3e294aae Make web app JsInterop more robust to prevent race conditions 2025-10-31 22:52:37 +01:00
Leendert de Borst
81ad1ec5e7 Update quick vault unlock explanation text 2025-10-31 22:42:20 +01:00
Leendert de Borst
8c3007b6f4 Update VaultStoreTest.kt (#1286) 2025-10-31 22:41:24 +01:00
Leendert de Borst
e4cd9fe6ed Update filepreview modal to support image panning/zooming (#1286) 2025-10-31 22:41:24 +01:00
Leendert de Borst
6dc5e4806b Fix multi private domain encoding issue with all-in-one docker container (#1287) 2025-10-31 21:59:10 +01:00
Leendert de Borst
7a72416e83 Fix email domain field issues that did not properly show multiple domains (#1287) 2025-10-31 21:59:10 +01:00
Leendert de Borst
727d7e6025 Update LoadingOverlayOverview.swift to avoid obstructing face id (#1325) 2025-10-31 18:37:30 +01:00
Leendert de Borst
506bc37eac Move initialize status to off center to prevent faceid occlusion (#1325) 2025-10-31 18:37:30 +01:00
Leendert de Borst
a69b1049a6 Improve sqlite flow in browser extension (#1325) 2025-10-31 18:37:30 +01:00
Leendert de Borst
7f3508030e Refactor (#1325) 2025-10-31 18:37:30 +01:00
Leendert de Borst
0b2fd61fd0 Tweak mobile app credential save animation (#1325) 2025-10-31 18:37:30 +01:00
Leendert de Borst
b76654c9d2 Update kotlin sqlite implementation (#1325) 2025-10-31 18:37:30 +01:00
Leendert de Borst
68c7453c08 Use Swift sqlite backup API instead of manual cursor transfer (#1325) 2025-10-31 18:37:30 +01:00
Leendert de Borst
dbbc6a96db Improve persist db to encrypted storage Kotlin flow (#1325) 2025-10-31 18:37:30 +01:00
Leendert de Borst
f6ad5667ef Update Vaultstore+Query.swift to use proper vacuum for persist instead of raw table copy to preserve FK etc. (#1325) 2025-10-31 18:37:30 +01:00
Leendert de Borst
ed8642de41 Refactor vault persist to separate method (#1325) 2025-10-31 18:37:30 +01:00
Leendert de Borst
bcd3673a00 Remove expo sqlite lib, update iOS pods, fix iOS quick autofill sanity checks (#1325) 2025-10-31 18:37:30 +01:00
Leendert de Borst
c180fdf505 Tweak mobile app logout flow to suppress session expired warnings (#1325) 2025-10-31 18:37:30 +01:00
Leendert de Borst
3664f5bc20 Tweak browser extension logout flow to suppress session errors (#1325) 2025-10-31 18:37:30 +01:00
Leendert de Borst
c134c2642a Improve light/dark mode switcher, remove duplicate notes label 2025-10-30 09:21:35 +01:00
Leendert de Borst
003ef1f096 Update Android passkey layout merge issue 2025-10-29 12:08:02 +01:00
Leendert de Borst
386da4b227 Add Polish language option to all apps (#1321) 2025-10-29 10:33:12 +01:00
Leendert de Borst
7ca816a60e Fix mobile app translation file syntax 2025-10-29 10:23:31 +01:00
Leendert de Borst
932d79fd85 New Crowdin updates
* New translations activesessionssection.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations deleteaccountsection.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations quickvaultunlocksection.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations home.en.resx (French)
Update translations from Crowdin [ci skip]

* New translations home.en.resx (Spanish)
Update translations from Crowdin [ci skip]

* New translations home.en.resx (Catalan)
Update translations from Crowdin [ci skip]

* New translations home.en.resx (German)
Update translations from Crowdin [ci skip]

* New translations home.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations home.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations home.en.resx (Italian)
Update translations from Crowdin [ci skip]

* New translations home.en.resx (Dutch)
Update translations from Crowdin [ci skip]

* New translations home.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations home.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations home.en.resx (Swedish)
Update translations from Crowdin [ci skip]

* New translations home.en.resx (Turkish)
Update translations from Crowdin [ci skip]

* New translations home.en.resx (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations home.en.resx (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations home.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations welcome.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations en.json (French)
Update translations from Crowdin [ci skip]

* New translations en.json (Spanish)
Update translations from Crowdin [ci skip]

* New translations en.json (Catalan)
Update translations from Crowdin [ci skip]

* New translations en.json (German)
Update translations from Crowdin [ci skip]

* New translations en.json (Finnish)
Update translations from Crowdin [ci skip]

* New translations en.json (Hebrew)
Update translations from Crowdin [ci skip]

* New translations en.json (Italian)
Update translations from Crowdin [ci skip]

* New translations en.json (Dutch)
Update translations from Crowdin [ci skip]

* New translations en.json (Polish)
Update translations from Crowdin [ci skip]

* New translations en.json (Russian)
Update translations from Crowdin [ci skip]

* New translations en.json (Swedish)
Update translations from Crowdin [ci skip]

* New translations en.json (Turkish)
Update translations from Crowdin [ci skip]

* New translations en.json (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations en.json (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations en.json (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations en.json (Polish)
Update translations from Crowdin [ci skip]

* New translations en.json (Polish)
Update translations from Crowdin [ci skip]

* New translations termsandconditionsstep.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations defaultpasswordsettings.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations passwordsettingspopup.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations quickvaultunlocksection.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations recentauthlogssection.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations showrecoverycodes.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations twofactorauthenticationsection.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations createnewidentitywidget.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations searchwidget.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations footer.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations topmenu.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations forgotpassword.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations setup.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations en.json (Polish)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (Polish)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (Polish)
Update translations from Crowdin [ci skip]

* New translations en.json (Finnish)
Update translations from Crowdin [ci skip]

* New translations en.json (Polish)
Update translations from Crowdin [ci skip]

* New translations totpcodes.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations start.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations unlock.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (Finnish)
Update translations from Crowdin [ci skip]

* New translations en.json (Polish)
Update translations from Crowdin [ci skip]

* New translations login.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations passwordstep.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations usernamestep.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations totpviewer.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations topmenu.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations home.en.resx (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations apierrors.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations en.json (Finnish)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (Finnish)
Update translations from Crowdin [ci skip]

* New translations clipboardcountdownbar.en.resx (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations home.en.resx (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations login.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations register.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations totpcodes.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations emailmodal.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations emailpreview.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations emailrow.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations recentemails.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations editemailformrow.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations importservicecard.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations deleteaccountsection.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations passwordchangesection.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations quickvaultunlocksection.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations recentauthlogssection.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations showrecoverycodes.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations footer.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations home.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations apps.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations general.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations importexport.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations security.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations creating.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations errorvaultdecrypt.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations vaultdecryptionprogress.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations welcome.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations changepassword.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations resetvault.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations resetvaultsection.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations pendingmigrations.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations defaultpasswordsettings.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations importservices.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations passwordsettingspopup.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations activesessionssection.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations twofactorauthenticationsection.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations createnewidentitywidget.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations searchwidget.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations forgotpassword.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations logout.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations setup.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations start.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations unlock.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations addedit.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations delete.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations home.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations home.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations view.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations apierrors.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations deleteaccount.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations enable2fa.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations en.json (Finnish)
Update translations from Crowdin [ci skip]

* New translations disable2fa.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations clipboardcountdownbar.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations quickvaultunlocksection.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations general.en.resx (Finnish)
Update translations from Crowdin [ci skip]
2025-10-29 10:02:12 +01:00
Leendert de Borst
d8ef99207f Update login page logo margins for Android to prevent pill occlusion (#1319) 2025-10-29 00:14:50 +01:00
Leendert de Borst
c7182e7a21 Tweak app layout margins for iOS 26+ (#1319) 2025-10-29 00:14:50 +01:00
Leendert de Borst
fa451dc2cc Add passkey architecture documentation 2025-10-28 14:17:28 +01:00
Leendert de Borst
85d89b2b2c Bump wxt version (#1316) 2025-10-28 13:47:05 +01:00
Leendert de Borst
7d22bc34a7 Remove old argon2 types in mobile app 2025-10-28 13:23:21 +01:00
Leendert de Borst
b1a06cb2da Update docs (#1313) 2025-10-28 13:03:36 +01:00
Leendert de Borst
e5a15b2486 Update VersionCompatibility tests (#1313) 2025-10-28 13:03:36 +01:00
Leendert de Borst
c1e8a9b44e Add semantic versioning checks to vault SQL migrations to allow backwards compatible changes (#1313) 2025-10-28 13:03:36 +01:00
Leendert de Borst
d628e9cc4c New Crowdin updates (#1297)
* New translations creating.en.resx (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations creating.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations errorvaultdecrypt.en.resx (French)
Update translations from Crowdin [ci skip]

* New translations errorvaultdecrypt.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations errorvaultdecrypt.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations errorvaultdecrypt.en.resx (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations errorvaultdecrypt.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations vaultdecryptionprogress.en.resx (French)
Update translations from Crowdin [ci skip]

* New translations vaultdecryptionprogress.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations vaultdecryptionprogress.en.resx (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations vaultdecryptionprogress.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations sync.en.resx (French)
Update translations from Crowdin [ci skip]

* New translations sync.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations sync.en.resx (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations sync.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations welcome.en.resx (French)
Update translations from Crowdin [ci skip]

* New translations welcome.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations welcome.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations welcome.en.resx (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations welcome.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations apierrors.en.resx (French)
Update translations from Crowdin [ci skip]

* New translations apierrors.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations apierrors.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations apierrors.en.resx (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations apierrors.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations changepassword.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations changepassword.en.resx (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations changepassword.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations deleteaccount.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations deleteaccount.en.resx (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations deleteaccount.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations enable2fa.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations enable2fa.en.resx (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations enable2fa.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations validationmessages.en.resx (French)
Update translations from Crowdin [ci skip]

* New translations validationmessages.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations validationmessages.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations validationmessages.en.resx (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations validationmessages.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations en.json (French)
Update translations from Crowdin [ci skip]

* New translations en.json (Spanish)
Update translations from Crowdin [ci skip]

* New translations en.json (Catalan)
Update translations from Crowdin [ci skip]

* New translations en.json (German)
Update translations from Crowdin [ci skip]

* New translations en.json (Finnish)
Update translations from Crowdin [ci skip]

* New translations en.json (Hebrew)
Update translations from Crowdin [ci skip]

* New translations en.json (Italian)
Update translations from Crowdin [ci skip]

* New translations en.json (Dutch)
Update translations from Crowdin [ci skip]

* New translations en.json (Polish)
Update translations from Crowdin [ci skip]

* New translations en.json (Russian)
Update translations from Crowdin [ci skip]

* New translations en.json (Swedish)
Update translations from Crowdin [ci skip]

* New translations en.json (Turkish)
Update translations from Crowdin [ci skip]

* New translations en.json (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations en.json (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations en.json (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations resetvault.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations resetvault.en.resx (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations resetvault.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations resetvaultsection.en.resx (French)
Update translations from Crowdin [ci skip]

* New translations resetvaultsection.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations resetvaultsection.en.resx (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations resetvaultsection.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations disable2fa.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations disable2fa.en.resx (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations disable2fa.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations infoplist.strings (Polish)
Update translations from Crowdin [ci skip]

* New translations infoplist.strings (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations infoplist.strings (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations strings.xml (French)
Update translations from Crowdin [ci skip]

* New translations strings.xml (Spanish)
Update translations from Crowdin [ci skip]

* New translations strings.xml (Catalan)
Update translations from Crowdin [ci skip]

* New translations strings.xml (German)
Update translations from Crowdin [ci skip]

* New translations strings.xml (Finnish)
Update translations from Crowdin [ci skip]

* New translations strings.xml (Hebrew)
Update translations from Crowdin [ci skip]

* New translations strings.xml (Italian)
Update translations from Crowdin [ci skip]

* New translations strings.xml (Dutch)
Update translations from Crowdin [ci skip]

* New translations strings.xml (Polish)
Update translations from Crowdin [ci skip]

* New translations strings.xml (Russian)
Update translations from Crowdin [ci skip]

* New translations strings.xml (Swedish)
Update translations from Crowdin [ci skip]

* New translations strings.xml (Turkish)
Update translations from Crowdin [ci skip]

* New translations strings.xml (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations strings.xml (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations strings.xml (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (French)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (Spanish)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (Catalan)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (German)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (Finnish)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (Hebrew)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (Italian)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (Dutch)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (Polish)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (Russian)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (Swedish)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (Turkish)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations pendingmigrations.en.resx (French)
Update translations from Crowdin [ci skip]

* New translations pendingmigrations.en.resx (Spanish)
Update translations from Crowdin [ci skip]

* New translations pendingmigrations.en.resx (Catalan)
Update translations from Crowdin [ci skip]

* New translations pendingmigrations.en.resx (German)
Update translations from Crowdin [ci skip]

* New translations pendingmigrations.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations pendingmigrations.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations pendingmigrations.en.resx (Italian)
Update translations from Crowdin [ci skip]

* New translations pendingmigrations.en.resx (Dutch)
Update translations from Crowdin [ci skip]

* New translations pendingmigrations.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations pendingmigrations.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations pendingmigrations.en.resx (Swedish)
Update translations from Crowdin [ci skip]

* New translations pendingmigrations.en.resx (Turkish)
Update translations from Crowdin [ci skip]

* New translations pendingmigrations.en.resx (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations pendingmigrations.en.resx (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations pendingmigrations.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (French)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (Spanish)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (Catalan)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (German)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (Finnish)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (Hebrew)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (Italian)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (Dutch)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (Polish)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (Russian)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (Swedish)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (Turkish)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations en.json (Italian)
Update translations from Crowdin [ci skip]

* New translations login.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations register.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations usernamestep.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations totpcodes.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations totpviewer.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations emailmodal.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations emailpreview.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations recentemails.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations editemailformrow.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations addedit.en.resx (Italian)
Update translations from Crowdin [ci skip]

* New translations home.en.resx (Italian)
Update translations from Crowdin [ci skip]

* New translations view.en.resx (Italian)
Update translations from Crowdin [ci skip]

* New translations changepassword.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations deleteaccount.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations en.json (French)
Update translations from Crowdin [ci skip]

* New translations en.json (Spanish)
Update translations from Crowdin [ci skip]

* New translations en.json (Catalan)
Update translations from Crowdin [ci skip]

* New translations en.json (German)
Update translations from Crowdin [ci skip]

* New translations en.json (Finnish)
Update translations from Crowdin [ci skip]

* New translations en.json (Hebrew)
Update translations from Crowdin [ci skip]

* New translations en.json (Italian)
Update translations from Crowdin [ci skip]

* New translations en.json (Dutch)
Update translations from Crowdin [ci skip]

* New translations en.json (Polish)
Update translations from Crowdin [ci skip]

* New translations en.json (Russian)
Update translations from Crowdin [ci skip]

* New translations en.json (Swedish)
Update translations from Crowdin [ci skip]

* New translations en.json (Turkish)
Update translations from Crowdin [ci skip]

* New translations en.json (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations en.json (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations en.json (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations resetvault.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations strings.xml (Italian)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (Italian)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (Italian)
Update translations from Crowdin [ci skip]

* New translations importservicecard.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations importservices.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations deleteaccount.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations enable2fa.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations disable2fa.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations clipboardcountdownbar.en.resx (French)
Update translations from Crowdin [ci skip]

* New translations clipboardcountdownbar.en.resx (Spanish)
Update translations from Crowdin [ci skip]

* New translations clipboardcountdownbar.en.resx (Catalan)
Update translations from Crowdin [ci skip]

* New translations clipboardcountdownbar.en.resx (German)
Update translations from Crowdin [ci skip]

* New translations clipboardcountdownbar.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations clipboardcountdownbar.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations clipboardcountdownbar.en.resx (Italian)
Update translations from Crowdin [ci skip]

* New translations clipboardcountdownbar.en.resx (Dutch)
Update translations from Crowdin [ci skip]

* New translations clipboardcountdownbar.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations clipboardcountdownbar.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations clipboardcountdownbar.en.resx (Swedish)
Update translations from Crowdin [ci skip]

* New translations clipboardcountdownbar.en.resx (Turkish)
Update translations from Crowdin [ci skip]

* New translations clipboardcountdownbar.en.resx (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations clipboardcountdownbar.en.resx (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations clipboardcountdownbar.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations en.json (Dutch)
Update translations from Crowdin [ci skip]

* New translations importservicecard.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations importservices.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations addedit.en.resx (Dutch)
Update translations from Crowdin [ci skip]

* New translations home.en.resx (Dutch)
Update translations from Crowdin [ci skip]

* New translations view.en.resx (Dutch)
Update translations from Crowdin [ci skip]

* New translations en.json (Dutch)
Update translations from Crowdin [ci skip]

* New translations strings.xml (Dutch)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (Dutch)
Update translations from Crowdin [ci skip]

* New translations clipboardcountdownbar.en.resx (Dutch)
Update translations from Crowdin [ci skip]

* New translations strings.xml (Dutch)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (Dutch)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (Dutch)
Update translations from Crowdin [ci skip]

* New translations en.json (Finnish)
Update translations from Crowdin [ci skip]

* New translations en.json (Polish)
Update translations from Crowdin [ci skip]

* New translations en.json (Polish)
Update translations from Crowdin [ci skip]

* New translations sharedresources.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations en.json (Finnish)
Update translations from Crowdin [ci skip]

* New translations importservices.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations addedit.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations home.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations view.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations en.json (Polish)
Update translations from Crowdin [ci skip]

* New translations strings.xml (Finnish)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (Finnish)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (Finnish)
Update translations from Crowdin [ci skip]

* New translations clipboardcountdownbar.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations sharedresources.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations en.json (Polish)
Update translations from Crowdin [ci skip]

* New translations sync.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations validationmessages.en.resx (Polish)
Update translations from Crowdin [ci skip]

* New translations en.json (Polish)
Update translations from Crowdin [ci skip]

* New translations infoplist.strings (Polish)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (Polish)
Update translations from Crowdin [ci skip]

* New translations en.json (Hebrew)
Update translations from Crowdin [ci skip]

* New translations addedit.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations home.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations en.json (Hebrew)
Update translations from Crowdin [ci skip]

* New translations strings.xml (Hebrew)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (Hebrew)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (Hebrew)
Update translations from Crowdin [ci skip]

* New translations en.json (Hebrew)
Update translations from Crowdin [ci skip]

* New translations addedit.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations home.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations view.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations en.json (Hebrew)
Update translations from Crowdin [ci skip]

* New translations strings.xml (Hebrew)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (Hebrew)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (Hebrew)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (Finnish)
Update translations from Crowdin [ci skip]

* New translations home.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations importexport.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations infoplist.strings (Finnish)
Update translations from Crowdin [ci skip]

* New translations strings.xml (Polish)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (Finnish)
Update translations from Crowdin [ci skip]

* New translations apps.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations general.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations importexport.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations security.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations en.json (Polish)
Update translations from Crowdin [ci skip]

* New translations strings.xml (Polish)
Update translations from Crowdin [ci skip]

* New translations general.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations creating.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations errorvaultdecrypt.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations vaultdecryptionprogress.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations sync.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations welcome.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations en.json (Polish)
Update translations from Crowdin [ci skip]

* New translations pendingmigrations.en.resx (Finnish)
Update translations from Crowdin [ci skip]
2025-10-28 11:22:04 +01:00
Leendert de Borst
3a50b6e85b Persist custom API url during logout on Android (#1311) 2025-10-28 11:21:43 +01:00
Leendert de Borst
9641514b3b Add attachments credential filter to all clients (#1309) 2025-10-28 11:21:33 +01:00
Leendert de Borst
975ae9bd74 Pass information from JSInterop as base64 strings instead of byte arrays to bypass .NET issue (#1307) 2025-10-27 22:15:42 +01:00
Leendert de Borst
3bead0bbfc Merge branch 'main' of https://github.com/aliasvault/aliasvault
* 'main' of https://github.com/aliasvault/aliasvault:
  Improve FormDetector.ts to avoid overwriting already filled in fields (#1305)
  Fix private email domain check by doing exact comparison instead of wildcard (#1303)
2025-10-27 15:25:38 +01:00
Leendert de Borst
a77417c990 Cleanup mobile app translations 2025-10-27 15:23:53 +01:00
Leendert de Borst
dc48ac23dd Improve FormDetector.ts to avoid overwriting already filled in fields (#1305) 2025-10-27 15:19:08 +01:00
Leendert de Borst
4428f428dc Fix private email domain check by doing exact comparison instead of wildcard (#1303) 2025-10-27 15:08:49 +01:00
Leendert de Borst
5a6d317e31 Add manual clipboard clear button if automatic clipboard clear fails (#1301) 2025-10-27 14:53:57 +01:00
Leendert de Borst
6f24fd6453 Remove .map files from JS dist libs 2025-10-27 13:32:21 +01:00
Leendert de Borst
af60b2e22d Merge branch 'main' of https://github.com/aliasvault/aliasvault
* 'main' of https://github.com/aliasvault/aliasvault:
  Bump the npm_and_yarn group across 2 directories with 1 update
  Update native iOS search filter to use AND/OR (#1298)
  Improve credential search to use and/or in browser extension and mobile app (#1298)
2025-10-27 13:15:07 +01:00
Leendert de Borst
85642eab64 Update Docker static asset caching configuration 2025-10-27 13:15:00 +01:00
dependabot[bot]
8aad6f845e Bump the npm_and_yarn group across 2 directories with 1 update
Bumps the npm_and_yarn group with 1 update in the /shared/password-generator directory: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).
Bumps the npm_and_yarn group with 1 update in the /shared/vault-sql directory: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).


Updates `vite` from 6.3.6 to 6.4.1
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/create-vite@6.4.1/packages/vite)

Updates `vite` from 7.1.5 to 7.1.12
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/create-vite@6.4.1/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.4.1
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: vite
  dependency-version: 7.1.12
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-27 13:10:29 +01:00
Leendert de Borst
4ba2c8e6ab Update native iOS search filter to use AND/OR (#1298) 2025-10-27 13:10:15 +01:00
Leendert de Borst
9da88cc7e7 Improve credential search to use and/or in browser extension and mobile app (#1298) 2025-10-27 13:10:15 +01:00
Leendert de Borst
e67fce5e39 Add cache busting to AliasVault.Client dynamically loaded JS dist libs 2025-10-27 13:10:07 +01:00
Leendert de Borst
3c94eb873d Improve browser extension auth settings UI (#1293) 2025-10-27 12:02:24 +01:00
Leendert de Borst
16418e1513 Update hyperlinks to be relative in admin (#1295) 2025-10-27 12:02:09 +01:00
Leendert de Borst
7ddb035f1a Merge pull request #1277 from aliasvault/520-feature-request-add-support-for-passkeys
Add support for passkeys
2025-10-27 11:52:11 +01:00
Leendert de Borst
f5c88639a6 Rebuild shared libraries (#520) 2025-10-27 11:32:55 +01:00
Leendert de Borst
d0baf8b6e0 Merge pull request #1292 from aliasvault/1257-add-russian-language-option
Add Russian language to apps
2025-10-27 11:29:26 +01:00
Leendert de Borst
6269b7ec7c Merge branch 'main' into 1257-add-russian-language-option 2025-10-27 11:26:55 +01:00
Leendert de Borst
5ee8d7a8f4 Add Portugese (Brazilian) as language option to apps (#1262) 2025-10-27 11:24:17 +01:00
Leendert de Borst
c1d41b3d8d Update IdentityHelperUtils.test.ts (#520) 2025-10-27 11:22:23 +01:00
Leendert de Borst
5fddf753f8 Merge branch '520-feature-request-add-support-for-passkeys' of https://github.com/aliasvault/aliasvault into 520-feature-request-add-support-for-passkeys
* '520-feature-request-add-support-for-passkeys' of https://github.com/aliasvault/aliasvault:
  Bump the npm_and_yarn group across 3 directories with 1 update
  Bump vite
  Update installation docs (#1280)
2025-10-27 11:12:39 +01:00
Leendert de Borst
712a9a0182 Update IdentityHelperUtils.ts (#520) 2025-10-27 11:12:26 +01:00
Leendert de Borst
f43f3cc51f Merge branch 'main' into 520-feature-request-add-support-for-passkeys 2025-10-27 10:57:40 +01:00
Leendert de Borst
99dc808de4 Clear CredentialIdentityStore contents on logout (#520) 2025-10-27 10:48:58 +01:00
Leendert de Borst
f97efea681 Add backup rules for CredentialIdentityStore kotlin implementation (#520) 2025-10-27 10:28:52 +01:00
Leendert de Borst
9ec245c102 Add initial credential identity store sync for iOS if store is empty (#520) 2025-10-27 10:25:18 +01:00
Leendert de Borst
fc9c59b077 Update iOS autofill setup setting link to general settings page (#520) 2025-10-27 10:10:16 +01:00
Leendert de Borst
5fe2c3ab4c Update PasskeyAuthenticator.ts (#520) 2025-10-27 09:35:38 +01:00
Leendert de Borst
2c4af6c85b Update AliasVaultPasskeyProvider.test.ts (#520) 2025-10-26 21:23:40 +01:00
Leendert de Borst
99a24c23e4 Cleanup PasskeyAuthenticator.ts (#520) 2025-10-26 21:20:08 +01:00
Leendert de Borst
1427693c1d Cleanup log statements (#520) 2025-10-26 21:05:41 +01:00
Leendert de Borst
619f402ca0 Refactor webauthn.ts to use proper response type (#520) 2025-10-26 21:03:02 +01:00
Leendert de Borst
71ddbbe3d2 Streamline passkey display name creation (#520) 2025-10-26 20:51:52 +01:00
Leendert de Borst
ad086689dd Add passkey indicator to browser extension autofill popup (#520) 2025-10-26 17:06:04 +01:00
Leendert de Borst
dc114c6bfa Add mobile app login flow abort when manually skipped flow (#520) 2025-10-26 15:15:59 +01:00
Leendert de Borst
9843142419 Add passkey origin fallback for native apps (#520) 2025-10-26 14:50:06 +01:00
dependabot[bot]
9ba698bb74 Bump the npm_and_yarn group across 3 directories with 1 update
Bumps the npm_and_yarn group with 1 update in the /apps/browser-extension directory: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).
Bumps the npm_and_yarn group with 1 update in the /shared/identity-generator directory: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).
Bumps the npm_and_yarn group with 1 update in the /shared/password-generator directory: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).


Updates `vite` from 6.3.6 to 6.4.1
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/create-vite@6.4.1/packages/vite)

Updates `vite` from 6.3.6 to 6.4.1
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/create-vite@6.4.1/packages/vite)

Updates `vite` from 6.3.6 to 6.4.1
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/create-vite@6.4.1/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.4.1
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: vite
  dependency-version: 6.4.1
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: vite
  dependency-version: 6.4.1
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-26 00:00:39 +02:00
Leendert de Borst
5185dfa41d Refactor CredentialIdentityStore scaffolding (#520) 2025-10-25 17:06:34 +02:00
Leendert de Borst
ea4d72ceca Update NativeVaultManager.kt to offload register credentials in background task (#520) 2025-10-25 16:41:47 +02:00
Leendert de Borst
b2206cae8f Refactor VaultStore kotlin to separate components (#520) 2025-10-25 16:39:16 +02:00
Leendert de Borst
1f8fb2ea39 Add DateHelpers to Kotlin passkey flow (#520) 2025-10-25 15:37:52 +02:00
Leendert de Borst
b2476ab5c5 Add date normalization to all clients (#520) 2025-10-24 23:49:54 +02:00
Leendert de Borst
866c8e7834 Update authenticatorAttachment setting (#520) 2025-10-24 21:37:02 +02:00
Leendert de Borst
fb01b75f3d Persist encryption key when enabling biometrics on Android (#520) 2025-10-24 21:25:04 +02:00
Leendert de Borst
8b05d2aafa Update initialize.tsx to redirect if no faceid (#520) 2025-10-24 20:43:20 +02:00
Leendert de Borst
4d54649c3a Add sanity check warning if biometric auth is not enabled (#520) 2025-10-24 17:30:45 +02:00
Leendert de Borst
a5c8ff91b5 Cleanup (#520) 2025-10-24 17:05:05 +02:00
Leendert de Borst
5164c705c2 Only show skip button during skippable phases (#520) 2025-10-24 16:24:00 +02:00
Leendert de Borst
c00088d955 Add client server version check to Android sync (#520) 2025-10-24 15:36:40 +02:00
Leendert de Borst
6698771fc4 Add explicit biometric auth for passkey create and authenticate flows (#520) 2025-10-24 15:28:20 +02:00
Leendert de Borst
665662982c Cleanup todos and refactor detekt issues (#520) 2025-10-23 19:04:46 +02:00
Leendert de Borst
c7d3a9ea1e Update Android credential filter to only include entries with username and pass (#520) 2025-10-22 22:30:24 +02:00
Leendert de Borst
c24598c151 Update autofill settings docs (#520) 2025-10-22 22:14:54 +02:00
Leendert de Borst
b995ec728c Update colors.xml (#520) 2025-10-22 22:02:17 +02:00
dependabot[bot]
234193e99b Bump vite
Bumps the npm_and_yarn group with 1 update in the /shared/vault-sql directory: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).


Updates `vite` from 7.1.5 to 7.1.11
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.1.11/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 7.1.11
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-22 16:41:57 +02:00
Leendert de Borst
af06bbfd12 Update Android light/dark mode system bar theme (#520) 2025-10-21 22:33:15 +02:00
Leendert de Borst
646416c069 Improve passkey replace flow UI and navigation structure (#520) 2025-10-21 22:06:23 +02:00
Leendert de Borst
219bc88e30 Add passkey replace flow (#520) 2025-10-21 16:42:54 +02:00
Leendert de Borst
020f11d3a4 Fix passkey create handshake return type for Chrome CredMan (#520)
WIP
2025-10-21 16:42:53 +02:00
Leendert de Borst
4cea8aae5e Fix passkey create in Firefox Android (#520) 2025-10-21 15:28:27 +02:00
Leendert de Borst
1db63bbc6b Add loading animation to Android as separate template (#520) 2025-10-20 22:05:02 +02:00
Leendert de Borst
00c230a92e Update iOS passkey create flow to prevent 25308 error (#520) 2025-10-20 21:48:57 +02:00
Leendert de Borst
868bdc9aa2 Add theme colors (#520) 2025-10-20 20:13:41 +02:00
Leendert de Borst
4c9de1fc2f Add passkey create activity view (#520) 2025-10-20 19:58:05 +02:00
Leendert de Borst
3adc796295 Update favicon extraction to properly detect nulls (#520) 2025-10-20 19:06:43 +02:00
Leendert de Borst
30d223aba6 Commit created passkey to vault (#520) 2025-10-20 18:50:34 +02:00
Leendert de Borst
6eb43c4f8b Add Android passkey registration scaffolding (#520) 2025-10-20 15:35:02 +02:00
Leendert de Borst
f0260622fd Refactor PasskeyAuthenticationActivity.kt (#520) 2025-10-20 13:24:37 +02:00
Leendert de Borst
a0269f90f3 Tweak initialize timeout skip button (#520) 2025-10-20 11:18:44 +02:00
Leendert de Borst
11ea12499b Simplify PasskeyAuthenticationActivity.kt (#520) 2025-10-20 10:44:05 +02:00
Leendert de Borst
4cff77b927 Update connection skip icon and title (#520) 2025-10-20 10:44:05 +02:00
Leendert de Borst
fa517c38c0 Cleanup (#520) 2025-10-20 10:44:05 +02:00
Leendert de Borst
5e1f899a5e Refactor Android credential manager passkey implementation to conform to spec (#520) 2025-10-20 10:44:05 +02:00
Leendert de Borst
e1318e2147 Add quick unlock type enum to show custom label in view (#520) 2025-10-19 15:23:40 +02:00
Leendert de Borst
ee9f3ca0f9 Tweak quick autofill flow on iOS with explicit loading view (#520) 2025-10-18 20:53:07 +02:00
Leendert de Borst
026cfb91e9 Tweak Android passkey authentication scaffolding (#520)
WIP
2025-10-18 20:53:07 +02:00
Leendert de Borst
0b78e5fa77 Fix UUID lookup casing (#520) 2025-10-17 16:18:04 +02:00
Leendert de Borst
d5b11cc34c Add passkey authentication scaffolding (#520) 2025-10-17 16:07:36 +02:00
Leendert de Borst
ddf34a2d30 Fix first time login authorization header overwrite bug (#520) 2025-10-17 15:33:07 +02:00
Leendert de Borst
37acd87c44 Fix context menu translations which prevented clickhandler from working (#520) 2025-10-17 13:55:48 +02:00
Leendert de Borst
efaa7962cb Tweak if available iOS flags (#520) 2025-10-17 13:04:35 +02:00
Leendert de Borst
d4f0579eea Update comments (#520) 2025-10-17 11:36:27 +02:00
Leendert de Borst
ac78bb1afc Update UI (#520) 2025-10-17 11:08:56 +02:00
Leendert de Borst
8d3034676b Tweak native vault sync flow called from React Native (#520) 2025-10-16 23:09:27 +02:00
Leendert de Borst
d9588acf00 Refactor shared methods to VaultUtils framework, cleanup unused methods (#520) 2025-10-16 22:29:33 +02:00
Leendert de Borst
f213b1ac57 Refactor todos (#520) 2025-10-16 21:15:09 +02:00
Leendert de Borst
5f49013235 Make iOS vault init more robust to prevent cold boot errors (#520) 2025-10-16 17:34:00 +02:00
Leendert de Borst
bb0bee7870 Refresh iOS autofill identities on every vault mutation (#520) 2025-10-16 11:26:11 +02:00
Leendert de Borst
7c64e656ff Refactor (#520) 2025-10-16 11:24:04 +02:00
Leendert de Borst
90e846674e Cleanup (#520) 2025-10-16 11:05:29 +02:00
Leendert de Borst
3d684e59ea Use displayname override for credential title instead of passkey displayname (#520) 2025-10-15 21:22:36 +02:00
Leendert de Borst
a4d728c9e5 Update SqliteClient.tsx to also mark passkey as soft deleted when credential is deleted (#520) 2025-10-15 21:12:46 +02:00
Leendert de Borst
74e8f1b840 Add passkey to credential view and AddEdit page (#520) 2025-10-15 21:10:41 +02:00
Leendert de Borst
774afaf522 Add credential filter and passkey recognition to web app (#520) 2025-10-15 18:21:54 +02:00
Leendert de Borst
92623493e8 Tweak UI (#520) 2025-10-14 21:53:38 +02:00
Leendert de Borst
53c4242342 Add passkey instructions to iOS autofill settings page (#520) 2025-10-14 21:05:44 +02:00
Leendert de Borst
ed5c436084 Refactor (#520) 2025-10-14 19:43:40 +02:00
Leendert de Borst
dd2b08a4a3 Add react native credential filter and passkey indicators (#520) 2025-10-14 19:00:52 +02:00
Leendert de Borst
dad709fc20 Refactor passkey logic implementation (#520) 2025-10-14 17:01:36 +02:00
Leendert de Borst
8964b1080d Update passkey schema (#520) 2025-10-14 15:32:57 +02:00
Leendert de Borst
5ec9e53449 Cleanup (#520) 2025-10-14 13:08:22 +02:00
Leendert de Borst
18182cdda2 Refresh credential list after credential delete (#520) 2025-10-14 12:49:32 +02:00
Leendert de Borst
33ed79e951 Add server min version supported check to native iOS sync implementation (#520) 2025-10-14 12:42:10 +02:00
Leendert de Borst
c044a27a3f Add error code throw and detection to native vault sync logic implementation (#520) 2025-10-14 11:59:16 +02:00
Leendert de Borst
95753e3fa9 Add explicit server offline error message to passkey create flow (#520) 2025-10-13 21:04:48 +02:00
Leendert de Borst
9a3df923b5 Update passkey registration UI, refactor folder structure (#520) 2025-10-13 20:49:26 +02:00
Leendert de Borst
c41bf8a921 Add passkey replace flow (#520) 2025-10-13 15:20:01 +02:00
Leendert de Borst
d93ec10cc9 Add title input field to passkey create screen (#520) 2025-10-13 14:24:50 +02:00
Leendert de Borst
385ee841dd Update terminology (#520) 2025-10-13 14:18:19 +02:00
Leendert de Borst
7c533de8f3 Add PRF evaluation support on passkey registration (#520) 2025-10-13 13:55:57 +02:00
Leendert de Borst
92fe915d0f Refactor (#520) 2025-10-12 23:26:18 +02:00
Leendert de Borst
1905078bdc Refactor PRF (#520) 2025-10-12 22:55:36 +02:00
Leendert de Borst
974315ed8c Add PRF support to iOS passkey mechanism (#520) 2025-10-12 20:06:12 +02:00
Leendert de Borst
d8b8fc7922 Update unlock error message margins (#520) 2025-10-12 17:14:59 +02:00
Leendert de Borst
795adab0dc Update passkey provider selection UI (#520) 2025-10-12 14:29:54 +02:00
Leendert de Borst
020d1bcfa1 Fix credential card selection popup positioning (#520) 2025-10-12 14:27:00 +02:00
Leendert de Borst
1efc06eaac Add SwiftUI translations into VaultUI project directly (#520) 2025-10-12 13:55:44 +02:00
Leendert de Borst
19c7da5dc6 Update passkey create UI (#520) 2025-10-11 19:59:34 +02:00
Leendert de Borst
e85a3cab7f Update passkey registration UI (#520) 2025-10-11 18:32:58 +02:00
Leendert de Borst
0ab5ca9377 Update loading indicator feedback (#520) 2025-10-11 17:15:07 +02:00
Leendert de Borst
48000b76eb Update swift loading animation (#520) 2025-10-10 23:16:50 +02:00
Leendert de Borst
c27300bcb3 Fix favicon extraction in passkey create flow (#520) 2025-10-10 22:03:42 +02:00
Leendert de Borst
48acb81492 Implement Swift passkey create persist flow (#520) 2025-10-10 18:33:49 +02:00
Leendert de Borst
09f61bd7a2 Cleanup RN AsyncStorage calls (#520) 2025-10-10 16:35:46 +02:00
Leendert de Borst
4bfe69750c Implement working vault mutate native flow (#520) 2025-10-10 13:05:05 +02:00
Leendert de Borst
afab20f59b Move vault sync/mutate to swift/kotlin layer (#520) 2025-10-10 12:50:24 +02:00
Leendert de Borst
3bc3c165f6 Move webapi calls to native swift/kotlin layer (#520) 2025-10-10 10:26:58 +02:00
Leendert de Borst
bc6f492208 Update local passkey create logic with proper date formatting (#520) 2025-10-09 16:20:34 +02:00
Leendert de Borst
fa4c80858c Implement swift passkey create logic and unittest (#520) 2025-10-09 15:52:38 +02:00
Leendert de Borst
6c94ed5193 Add passkey registration screen detection (#520) 2025-10-09 14:34:48 +02:00
Leendert de Borst
3658b606c2 Sync iOS CredentialIdentityStore via React Native callback (#520) 2025-10-09 13:37:18 +02:00
Leendert de Borst
01eee844de Implement iOS passkey selection callback (#520) 2025-10-08 19:16:25 +02:00
Leendert de Borst
ac7ea057d4 Show passkey specific credential view list on "show more" (#520) 2025-10-08 18:49:22 +02:00
Leendert de Borst
00023ea944 Make passkey authentication work on iOS (#520) 2025-10-08 16:43:15 +02:00
Leendert de Borst
bd78cfe778 Make webauthn quick fill suggestion work (#520) 2025-10-08 15:45:10 +02:00
Leendert de Borst
c2b6e8af1e Fix iOS passkey data type parsing (#520) 2025-10-08 15:30:05 +02:00
Leendert de Borst
f0fdfcdf19 Add passkeys to credential store for quicktype (#520) 2025-10-08 13:56:28 +02:00
Leendert de Borst
479e32ddac Enable iOS QuickType password autofill for iOS 26+ (#520) 2025-10-08 13:06:41 +02:00
Leendert de Borst
4661e36ef4 Add iOS passkey scaffolding (#520) 2025-10-08 12:43:59 +02:00
Leendert de Borst
26eb965b1d Add React Native passkey scaffolding (#520) 2025-10-08 12:26:43 +02:00
Leendert de Borst
ae4aeb6f45 Create fido_metadata.json (#520) 2025-10-08 10:36:04 +02:00
Leendert de Borst
5b62b035ee Add iOS passkey logic scaffolding (#520) 2025-10-07 16:46:11 +02:00
Leendert de Borst
8416c7c15f Store PRF secret in separate column (#520) 2025-10-07 13:27:45 +02:00
Leendert de Borst
1a9e1967ed Add FK repair script to migration to fix older vaults (#520) 2025-10-07 13:16:16 +02:00
Leendert de Borst
9156923f92 Add separate PrfKey column, recreate migrations (#520) 2025-10-07 10:44:46 +02:00
Leendert de Borst
b8a15930cd Fix passkey IsDeleted flag when deleting credential, fix favicon null handling (#520) 2025-10-07 09:45:28 +02:00
Leendert de Borst
544fea83b0 Refactor browser extension component directories (#520) 2025-10-06 23:41:16 +02:00
Leendert de Borst
032417aeec Tweak passkey card display (#520) 2025-10-06 23:35:28 +02:00
Leendert de Borst
30e213919d Tweak passkey create/authenticate screen UI (#520) 2025-10-06 23:25:30 +02:00
Leendert de Borst
98e52b8756 Add PRF extension support to webauthn passkey implementation (#520) 2025-10-06 18:43:27 +02:00
Leendert de Borst
240a0854be Clear pending redirect when opening main popup without redirect (#520) 2025-10-05 14:41:25 +02:00
Leendert de Borst
57f6ec1be7 Add passkey provider enable/disable toggle for specific website (#520) 2025-10-05 14:01:24 +02:00
Leendert de Borst
df9eacdf13 Remove passkeys list page (#520) 2025-10-05 12:36:27 +02:00
Leendert de Borst
eebf7aff41 Add filter to credential list (#520) 2025-10-05 12:28:26 +02:00
Leendert de Borst
10c9478238 Update credential card / details / add-edit to include passkeys (#520) 2025-10-05 11:12:38 +02:00
Leendert de Borst
3b1199d2db Cleanup passkey create and authenticate flows (#520) 2025-10-05 10:05:53 +02:00
Leendert de Borst
405b44383f Update passkey create flow to support replacing existing entries (#520) 2025-10-05 09:59:24 +02:00
Leendert de Borst
cf90721197 Update installation docs (#1280) 2025-10-04 13:50:41 +02:00
Leendert de Borst
b62078f97e Add passkey settings page (#520) 2025-10-03 15:55:49 +02:00
Leendert de Borst
74f4bc0ee9 Add modal layout for passkey popup actions (#520) 2025-10-03 14:17:10 +02:00
Leendert de Borst
7a65678ba2 Add unlock redirect hook with path restore (#520) 2025-10-03 13:41:30 +02:00
Leendert de Borst
2a208b5cff Refactoring (#520) 2025-10-03 12:48:13 +02:00
Leendert de Borst
6a0e8fc5ca Add AAGUID (#520) 2025-10-03 11:47:57 +02:00
Leendert de Borst
dad476548e Refactor base64url usage (#520) 2025-10-03 11:39:23 +02:00
Leendert de Borst
1cf49eed7e Integrate passkey create/get with vault storage (#520) 2025-10-03 09:34:53 +02:00
Leendert de Borst
04dfd41281 Update vault sql passkey model (#520) 2025-10-03 07:03:44 +02:00
Leendert de Borst
b31c94c582 Refactor WebAuthnInterceptor.ts (#520) 2025-10-02 17:39:41 +02:00
Leendert de Borst
5569202b9a Refactor AliasVaultPasskeyProvider.ts (#520) 2025-10-02 17:33:27 +02:00
Leendert de Borst
0ffb14ba0a Refactor (#520) 2025-10-02 16:21:39 +02:00
Leendert de Borst
db227894b6 Add webauthn types and return all required metadata fields (#520) 2025-10-02 13:44:03 +02:00
Leendert de Borst
46e217f523 Add response types and more unit tests (#520) 2025-10-02 11:16:19 +02:00
Leendert de Borst
d40d2d9c43 Do not close passkey popup windows for testing purposes (#520) 2025-10-02 10:52:15 +02:00
Leendert de Borst
1a5ed775de Update PasskeyHandler.ts (#520) 2025-10-02 10:42:29 +02:00
Leendert de Borst
a16d773686 Update passkey selection UI (#520) 2025-10-02 10:26:19 +02:00
Leendert de Borst
4ebb02795a Refactor passkey creation and retrieval to dedicated class (#520) 2025-10-02 07:18:35 +02:00
Leendert de Borst
5a70e7e20e Merge branch 'main' into 520-feature-request-add-support-for-passkeys
* main:
  Tweak browser extension server connection error flow
  Update FormDetector with additional birthdate field names (#1278)
2025-10-01 14:20:17 +02:00
Leendert de Borst
18ee97f6e5 Tweak browser extension server connection error flow 2025-10-01 14:20:02 +02:00
Leendert de Borst
4ffac949ee Set signCount to 0 (#520) 2025-10-01 13:33:45 +02:00
Leendert de Borst
db15c9ab25 Update FormDetector with additional birthdate field names (#1278) 2025-10-01 13:14:02 +02:00
Leendert de Borst
0ca4a7b8c7 Add signCount increment flow (#520) 2025-09-30 21:33:23 +02:00
Leendert de Borst
364093e789 Add packaged attestation support (#520) 2025-09-30 21:04:03 +02:00
Leendert de Borst
61c124364a Update passkey add and retrieve data flow (#520) 2025-09-30 20:51:45 +02:00
Leendert de Borst
0f62d15d74 Update PasskeyAuthenticate.tsx (#520) 2025-09-30 16:46:29 +02:00
Leendert de Borst
536c020bfb Make create passkey param passing work (#520) 2025-09-30 16:25:19 +02:00
Leendert de Borst
3c91103c3a Update webauthn keys (#520) 2025-09-30 16:12:42 +02:00
Leendert de Borst
3b196afe26 Merge branch 'main' into 520-feature-request-add-support-for-passkeys
* main:
  Refactor browser extension logout to not await webapi revoke
2025-09-30 15:44:15 +02:00
Leendert de Borst
68934ba48c Refactor browser extension logout to not await webapi revoke 2025-09-30 15:39:02 +02:00
Leendert de Borst
03ecc472b7 Update passkeys migration with new sql lib structure (#520) 2025-09-30 13:01:49 +02:00
Leendert de Borst
b103aab646 Merge branch 'main' into 520-feature-request-add-support-for-passkeys
* main: (31 commits)
  Fix versioning suffix mismatch in browser extension package.json (#1274)
  Fix versioning suffix in wxt.config.ts (#1274)
  Delete nginx.conf (superseded by port specific configs)
  Update AppInfo.ts (#1274)
  Bump app alpha build version (#1274)
  Fix app initialize and reinitialize layout (#1274)
  Update bump version to use semver for app store marketing version (#1274)
  Update login/unlock.tsx (#1274)
  Use API version for communicating app version with API instead of full version (#1274)
  Bump version to 0.24.0 alpha for testing mobile app (#1274)
  Update versioning to support stage suffix (#1274)
  Add browser extension vault upgrade test (#1274)
  Update VaultMessageHandler.ts (#1274)
  Simplify mobile app error handling preventing duplicate popups (#1274)
  Update error type location (#1274)
  Mobile app logout flow and AppContext refactor (#1274)
  Check both dev and build folders for browser extension test (#1274)
  Add GitHub workflow for running e2e browser extension tests (#1274)
  Update BrowserExtensionPlaywrightTest.cs (#1274)
  Make ChromeExtensionTests.cs tests pass locally (#1274)
  ...
2025-09-30 13:00:09 +02:00
Leendert de Borst
2d43858457 Fix versioning suffix mismatch in browser extension package.json (#1274) 2025-09-29 16:59:29 +02:00
Leendert de Borst
6b63b6b45d Fix versioning suffix in wxt.config.ts (#1274) 2025-09-29 16:46:43 +02:00
Leendert de Borst
1c9573eeb9 Delete nginx.conf (superseded by port specific configs) 2025-09-29 15:16:24 +02:00
Leendert de Borst
97141af1f1 Update AppInfo.ts (#1274) 2025-09-29 13:41:28 +02:00
Leendert de Borst
82a20e1fc5 Bump app alpha build version (#1274) 2025-09-29 13:32:46 +02:00
Leendert de Borst
75eea4162d Fix app initialize and reinitialize layout (#1274) 2025-09-29 13:32:10 +02:00
Leendert de Borst
5eb28d3ddf Update bump version to use semver for app store marketing version (#1274) 2025-09-29 13:32:10 +02:00
Leendert de Borst
257174c459 Update login/unlock.tsx (#1274) 2025-09-29 13:32:09 +02:00
Leendert de Borst
37c09c2c55 Use API version for communicating app version with API instead of full version (#1274) 2025-09-29 13:32:09 +02:00
Leendert de Borst
85348610a6 Bump version to 0.24.0 alpha for testing mobile app (#1274) 2025-09-29 13:32:09 +02:00
Leendert de Borst
9941473937 Update versioning to support stage suffix (#1274) 2025-09-29 13:32:09 +02:00
Leendert de Borst
afcef4f3bb Add browser extension vault upgrade test (#1274) 2025-09-28 10:56:51 +02:00
Leendert de Borst
a44e4102db Update VaultMessageHandler.ts (#1274) 2025-09-28 10:56:51 +02:00
Leendert de Borst
63c5d61616 Simplify mobile app error handling preventing duplicate popups (#1274) 2025-09-28 10:56:51 +02:00
Leendert de Borst
14cbce97d4 Update error type location (#1274) 2025-09-28 10:56:51 +02:00
Leendert de Borst
e5d924a094 Mobile app logout flow and AppContext refactor (#1274) 2025-09-28 10:56:51 +02:00
Leendert de Borst
46c364bbb4 Check both dev and build folders for browser extension test (#1274) 2025-09-28 10:56:51 +02:00
Leendert de Borst
7eef9b986f Add GitHub workflow for running e2e browser extension tests (#1274) 2025-09-28 10:56:51 +02:00
Leendert de Borst
af384ff6d1 Update BrowserExtensionPlaywrightTest.cs (#1274) 2025-09-28 10:56:51 +02:00
Leendert de Borst
3a62554fe2 Make ChromeExtensionTests.cs tests pass locally (#1274) 2025-09-28 10:56:51 +02:00
Leendert de Borst
717894c21c Update NavigationContext.tsx (#1274) 2025-09-28 10:56:51 +02:00
Leendert de Borst
2f8bc97a5a Prevent logout loop (#1274) 2025-09-28 10:56:51 +02:00
Leendert de Borst
5215a0bdb8 Add logout event emitter (#1274) 2025-09-28 10:56:51 +02:00
Leendert de Borst
624296da0d Add AppContext (#1274) 2025-09-28 10:56:51 +02:00
Leendert de Borst
c6028c4f32 Update translations (#1274) 2025-09-28 10:56:51 +02:00
Leendert de Borst
2e4caf8261 Remove obsolete vault status checks (#1274) 2025-09-28 10:56:51 +02:00
Leendert de Borst
5aea4aa6a1 Refactor browser extension logout flow (#1274) 2025-09-28 10:56:51 +02:00
Leendert de Borst
cad95e779d Improve vault upgrade unknown vault version flow in browser extension (#1274) 2025-09-28 10:56:51 +02:00
Leendert de Borst
c88b0d1d8a Add translations for client outdated in browser extension and mobile app (#1271) 2025-09-25 17:37:46 +02:00
Leendert de Borst
60371796f3 Update vault upgrade translations and web app mappings (#1271) 2025-09-25 17:26:06 +02:00
Leendert de Borst
ac3941f4aa Make vault upgrade pages show latest compatible version (#1271) 2025-09-25 17:26:06 +02:00
Leendert de Borst
dbae407df6 Add passkey proof-of-concept browser extension scaffolding (#520) 2025-09-25 15:50:04 +02:00
Leendert de Borst
181a27e94e Add passkey client db migration (#520) 2025-09-25 11:42:53 +02:00
Leendert de Borst
9a367acbdc Autofocus password field on web app unlock screen (#1269) 2025-09-25 06:28:27 +02:00
Leendert de Borst
938e8869f2 Update reinitialize.tsx (#1267) 2025-09-24 16:35:28 +02:00
Leendert de Borst
a9203600c1 Mark offline mode for manual unlock usecase correctly (#1267) 2025-09-24 16:35:28 +02:00
Leendert de Borst
ad2028e473 Update offline banner UX (#1267) 2025-09-24 16:35:28 +02:00
Leendert de Borst
7cb7c02bb2 Add explicit offline mode override button during app sync flow (#1267) 2025-09-24 16:35:28 +02:00
Leendert de Borst
836e33f821 Merge pull request #1265 from aliasvault/1264-bug-autofill-sometimes-shows-too-much-irrelevant-suggestions
Autofill sometimes shows too much irrelevant suggestions
2025-09-24 11:23:02 +02:00
Leendert de Borst
8d37e8ddbc Hide Android autofill items that do not have email/username/password info (#1264) 2025-09-23 17:48:58 +02:00
Leendert de Borst
b71f0b6a27 Update Filter.ts (#1264) 2025-09-23 17:17:35 +02:00
Leendert de Borst
375b2e3c12 Add service name that is being searched for to Android autofill list (#1264) 2025-09-23 12:40:04 +02:00
Leendert de Borst
216875ef05 Add common two level public TLDs to autofill matching implementations (#1264) 2025-09-23 10:55:24 +02:00
Leendert de Borst
ceaea5f214 Add max postgres pool size limits to avoid concurrency errors (#1260) 2025-09-23 09:36:20 +02:00
Leendert de Borst
fe20fb0bdb Update TwoFactorAuthController.cs (#1260) 2025-09-23 09:36:20 +02:00
Leendert de Borst
6a35ad4f98 Remove AuthLog UserAgent column, update DeviceIdentifier column length (#1260) 2025-09-23 09:36:20 +02:00
Leendert de Borst
a6cd33733f Update NDK version for full 16kb page size support (#1258) 2025-09-21 18:26:01 +02:00
Leendert de Borst
4b988e78ff Bump Android app build version (#1258) 2025-09-21 18:26:01 +02:00
Leendert de Borst
b96f01089f Update Android dependencies for 16kb page support (#1258) 2025-09-21 18:26:01 +02:00
Leendert de Borst
4875c50c90 Add Russian language to apps (#1257) 2025-09-20 10:08:38 +02:00
Leendert de Borst
8458a8cd19 Update docs with update instructions 2025-09-19 15:05:00 +02:00
Leendert de Borst
becec9dc95 Update changelogs (#1254) 2025-09-19 15:04:21 +02:00
Leendert de Borst
a4bdb22bf4 Add liquid glass icon to Safari browser extension launcher (#1254) 2025-09-19 15:03:55 +02:00
889 changed files with 67049 additions and 20849 deletions

View File

@@ -44,6 +44,18 @@ runs:
- name: Setup Android SDK
uses: android-actions/setup-android@v3
- name: Configure Gradle JVM memory for CI
run: |
mkdir -p android
cat >> android/gradle.properties <<EOF
org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m -XX:+HeapDumpOnOutOfMemoryError
org.gradle.daemon.performance.disable-logging=true
org.gradle.daemon=true
org.gradle.caching=true
EOF
shell: bash
working-directory: apps/mobile-app
- name: Build JS bundle (Expo)
run: |
mkdir -p build

View File

@@ -86,3 +86,53 @@ jobs:
timeout_minutes: 60
max_attempts: 3
command: cd apps/server && dotnet test Tests/AliasVault.E2ETests --no-build --verbosity normal --filter "FullyQualifiedName~.E2ETests.Tests.Client.Shard${{ matrix.shard }}."
browser-extension-tests:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 9.0.x
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
- name: Install dependencies
run: dotnet workload install wasm-tools
- name: Build server
working-directory: apps/server
run: dotnet build
- name: Build browser extension
working-directory: apps/browser-extension
run: |
npm install
npm run build:chrome
- name: Start dev database
run: ./install.sh configure-dev-db start
- name: Ensure browsers are installed
working-directory: apps/server
run: pwsh Tests/AliasVault.E2ETests/bin/Debug/net9.0/playwright.ps1 install --with-deps
- name: Run ExtensionTests with retry
uses: nick-fields/retry@v3
with:
timeout_minutes: 60
max_attempts: 3
command: cd apps/server && dotnet test Tests/AliasVault.E2ETests --no-build --verbosity normal --filter "Category=ExtensionTests"
- name: Upload Test Results
if: always()
uses: actions/upload-artifact@v4
with:
name: extension-test-results
path: TestResults-Extension.xml

3
.gitignore vendored
View File

@@ -431,3 +431,6 @@ temp
# Android keystore file (for publishing to Google Play)
*.keystore
# Safari extension build files
apps/browser-extension/safari-xcode/AliasVault/build

View File

@@ -23,5 +23,7 @@
"path": "../shared"
}
],
"settings": {}
"settings": {
"java.configuration.updateBuildConfiguration": "disabled"
}
}

View File

@@ -1,5 +1,5 @@
# SECURITY.md
This document describes the encryption algorithms used by AliasVault in order to keep its user data secure.
# ARCHITECTURE.md
This document provides a high-level overview of the AliasVault architecture, focusing on the encryption algorithms used to ensure the security of user data.
## Overview
AliasVault features a [zero-knowledge architecture](https://en.wikipedia.org/wiki/Zero-knowledge_service) and uses a combination of encryption algorithms to protect the data of its users.
@@ -10,14 +10,18 @@ All data is encrypted at rest and in transit. This ensures that even if the Alia
the user's data remains secure.
## Encryption algorithms
The following encryption algorithms are used by AliasVault:
The following encryption algorithms and standards are used by AliasVault:
- [Argon2id](#argon2id)
- [SRP](#srp)
- [AES-GCM](#aes-gcm)
- [RSA-OAEP](#rsa-oaep)
### Core Vault Encryption
- [Argon2id](#argon2id) - Key derivation from master password
- [SRP](#srp) - Secure authentication protocol
- [AES-GCM](#aes-gcm) - Vault data encryption
Below is a detailed explanation of each encryption algorithm.
### Additional Features
- [RSA-OAEP](#rsa-oaep) - Email encryption
- [Passkeys (WebAuthn)](#passkeys-webauthn) - Passwordless authentication
Below is a detailed explanation of each encryption algorithm and standard.
For more information about how these algorithms are specifically used in AliasVault, see the [Architecture Documentation](https://docs.aliasvault.net/architecture) section on the documentation site.
@@ -93,3 +97,39 @@ This implementation ensures that:
- Even if the server is compromised, email contents remain encrypted and unreadable
More information about RSA-OAEP can be found on the [RSA-OAEP](https://en.wikipedia.org/wiki/Optimal_asymmetric_encryption_padding) Wikipedia page.
### Passkeys (WebAuthn)
AliasVault includes a virtual passkey authenticator that is fully compatible with the WebAuthn Level 2 specification. This enables users to securely store and use passkeys across their devices through the encrypted vault, providing a seamless and secure alternative to traditional password authentication.
#### Implementation Details
AliasVault implements passkey functionality across all supported platforms:
- **Browser Extension**: Virtual authenticator using the Web Crypto API
- **iOS**: Native Swift implementation using CryptoKit
- **Android**: Native Kotlin implementation using AndroidKeyStore
All implementations follow the WebAuthn Level 2 specification and use:
- ES256 (ECDSA P-256) for key pair generation
- CBOR/COSE encoding for attestation objects
- Proper authenticator data with WebAuthn flags (UP, UV, BE, BS, AT)
- AliasVault AAGUID (Authenticator Attestation GUID): `a11a5faa-9f32-4b8c-8c5d-2f7d13e8c942`
- Self-attestation (packed format) or none attestation
- Sign count always 0 for syncable passkeys
- BE/BS flags indicating backup-eligible and backed-up status
#### Key Features
1. **Zero-Knowledge Passkey Storage**: Passkey private keys are stored as encrypted entries in the user's vault alongside passwords and other credentials. The server never has access to the unencrypted private keys.
2. **Cross-Platform Sync**: Passkeys automatically sync across all devices where the user's vault is accessible, enabling seamless authentication on any platform (browser extension, iOS app, or Android app).
3. **PRF Extension Support**: Implements the hmac-secret (PRF) extension, allowing relying parties to derive additional secrets from passkeys for encryption keys or other cryptographic operations. Currently supported on browser extension and iOS; Android support is pending due to limited Android API support.
4. **Standards Compliance**: Full adherence to WebAuthn Level 2 specification ensures compatibility with all WebAuthn-compliant relying parties and services.
#### Security Benefits
- Private keys remain encrypted in the vault at all times
- All passkey operations (key generation, signing) occur on the client device
- Passkeys benefit from the same zero-knowledge architecture as passwords
- Cross-device sync provides convenience without compromising security
- Eliminates phishing risks through cryptographic domain binding
More information about WebAuthn can be found on the [WebAuthn specification](https://www.w3.org/TR/webauthn-2/) page.

View File

@@ -102,7 +102,7 @@ AliasVault takes security seriously and implements various measures to protect y
- Zero-knowledge architecture ensures the server never has access to your unencrypted data
For detailed information about our encryption implementation and security architecture, see the following documents:
- [SECURITY.md](SECURITY.md)
- [ARCHITECTURE.md](ARCHITECTURE.md)
- [Security Architecture Diagram](https://docs.aliasvault.net/architecture)
## Features & Roadmap

1
apps/.version/major.txt Normal file
View File

@@ -0,0 +1 @@
0

1
apps/.version/minor.txt Normal file
View File

@@ -0,0 +1 @@
24

1
apps/.version/patch.txt Normal file
View File

@@ -0,0 +1 @@
0

1
apps/.version/suffix.txt Normal file
View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@
0.24.0

View File

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
"name": "aliasvault-browser-extension",
"description": "AliasVault Browser Extension",
"private": true,
"version": "0.23.2",
"version": "0.24.0",
"type": "module",
"scripts": {
"dev:chrome": "wxt -b chrome",
@@ -67,6 +67,6 @@
"tailwindcss": "^3.4.17",
"typescript": "^5.6.3",
"vite-plugin-static-copy": "^2.3.2",
"wxt": "^0.20.6"
"wxt": "^0.20.11"
}
}

View File

@@ -25,6 +25,10 @@
CE0CAFE02D81A9F8006174AB /* icon in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFD82D81A9F8006174AB /* icon */; };
CE0CAFE12D81A9F8006174AB /* assets in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFD92D81A9F8006174AB /* assets */; };
CE0CAFE22D81A9F8006174AB /* src in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFDA2D81A9F8006174AB /* src */; };
CE0E5BB02E7D8BA600515C80 /* AliasVault.icon in Resources */ = {isa = PBXBuildFile; fileRef = CE0E5BAF2E7D8BA600515C80 /* AliasVault.icon */; };
CEA194E42EB6221B00EAB23B /* webauthn.js in Resources */ = {isa = PBXBuildFile; fileRef = CEA194E32EB6221B00EAB23B /* webauthn.js */; };
CEA194E72EB6248D00EAB23B /* offscreen.html in Resources */ = {isa = PBXBuildFile; fileRef = CEA194E52EB6248D00EAB23B /* offscreen.html */; };
CEA194E82EB6248D00EAB23B /* offscreen.js in Resources */ = {isa = PBXBuildFile; fileRef = CEA194E62EB6248D00EAB23B /* offscreen.js */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -76,6 +80,10 @@
CE0CAFD82D81A9F8006174AB /* icon */ = {isa = PBXFileReference; lastKnownFileType = folder; name = icon; path = "../../../dist/safari-mv2/icon"; sourceTree = "<group>"; };
CE0CAFD92D81A9F8006174AB /* assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = assets; path = "../../../dist/safari-mv2/assets"; sourceTree = "<group>"; };
CE0CAFDA2D81A9F8006174AB /* src */ = {isa = PBXFileReference; lastKnownFileType = folder; name = src; path = "../../../dist/safari-mv2/src"; sourceTree = "<group>"; };
CE0E5BAF2E7D8BA600515C80 /* AliasVault.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = AliasVault.icon; sourceTree = "<group>"; };
CEA194E32EB6221B00EAB23B /* webauthn.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; name = webauthn.js; path = "/Users/user/Projects/AliasVault/apps/browser-extension/dist/safari-mv2/webauthn.js"; sourceTree = "<absolute>"; };
CEA194E52EB6248D00EAB23B /* offscreen.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; name = offscreen.html; path = "/Users/user/Projects/AliasVault/apps/browser-extension/dist/safari-mv2/offscreen.html"; sourceTree = "<absolute>"; };
CEA194E62EB6248D00EAB23B /* offscreen.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; name = offscreen.js; path = "/Users/user/Projects/AliasVault/apps/browser-extension/dist/safari-mv2/offscreen.js"; sourceTree = "<absolute>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -117,6 +125,7 @@
CE0CAFA52D81A9F7006174AB /* AliasVault */ = {
isa = PBXGroup;
children = (
CE0E5BAF2E7D8BA600515C80 /* AliasVault.icon */,
CE0CAFA62D81A9F7006174AB /* AppDelegate.swift */,
CE0CAFB22D81A9F7006174AB /* ViewController.swift */,
CE0CAFB42D81A9F7006174AB /* Main.storyboard */,
@@ -154,6 +163,9 @@
CE0CAFD22D81A9F8006174AB /* Resources */ = {
isa = PBXGroup;
children = (
CEA194E52EB6248D00EAB23B /* offscreen.html */,
CEA194E62EB6248D00EAB23B /* offscreen.js */,
CEA194E32EB6221B00EAB23B /* webauthn.js */,
CE0CAFD32D81A9F8006174AB /* background.js */,
CE0CAFD42D81A9F8006174AB /* popup.html */,
CE0CAFD52D81A9F8006174AB /* chunks */,
@@ -248,6 +260,7 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
CE0E5BB02E7D8BA600515C80 /* AliasVault.icon in Resources */,
CE0CAFAD2D81A9F7006174AB /* Icon.png in Resources */,
CE0CAFB12D81A9F7006174AB /* Script.js in Resources */,
CE0CAFAB2D81A9F7006174AB /* Base in Resources */,
@@ -262,8 +275,11 @@
buildActionMask = 2147483647;
files = (
CE0CAFDD2D81A9F8006174AB /* chunks in Resources */,
CEA194E42EB6221B00EAB23B /* webauthn.js in Resources */,
CE0CAFE02D81A9F8006174AB /* icon in Resources */,
CE0CAFE12D81A9F8006174AB /* assets in Resources */,
CEA194E72EB6248D00EAB23B /* offscreen.html in Resources */,
CEA194E82EB6248D00EAB23B /* offscreen.js in Resources */,
CE0CAFE22D81A9F8006174AB /* src in Resources */,
CE0CAFDB2D81A9F8006174AB /* background.js in Resources */,
CE0CAFDF2D81A9F8006174AB /* manifest.json in Resources */,
@@ -447,7 +463,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 230200;
CURRENT_PROJECT_VERSION = 2400902;
DEVELOPMENT_TEAM = 8PHW4HN3F7;
ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES;
@@ -460,7 +476,7 @@
"@executable_path/../../../../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
MARKETING_VERSION = 0.23.2;
MARKETING_VERSION = 0.24.0;
OTHER_LDFLAGS = (
"-framework",
SafariServices,
@@ -479,7 +495,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = "AliasVault Extension/AliasVault_Extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 230200;
CURRENT_PROJECT_VERSION = 2400902;
DEVELOPMENT_TEAM = 8PHW4HN3F7;
ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES;
@@ -492,7 +508,7 @@
"@executable_path/../../../../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
MARKETING_VERSION = 0.23.2;
MARKETING_VERSION = 0.24.0;
OTHER_LDFLAGS = (
"-framework",
SafariServices,
@@ -509,13 +525,13 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_APPICON_NAME = AliasVault;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = AliasVault/AliasVault.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 230200;
CURRENT_PROJECT_VERSION = 2400902;
DEVELOPMENT_TEAM = 8PHW4HN3F7;
ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES;
@@ -530,7 +546,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
MARKETING_VERSION = 0.23.2;
MARKETING_VERSION = 0.24.0;
OTHER_LDFLAGS = (
"-framework",
SafariServices,
@@ -549,12 +565,12 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_APPICON_NAME = AliasVault;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = AliasVault/AliasVault.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 230200;
CURRENT_PROJECT_VERSION = 2400902;
DEVELOPMENT_TEAM = 8PHW4HN3F7;
ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES;
@@ -569,7 +585,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
MARKETING_VERSION = 0.23.2;
MARKETING_VERSION = 0.24.0;
OTHER_LDFLAGS = (
"-framework",
SafariServices,

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -0,0 +1,33 @@
{
"fill" : "automatic",
"groups" : [
{
"layers" : [
{
"blend-mode" : "overlay",
"fill" : {
"automatic-gradient" : "display-p3:0.90471,0.76358,0.48553,1.00000"
},
"glass" : true,
"hidden" : false,
"image-name" : "icon-1024.png",
"name" : "icon-1024"
}
],
"shadow" : {
"kind" : "neutral",
"opacity" : 0.5
},
"translucency" : {
"enabled" : true,
"value" : 0.5
}
}
],
"supported-platforms" : {
"circles" : [
"watchOS"
],
"squares" : "shared"
}
}

View File

@@ -1,68 +0,0 @@
{
"images" : [
{
"size" : "16x16",
"idiom" : "mac",
"filename" : "mac-icon-16@1x.png",
"scale" : "1x"
},
{
"size" : "16x16",
"idiom" : "mac",
"filename" : "mac-icon-16@2x.png",
"scale" : "2x"
},
{
"size" : "32x32",
"idiom" : "mac",
"filename" : "mac-icon-32@1x.png",
"scale" : "1x"
},
{
"size" : "32x32",
"idiom" : "mac",
"filename" : "mac-icon-32@2x.png",
"scale" : "2x"
},
{
"size" : "128x128",
"idiom" : "mac",
"filename" : "mac-icon-128@1x.png",
"scale" : "1x"
},
{
"size" : "128x128",
"idiom" : "mac",
"filename" : "mac-icon-128@2x.png",
"scale" : "2x"
},
{
"size" : "256x256",
"idiom" : "mac",
"filename" : "mac-icon-256@1x.png",
"scale" : "1x"
},
{
"size" : "256x256",
"idiom" : "mac",
"filename" : "mac-icon-256@2x.png",
"scale" : "2x"
},
{
"size" : "512x512",
"idiom" : "mac",
"filename" : "mac-icon-512@1x.png",
"scale" : "1x"
},
{
"size" : "512x512",
"idiom" : "mac",
"filename" : "mac-icon-512@2x.png",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

View File

@@ -0,0 +1,179 @@
#!/usr/bin/env bash
BUNDLE_ID="net.aliasvault.safari.extension"
# Build settings
SCHEME="AliasVault"
PROJECT="AliasVault.xcodeproj"
CONFIG="Release"
ARCHIVE_PATH="$PWD/build/${SCHEME}.xcarchive"
EXPORT_DIR="$PWD/build/export"
EXPORT_PLIST="$PWD/exportOptions.plist"
# Put the fastlane API key in the home directory
API_KEY_PATH="$HOME/APPSTORE_CONNECT_FASTLANE.json"
# ------------------------------------------
if [ ! -f "$API_KEY_PATH" ]; then
echo "❌ API key file '$API_KEY_PATH' does not exist. Please provide the App Store Connect API key at this path."
exit 1
fi
# ------------------------------------------
# Shared function to extract version info
# ------------------------------------------
extract_version_info() {
local pkg_path="$1"
# For .pkg files, we need to expand and find the Info.plist
local temp_dir=$(mktemp -d -t aliasvault-pkg-extract)
trap "rm -rf '$temp_dir'" EXIT
# Expand the pkg to find the app bundle
pkgutil --expand "$pkg_path" "$temp_dir/expanded" 2>/dev/null
# Find the payload and extract it
local payload=$(find "$temp_dir/expanded" -name "Payload" | head -n 1)
if [ -n "$payload" ]; then
mkdir -p "$temp_dir/contents"
cd "$temp_dir/contents"
cat "$payload" | gunzip -dc | cpio -i 2>/dev/null
# Find Info.plist in the extracted contents
local info_plist=$(find "$temp_dir/contents" -name "Info.plist" -path "*/Contents/Info.plist" | head -n 1)
if [ -n "$info_plist" ]; then
# Read version and build from the plist
VERSION=$(/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" "$info_plist" 2>/dev/null)
BUILD=$(/usr/libexec/PlistBuddy -c "Print :CFBundleVersion" "$info_plist" 2>/dev/null)
if [ -n "$VERSION" ] && [ -n "$BUILD" ]; then
return 0
fi
fi
fi
# Fallback: try to read from the archive directly if it's in a known location
local archive_plist="$ARCHIVE_PATH/Info.plist"
if [ -f "$archive_plist" ]; then
VERSION=$(/usr/libexec/PlistBuddy -c "Print :ApplicationProperties:CFBundleShortVersionString" "$archive_plist" 2>/dev/null)
BUILD=$(/usr/libexec/PlistBuddy -c "Print :ApplicationProperties:CFBundleVersion" "$archive_plist" 2>/dev/null)
if [ -n "$VERSION" ] && [ -n "$BUILD" ]; then
return 0
fi
fi
echo "❌ Could not extract version info from package"
exit 1
}
# ------------------------------------------
# Ask if user wants to build or use existing
# ------------------------------------------
echo ""
echo "What do you want to do?"
echo " 1) Build and submit to App Store"
echo " 2) Build only"
echo " 3) Submit existing PKG to App Store"
echo ""
read -p "Enter choice (1, 2, or 3): " -r CHOICE
echo ""
# ------------------------------------------
# Build PKG (for options 1 and 2)
# ------------------------------------------
if [[ $CHOICE == "1" || $CHOICE == "2" ]]; then
echo "Building browser extension..."
cd ../..
npm run build:safari
cd safari-xcode/AliasVault
echo "Building PKG..."
# Clean + archive
xcodebuild \
-project "$PROJECT" \
-scheme "$SCHEME" \
-configuration "$CONFIG" \
-archivePath "$ARCHIVE_PATH" \
clean archive \
-allowProvisioningUpdates
# Export .pkg
rm -rf "$EXPORT_DIR"
xcodebuild -exportArchive \
-archivePath "$ARCHIVE_PATH" \
-exportOptionsPlist "$EXPORT_PLIST" \
-exportPath "$EXPORT_DIR" \
-allowProvisioningUpdates
PKG_PATH=$(ls "$EXPORT_DIR"/*.pkg)
# Extract version info from newly built PKG
extract_version_info "$PKG_PATH"
echo "PKG built at: $PKG_PATH"
echo " Version: $VERSION"
echo " Build: $BUILD"
echo ""
# Exit if build-only
if [[ $CHOICE == "2" ]]; then
echo "✅ Build complete. Exiting."
exit 0
fi
fi
# ------------------------------------------
# Submit to App Store (for options 1 and 3)
# ------------------------------------------
if [[ $CHOICE == "3" ]]; then
# Use existing PKG
PKG_PATH="$EXPORT_DIR/AliasVault.pkg"
if [ ! -f "$PKG_PATH" ]; then
echo "❌ PKG file not found at: $PKG_PATH"
exit 1
fi
# Extract version info from existing PKG
extract_version_info "$PKG_PATH"
echo "Using existing PKG: $PKG_PATH"
echo " Version: $VERSION"
echo " Build: $BUILD"
echo ""
fi
if [[ $CHOICE != "1" && $CHOICE != "3" ]]; then
echo "❌ Invalid choice. Please enter 1, 2, or 3."
exit 1
fi
echo ""
echo "================================================"
echo "Submitting to App Store:"
echo " Version: $VERSION"
echo " Build: $BUILD"
echo "================================================"
echo ""
read -p "Are you sure you want to push this to App Store? (y/n): " -r
echo ""
if [[ ! $REPLY =~ ^([Yy]([Ee][Ss])?|[Yy])$ ]]; then
echo "❌ Submission cancelled"
exit 1
fi
echo "✅ Proceeding with upload..."
fastlane deliver \
--pkg "$PKG_PATH" \
--skip_screenshots \
--skip_metadata \
--api_key_path "$API_KEY_PATH" \
--run_precheck_before_submit=false

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>app-store</string>
<key>signingStyle</key>
<string>automatic</string>
<key>destination</key>
<string>export</string>
<key>stripSwiftSymbols</key>
<true/>
<key>compileBitcode</key>
<true/>
<key>manageAppVersionAndBuildNumber</key>
<false/>
</dict>
</plist>

View File

@@ -1,13 +1,18 @@
/**
* Background script entry point - handles messages from the content script
*/
import { onMessage, sendMessage } from "webext-bridge/background";
import { handleResetAutoLockTimer, handlePopupHeartbeat, handleSetAutoLockTimeout } from '@/entrypoints/background/AutolockTimeoutHandler';
import { handleClipboardCopied, handleCancelClipboardClear, handleGetClipboardClearTimeout, handleSetClipboardClearTimeout, handleGetClipboardCountdownState } from '@/entrypoints/background/ClipboardClearHandler';
import { setupContextMenus } from '@/entrypoints/background/ContextMenu';
import { handleGetWebAuthnSettings, handleWebAuthnCreate, handleWebAuthnGet, handlePasskeyPopupResponse, handleGetRequestData } from '@/entrypoints/background/PasskeyHandler';
import { handleOpenPopup, handlePopupWithCredential, handleOpenPopupCreateCredential, handleToggleContextMenu } from '@/entrypoints/background/PopupMessageHandler';
import { handleCheckAuthStatus, handleClearPersistedFormValues, handleClearVault, handleCreateIdentity, handleGetCredentials, handleGetDefaultEmailDomain, handleGetDefaultIdentitySettings, handleGetEncryptionKey, handleGetEncryptionKeyDerivationParams, handleGetPasswordSettings, handleGetPersistedFormValues, handleGetVault, handlePersistFormValues, handleStoreEncryptionKey, handleStoreEncryptionKeyDerivationParams, handleStoreVault, handleSyncVault, handleUploadVault } from '@/entrypoints/background/VaultMessageHandler';
import { GLOBAL_CONTEXT_MENU_ENABLED_KEY } from '@/utils/Constants';
import type { EncryptionKeyDerivationParams } from '@/utils/dist/shared/models/metadata';
import { EncryptionKeyDerivationParams } from "@/utils/dist/shared/models/metadata";
import { defineBackground, storage, browser } from '#imports';
@@ -62,6 +67,13 @@ export default defineBackground({
// Handle clipboard copied from context menu
onMessage('CLIPBOARD_COPIED_FROM_CONTEXT', () => handleClipboardCopied());
// Passkey/WebAuthn management messages
onMessage('GET_WEBAUTHN_SETTINGS', ({ data }) => handleGetWebAuthnSettings(data));
onMessage('WEBAUTHN_CREATE', ({ data }) => handleWebAuthnCreate(data));
onMessage('WEBAUTHN_GET', ({ data }) => handleWebAuthnGet(data));
onMessage('PASSKEY_POPUP_RESPONSE', ({ data }) => handlePasskeyPopupResponse(data));
onMessage('GET_REQUEST_DATA', ({ data }) => handleGetRequestData(data));
// Setup context menus
const isContextMenuEnabled = await storage.getItem(GLOBAL_CONTEXT_MENU_ENABLED_KEY) ?? true;
if (isContextMenuEnabled) {

View File

@@ -0,0 +1,230 @@
/**
* PasskeyHandler - Handles passkey popup management in background
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
import { extractDomain, extractRootDomain } from '@/entrypoints/contentScript/Filter';
import {
PASSKEY_PROVIDER_ENABLED_KEY,
PASSKEY_DISABLED_SITES_KEY
} from '@/utils/Constants';
import type {
PasskeyPopupResponse,
WebAuthnCreateRequest,
WebAuthnGetRequest,
PendingPasskeyRequest,
PendingPasskeyCreateRequest,
PendingPasskeyGetRequest,
WebAuthnSettingsResponse,
WebAuthnCreationPayload,
WebAuthnPublicKeyGetPayload
} from '@/utils/passkey/types';
import { browser, storage } from '#imports';
// Pending popup requests
const pendingRequests = new Map<string, {
resolve: (value: any) => void;
reject: (error: any) => void;
/**
* Store window ID in order to close the popup window from background script later.
*/
windowId?: number;
}>();
// Store request data temporarily (to avoid URL length limits)
const pendingRequestData = new Map<string, PendingPasskeyRequest>();
/**
* Handle WebAuthn settings request
*/
export async function handleGetWebAuthnSettings(data: any): Promise<WebAuthnSettingsResponse> {
// Check if passkey provider is enabled in settings (default to true if not set)
const globalEnabled = await storage.getItem(PASSKEY_PROVIDER_ENABLED_KEY);
if (globalEnabled === false) {
return { enabled: false };
}
// If hostname is provided, check if it's disabled for that site
const { hostname } = data || {};
if (hostname) {
// Extract base domain for matching
const baseDomain = extractRootDomain(extractDomain(hostname));
// Check disabled sites
const disabledSites = await storage.getItem(PASSKEY_DISABLED_SITES_KEY) as string[] ?? [];
if (disabledSites.includes(baseDomain)) {
return { enabled: false };
}
}
return { enabled: true };
}
/**
* Handle WebAuthn create (registration) request
*/
export async function handleWebAuthnCreate(data: any): Promise<any> {
const { publicKey, origin } = data as WebAuthnCreateRequest;
const requestId = Math.random().toString(36).substr(2, 9);
// Store request data temporarily (to avoid URL length limits)
const requestData: PendingPasskeyCreateRequest = {
type: 'create',
requestId,
origin,
publicKey: publicKey as WebAuthnCreationPayload
};
pendingRequestData.set(requestId, requestData);
// Create popup using main popup with hash navigation - only pass requestId
const popupUrl = browser.runtime.getURL('/popup.html') + '#/passkeys/create?' + new URLSearchParams({
requestId
}).toString();
try {
const popup = await browser.windows.create({
url: popupUrl,
type: 'popup',
width: 450,
height: 600,
focused: true
});
// Wait for response from popup
return new Promise((resolve, reject) => {
pendingRequests.set(requestId, { resolve, reject, windowId: popup.id });
// Clean up if popup is closed without response
const checkClosed = setInterval(async () => {
try {
if (popup.id) {
const _window = await browser.windows.get(popup.id);
// Window still exists, continue waiting
}
} catch {
// Window no longer exists
clearInterval(checkClosed);
if (pendingRequests.has(requestId)) {
pendingRequests.delete(requestId);
pendingRequestData.delete(requestId);
resolve({ cancelled: true });
}
}
}, 1000);
});
} catch {
return { error: 'Failed to create popup window' };
}
}
/**
* Handle WebAuthn get (authentication) request
* Note: Passkey retrieval is now handled in the popup via SqliteClient
*/
export async function handleWebAuthnGet(data: any): Promise<any> {
const { publicKey, origin } = data as WebAuthnGetRequest;
const requestId = Math.random().toString(36).substr(2, 9);
// Store request data temporarily (to avoid URL length limits)
const requestData: PendingPasskeyGetRequest = {
type: 'get',
requestId,
origin,
publicKey: publicKey as WebAuthnPublicKeyGetPayload,
passkeys: [] // Will be populated by the popup from vault
};
pendingRequestData.set(requestId, requestData);
// Create popup using main popup with hash navigation - only pass requestId
const popupUrl = browser.runtime.getURL('/popup.html') + '#/passkeys/authenticate?' + new URLSearchParams({
requestId
}).toString();
try {
const popup = await browser.windows.create({
url: popupUrl,
type: 'popup',
width: 450,
height: 600,
focused: true
});
// Wait for response from popup
return new Promise((resolve, reject) => {
pendingRequests.set(requestId, { resolve, reject, windowId: popup.id });
// Clean up if popup is closed without response
const checkClosed = setInterval(async () => {
try {
if (popup.id) {
const _window = await browser.windows.get(popup.id);
// Window still exists, continue waiting
}
} catch {
// Window no longer exists
clearInterval(checkClosed);
if (pendingRequests.has(requestId)) {
pendingRequests.delete(requestId);
pendingRequestData.delete(requestId);
resolve({ cancelled: true });
}
}
}, 1000);
});
} catch {
return { error: 'Failed to create popup window' };
}
}
/**
* Handle response from passkey popup
*/
export async function handlePasskeyPopupResponse(data: any): Promise<{ success: boolean }> {
const { requestId, credential, fallback, cancelled } = data as PasskeyPopupResponse;
const request = pendingRequests.get(requestId);
if (!request) {
return { success: false };
}
/**
* Close the popup window from background script to ensure it always works.
* Calling window.close() from the popup does not work in all browsers.
*/
if (request.windowId) {
try {
await browser.windows.remove(request.windowId);
} catch (error) {
// Window might already be closed, ignore error
console.debug('Failed to close popup window:', error);
}
}
// Clean up both maps
pendingRequests.delete(requestId);
pendingRequestData.delete(requestId);
if (cancelled) {
request.resolve({ cancelled: true });
} else if (fallback) {
request.resolve({ fallback: true });
} else if (credential) {
request.resolve({ credential });
} else {
request.resolve({ cancelled: true });
}
return { success: true };
}
/**
* Get request data by request ID
*/
export async function handleGetRequestData(data: any): Promise<PendingPasskeyRequest | null> {
const { requestId } = data as { requestId: string };
const requestData = pendingRequestData.get(requestId);
return requestData || null;
}

View File

@@ -5,6 +5,7 @@ import type { EncryptionKeyDerivationParams } from '@/utils/dist/shared/models/m
import type { Vault, VaultResponse, VaultPostResponse } from '@/utils/dist/shared/models/webapi';
import { EncryptionUtility } from '@/utils/EncryptionUtility';
import { SqliteClient } from '@/utils/SqliteClient';
import { VaultVersionIncompatibleError } from '@/utils/types/errors/VaultVersionIncompatibleError';
import { BoolResponse as messageBoolResponse } from '@/utils/types/messaging/BoolResponse';
import { CredentialsResponse as messageCredentialsResponse } from '@/utils/types/messaging/CredentialsResponse';
import { IdentitySettingsResponse } from '@/utils/types/messaging/IdentitySettingsResponse';
@@ -57,6 +58,18 @@ export async function handleCheckAuthStatus() : Promise<{ isLoggedIn: boolean, i
};
} catch (error) {
console.error('Error checking pending migrations:', error);
// If it's a version incompatibility error, we need to handle it specially
if (error instanceof VaultVersionIncompatibleError) {
// Return the error so the UI can handle it appropriately (logout user)
return {
isLoggedIn,
isVaultLocked,
hasPendingMigrations: false,
error: error.message
};
}
return {
isLoggedIn,
isVaultLocked,
@@ -98,7 +111,7 @@ export async function handleStoreVault(
return { success: true };
} catch (error) {
console.error('Failed to store vault:', error);
return { success: false, error: await t('common.errors.failedToStoreVault') };
return { success: false, error: await t('common.errors.unknownError') };
}
}
@@ -489,7 +502,7 @@ async function uploadNewVaultToServer(sqliteClient: SqliteClient) : Promise<Vaul
client: '', // Empty on purpose, API will not use this for vault updates.
updatedAt: new Date().toISOString(),
username: username,
version: sqliteClient.getDatabaseVersion().version
version: (await sqliteClient.getDatabaseVersion()).version
};
const webApi = new WebApiService(() => {});

View File

@@ -1,8 +1,13 @@
/**
* Content script entry point - handles autofill UI and WebAuthn passkey interception
*/
import '@/entrypoints/contentScript/style.css';
import { onMessage } from "webext-bridge/content-script";
import { injectIcon, popupDebounceTimeHasPassed, validateInputField } from '@/entrypoints/contentScript/Form';
import { isAutoShowPopupEnabled, openAutofillPopup, removeExistingPopup, createUpgradeRequiredPopup } from '@/entrypoints/contentScript/Popup';
import { initializeWebAuthnInterceptor } from '@/entrypoints/contentScript/WebAuthnInterceptor';
import { FormDetector } from '@/utils/formDetector/FormDetector';
import { BoolResponse as messageBoolResponse } from '@/utils/types/messaging/BoolResponse';
@@ -26,6 +31,9 @@ export default defineContentScript({
return;
}
// Initialize WebAuthn interceptor for passkey support
await initializeWebAuthnInterceptor(ctx);
// Wait for 750ms to give the host page time to load and to increase the chance that the body is available and ready.
await new Promise(resolve => setTimeout(resolve, 750));

View File

@@ -16,7 +16,7 @@ type CredentialWithPriority = Credential & {
* @param url - URL or domain string
* @returns Normalized domain without protocol or www
*/
function extractDomain(url: string): string {
export function extractDomain(url: string): string {
if (!url) {
return '';
}
@@ -36,6 +36,92 @@ function extractDomain(url: string): string {
return domain;
}
/**
* Extract root domain from a domain string.
* E.g., "sub.example.com" -> "example.com"
* E.g., "sub.example.com.au" -> "example.com.au"
* E.g., "sub.example.co.uk" -> "example.co.uk"
*/
export function extractRootDomain(domain: string): string {
const parts = domain.split('.');
if (parts.length < 2) {
return domain;
}
// Common two-level public TLDs
const twoLevelTlds = new Set([
// Australia
'com.au', 'net.au', 'org.au', 'edu.au', 'gov.au', 'asn.au', 'id.au',
// United Kingdom
'co.uk', 'org.uk', 'net.uk', 'ac.uk', 'gov.uk', 'plc.uk', 'ltd.uk', 'me.uk',
// Canada
'co.ca', 'net.ca', 'org.ca', 'gc.ca', 'ab.ca', 'bc.ca', 'mb.ca', 'nb.ca', 'nf.ca', 'nl.ca', 'ns.ca', 'nt.ca', 'nu.ca',
'on.ca', 'pe.ca', 'qc.ca', 'sk.ca', 'yk.ca',
// India
'co.in', 'net.in', 'org.in', 'edu.in', 'gov.in', 'ac.in', 'res.in', 'gen.in', 'firm.in', 'ind.in',
// Japan
'co.jp', 'ne.jp', 'or.jp', 'ac.jp', 'ad.jp', 'ed.jp', 'go.jp', 'gr.jp', 'lg.jp',
// South Africa
'co.za', 'net.za', 'org.za', 'edu.za', 'gov.za', 'ac.za', 'web.za',
// New Zealand
'co.nz', 'net.nz', 'org.nz', 'edu.nz', 'govt.nz', 'ac.nz', 'geek.nz', 'gen.nz', 'kiwi.nz', 'maori.nz', 'mil.nz', 'school.nz',
// Brazil
'com.br', 'net.br', 'org.br', 'edu.br', 'gov.br', 'mil.br', 'art.br', 'etc.br', 'adv.br', 'arq.br', 'bio.br', 'cim.br',
'cng.br', 'cnt.br', 'ecn.br', 'eng.br', 'esp.br', 'eti.br', 'far.br', 'fnd.br', 'fot.br', 'fst.br', 'g12.br', 'geo.br',
'ggf.br', 'jor.br', 'lel.br', 'mat.br', 'med.br', 'mus.br', 'not.br', 'ntr.br', 'odo.br', 'ppg.br', 'pro.br', 'psc.br',
'psi.br', 'qsl.br', 'rec.br', 'slg.br', 'srv.br', 'tmp.br', 'trd.br', 'tur.br', 'tv.br', 'vet.br', 'zlg.br',
// Russia
'com.ru', 'net.ru', 'org.ru', 'edu.ru', 'gov.ru', 'int.ru', 'mil.ru', 'spb.ru', 'msk.ru',
// China
'com.cn', 'net.cn', 'org.cn', 'edu.cn', 'gov.cn', 'mil.cn', 'ac.cn', 'ah.cn', 'bj.cn', 'cq.cn', 'fj.cn', 'gd.cn', 'gs.cn',
'gz.cn', 'gx.cn', 'ha.cn', 'hb.cn', 'he.cn', 'hi.cn', 'hk.cn', 'hl.cn', 'hn.cn', 'jl.cn', 'js.cn', 'jx.cn', 'ln.cn', 'mo.cn',
'nm.cn', 'nx.cn', 'qh.cn', 'sc.cn', 'sd.cn', 'sh.cn', 'sn.cn', 'sx.cn', 'tj.cn', 'tw.cn', 'xj.cn', 'xz.cn', 'yn.cn', 'zj.cn',
// Mexico
'com.mx', 'net.mx', 'org.mx', 'edu.mx', 'gob.mx',
// Argentina
'com.ar', 'net.ar', 'org.ar', 'edu.ar', 'gov.ar', 'mil.ar', 'int.ar',
// Chile
'com.cl', 'net.cl', 'org.cl', 'edu.cl', 'gov.cl', 'mil.cl',
// Colombia
'com.co', 'net.co', 'org.co', 'edu.co', 'gov.co', 'mil.co', 'nom.co',
// Venezuela
'com.ve', 'net.ve', 'org.ve', 'edu.ve', 'gov.ve', 'mil.ve', 'web.ve',
// Peru
'com.pe', 'net.pe', 'org.pe', 'edu.pe', 'gob.pe', 'mil.pe', 'nom.pe',
// Ecuador
'com.ec', 'net.ec', 'org.ec', 'edu.ec', 'gov.ec', 'mil.ec', 'med.ec', 'fin.ec', 'pro.ec', 'info.ec',
// Europe
'co.at', 'or.at', 'ac.at', 'gv.at', 'priv.at',
'co.be', 'ac.be',
'co.dk', 'ac.dk',
'co.il', 'net.il', 'org.il', 'ac.il', 'gov.il', 'idf.il', 'k12.il', 'muni.il',
'co.no', 'ac.no', 'priv.no',
'co.pl', 'net.pl', 'org.pl', 'edu.pl', 'gov.pl', 'mil.pl', 'nom.pl', 'com.pl',
'co.th', 'net.th', 'org.th', 'edu.th', 'gov.th', 'mil.th', 'ac.th', 'in.th',
'co.kr', 'net.kr', 'org.kr', 'edu.kr', 'gov.kr', 'mil.kr', 'ac.kr', 'go.kr', 'ne.kr', 'or.kr', 'pe.kr', 're.kr', 'seoul.kr',
'kyonggi.kr',
// Others
'co.id', 'net.id', 'org.id', 'edu.id', 'gov.id', 'mil.id', 'web.id', 'ac.id', 'sch.id',
'co.ma', 'net.ma', 'org.ma', 'edu.ma', 'gov.ma', 'ac.ma', 'press.ma',
'co.ke', 'net.ke', 'org.ke', 'edu.ke', 'gov.ke', 'ac.ke', 'go.ke', 'info.ke', 'me.ke', 'mobi.ke', 'sc.ke',
'co.ug', 'net.ug', 'org.ug', 'edu.ug', 'gov.ug', 'ac.ug', 'sc.ug', 'go.ug', 'ne.ug', 'or.ug',
'co.tz', 'net.tz', 'org.tz', 'edu.tz', 'gov.tz', 'ac.tz', 'go.tz', 'hotel.tz', 'info.tz', 'me.tz', 'mil.tz', 'mobi.tz',
'ne.tz', 'or.tz', 'sc.tz', 'tv.tz',
]);
// Check if the last two parts form a known two-level TLD
if (parts.length >= 3) {
const lastTwoParts = parts.slice(-2).join('.');
if (twoLevelTlds.has(lastTwoParts)) {
// Take the last three parts for two-level TLDs
return parts.slice(-3).join('.');
}
}
// Default to last two parts for regular TLDs
return parts.length >= 2 ? parts.slice(-2).join('.') : domain;
}
/**
* Check if two domains match, supporting partial matches
* @param domain1 - First domain
@@ -60,13 +146,9 @@ function domainsMatch(domain1: string, domain2: string): boolean {
return true;
}
// Extract root domains for comparison
const d1Parts = d1.split('.');
const d2Parts = d2.split('.');
// Get the last 2 parts (domain.tld) for comparison
const d1Root = d1Parts.slice(-2).join('.');
const d2Root = d2Parts.slice(-2).join('.');
// Check root domain match
const d1Root = extractRootDomain(d1);
const d2Root = extractRootDomain(d2);
return d1Root === d2Root;
}

View File

@@ -633,10 +633,44 @@ function createCredentialList(credentials: Credential[], input: HTMLInputElement
const credTextContainer = document.createElement('div');
credTextContainer.className = 'av-credential-text';
// Service name (primary text)
// Service name (primary text) with passkey indicator
const serviceName = document.createElement('div');
serviceName.className = 'av-service-name';
serviceName.textContent = cred.ServiceName;
// Create a flex container for service name and passkey icon
const serviceNameContainer = document.createElement('div');
serviceNameContainer.style.display = 'flex';
serviceNameContainer.style.alignItems = 'center';
serviceNameContainer.style.gap = '4px';
const serviceNameText = document.createElement('span');
serviceNameText.textContent = cred.ServiceName;
serviceNameContainer.appendChild(serviceNameText);
// Add passkey indicator if credential has a passkey
if (cred.HasPasskey) {
const passkeyIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
passkeyIcon.setAttribute('class', 'av-passkey-icon');
passkeyIcon.setAttribute('viewBox', '0 0 24 24');
passkeyIcon.setAttribute('fill', 'none');
passkeyIcon.setAttribute('stroke', 'currentColor');
passkeyIcon.setAttribute('stroke-width', '2');
passkeyIcon.setAttribute('stroke-linecap', 'round');
passkeyIcon.setAttribute('stroke-linejoin', 'round');
passkeyIcon.setAttribute('aria-label', 'Has passkey');
passkeyIcon.style.width = '14px';
passkeyIcon.style.height = '14px';
passkeyIcon.style.flexShrink = '0';
passkeyIcon.style.opacity = '0.7';
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', 'M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4');
passkeyIcon.appendChild(path);
serviceNameContainer.appendChild(passkeyIcon);
}
serviceName.appendChild(serviceNameContainer);
// Details container (secondary text)
const detailsContainer = document.createElement('div');

View File

@@ -0,0 +1,256 @@
/**
* WebAuthn Interceptor - Handles communication between page and extension
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
import { sendMessage } from 'webext-bridge/content-script';
import type { WebAuthnSettingsResponse } from '@/utils/passkey/types';
import { browser } from '#imports';
// Firefox-specific global function for cloning objects into page context
declare function cloneInto<T>(obj: T, targetScope: any): T;
let interceptorInitialized = false;
/**
* Track last cancelled request to prevent rapid-fire popups.
* This is used to track the last time a WebAuthn request was cancelled.
* Some websites try to automatically re-trigger a WebAuthn request after a cancellation.
* which results in a jarring UX for the user.
* This cooldown prevents rapid-fire popups by waiting for a short period after a cancellation.
*/
let lastCancelledTimestamp = 0;
const CANCEL_COOLDOWN_MS = 500; // 500ms cooldown after a recent cancellation
/**
* Check if page is ready for WebAuthn interactions.
* Safari and other browsers can trigger WebAuthn requests during URL autocomplete
* or page prefetch, which creates popups before the user actually navigates to the page.
* We check if the document is visible and interactive to prevent these spurious requests.
*/
function isPageReadyForWebAuthn(): boolean {
// If page is hidden (prefetch/background tab), block the request
if (document.hidden || document.visibilityState === 'hidden') {
return false;
}
// If document is still loading (not even interactive), block the request
if (document.readyState === 'loading') {
return false;
}
// Page is visible and at least interactive - allow the request
return true;
}
/**
* Initialize the WebAuthn interceptor
*/
export async function initializeWebAuthnInterceptor(_ctx: any): Promise<void> {
if (interceptorInitialized) {
return;
}
// Listen for WebAuthn create events from the page
window.addEventListener('aliasvault:webauthn:create', async (event: any) => {
const { requestId, publicKey, origin } = event.detail;
/**
* Helper to dispatch event with Firefox compatibility
* Firefox has strict cross-context security, so we serialize to JSON and back
*/
const dispatchResponse = (detail: any): void => {
let eventDetail: any;
/*
* For Firefox, we need to ensure the detail is accessible in the page context
* cloneInto is a global function in Firefox content scripts
*/
if (typeof cloneInto !== 'undefined') {
// Firefox: serialize and clone into page context
const serialized = JSON.parse(JSON.stringify(detail));
eventDetail = cloneInto(serialized, (window as any).wrappedJSObject || window);
} else {
// Chrome/Edge: direct assignment works
eventDetail = detail;
}
window.dispatchEvent(new CustomEvent('aliasvault:webauthn:create:response', {
detail: eventDetail
}));
};
try {
/**
* Note: We don't block create (registration) requests based on page readiness.
* Registration is always user-initiated (button click), so it's never spurious.
*/
// Check if we're in cooldown period after a recent cancellation
const now = Date.now();
if (lastCancelledTimestamp > 0 && (now - lastCancelledTimestamp) < CANCEL_COOLDOWN_MS) {
// Silently fall back to native implementation during cooldown
dispatchResponse({
requestId,
fallback: true
});
return;
}
// Check if passkey provider is enabled
const enabled = await isWebAuthnInterceptionEnabled();
if (!enabled) {
// If disabled, signal fallback to native browser implementation
dispatchResponse({
requestId,
fallback: true
});
return;
}
// Send to background script to handle
const result = await sendMessage('WEBAUTHN_CREATE', {
publicKey,
origin
}, 'background');
// Track if user cancelled to enable cooldown
if (result && typeof result === 'object' && (result as any).cancelled) {
lastCancelledTimestamp = Date.now();
}
// Send response back to page
dispatchResponse({
requestId,
...(typeof result === 'object' && result !== null ? result : {})
});
} catch (error: any) {
dispatchResponse({
requestId,
error: error.message
});
}
});
// Listen for WebAuthn get events from the page
window.addEventListener('aliasvault:webauthn:get', async (event: any) => {
const { requestId, publicKey, origin } = event.detail;
/**
* Helper to dispatch event with Firefox compatibility
* Firefox has strict cross-context security, so we serialize to JSON and back
*/
const dispatchResponse = (detail: any): void => {
let eventDetail: any;
/*
* For Firefox, we need to ensure the detail is accessible in the page context
* cloneInto is a global function in Firefox content scripts
*/
if (typeof cloneInto !== 'undefined') {
// Firefox: serialize and clone into page context
const serialized = JSON.parse(JSON.stringify(detail));
eventDetail = cloneInto(serialized, (window as any).wrappedJSObject || window);
} else {
// Chrome/Edge: direct assignment works
eventDetail = detail;
}
window.dispatchEvent(new CustomEvent('aliasvault:webauthn:get:response', {
detail: eventDetail
}));
};
try {
// Block requests if page isn't ready (prevents prefetch/autocomplete popups)
if (!isPageReadyForWebAuthn()) {
dispatchResponse({
requestId,
fallback: true
});
return;
}
// Check if we're in cooldown period after a recent cancellation
const now = Date.now();
if (lastCancelledTimestamp > 0 && (now - lastCancelledTimestamp) < CANCEL_COOLDOWN_MS) {
// Silently fall back to native implementation during cooldown
dispatchResponse({
requestId,
fallback: true
});
return;
}
// Check if passkey provider is enabled
const enabled = await isWebAuthnInterceptionEnabled();
if (!enabled) {
// If disabled, signal fallback to native browser implementation
dispatchResponse({
requestId,
fallback: true
});
return;
}
// Send to background script to handle
const result = await sendMessage('WEBAUTHN_GET', {
publicKey,
origin
}, 'background');
// Track if user cancelled to enable cooldown
if (result && typeof result === 'object' && (result as any).cancelled) {
lastCancelledTimestamp = Date.now();
}
// Send response back to page
dispatchResponse({
requestId,
...(typeof result === 'object' && result !== null ? result : {})
});
} catch (error: any) {
dispatchResponse({
requestId,
error: error.message
});
}
});
// Inject the page script
const script = document.createElement('script');
script.src = browser.runtime.getURL('/webauthn.js');
script.async = true;
(document.head || document.documentElement).appendChild(script);
/**
* onload
*/
script.onload = () : void => {
script.remove();
};
/**
* onerror
*/
script.onerror = () : void => {
// Ignore
};
interceptorInitialized = true;
}
/**
* Check if WebAuthn interception is enabled for the current site
*/
export async function isWebAuthnInterceptionEnabled(): Promise<boolean> {
try {
const response = await sendMessage('GET_WEBAUTHN_SETTINGS', {
hostname: window.location.hostname
}, 'background') as unknown as WebAuthnSettingsResponse;
return response.enabled ?? false;
} catch {
return false;
}
}

View File

@@ -292,6 +292,68 @@ describe('Filter - Credential URL Matching', () => {
expect(matches[0].ServiceName).toBe('Reddit');
});
/**
* [#20] - Test reversed domain (Android package name) doesn't match on TLD
* Note: Android package name filtering is not applicable to browser extensions.
* This test is included for consistency with Android and iOS test suites but is skipped.
*/
it.skip('should not match credentials based on TLD when filtering reversed domains', () => {
/**
* Android package name detection is not implemented in browser extensions
* since they only deal with web URLs, not Android app contexts.
*/
});
/**
* [#21] - Test Android package names are properly detected and handled
* Note: Android package name filtering is not applicable to browser extensions.
* This test is included for consistency with Android and iOS test suites but is skipped.
*/
it.skip('should properly handle Android package names in filtering', () => {
/**
* Android package name detection is not implemented in browser extensions
* since they only deal with web URLs, not Android app contexts.
*/
});
// [#22] - Test multi-part TLDs like .com.au don't match incorrectly
it('should handle multi-part TLDs correctly without false matches', () => {
// Create test data with different .com.au domains
const australianCredentials = [
createTestCredential('Example Site AU', 'https://example.com.au', 'user@example.com.au'),
createTestCredential('BlaBla AU', 'https://blabla.blabla.com.au', 'user@blabla.com.au'),
createTestCredential('Another AU', 'https://another.com.au', 'user@another.com.au'),
createTestCredential('UK Site', 'https://example.co.uk', 'user@example.co.uk'),
];
// Test that blabla.blabla.com.au doesn't match other .com.au sites
const blablaMatches = filterCredentials(
australianCredentials,
'https://blabla.blabla.com.au',
''
);
expect(blablaMatches).toHaveLength(1);
expect(blablaMatches[0].ServiceName).toBe('BlaBla AU');
// Test that example.com.au doesn't match blabla.blabla.com.au
const exampleMatches = filterCredentials(
australianCredentials,
'https://example.com.au',
''
);
expect(exampleMatches).toHaveLength(1);
expect(exampleMatches[0].ServiceName).toBe('Example Site AU');
// Test that .co.uk domains work correctly too
const ukMatches = filterCredentials(
australianCredentials,
'https://example.co.uk',
''
);
expect(ukMatches).toHaveLength(1);
expect(ukMatches[0].ServiceName).toBe('UK Site');
});
/**
* Creates the shared test credential dataset used across all platforms.
* Note: when making changes to this list, make sure to update the corresponding list for iOS and Android tests as well.

View File

@@ -1,19 +1,17 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { HashRouter as Router, Routes, Route } from 'react-router-dom';
import { HashRouter as Router, Routes, Route, useLocation } from 'react-router-dom';
import { sendMessage } from 'webext-bridge/popup';
import { ClipboardCountdownBar } from '@/entrypoints/popup/components/ClipboardCountdownBar';
import BottomNav from '@/entrypoints/popup/components/Layout/BottomNav';
import Header from '@/entrypoints/popup/components/Layout/Header';
import DefaultLayout from '@/entrypoints/popup/components/Layout/DefaultLayout';
import PasskeyLayout from '@/entrypoints/popup/components/Layout/PasskeyLayout';
import LoadingSpinner from '@/entrypoints/popup/components/LoadingSpinner';
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
import { useApp } from '@/entrypoints/popup/context/AppContext';
import { useHeaderButtons } from '@/entrypoints/popup/context/HeaderButtonsContext';
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
import { NavigationProvider } from '@/entrypoints/popup/context/NavigationContext';
import AuthSettings from '@/entrypoints/popup/pages/auth/AuthSettings';
import Login from '@/entrypoints/popup/pages/auth/Login';
import Logout from '@/entrypoints/popup/pages/auth/Logout';
import Unlock from '@/entrypoints/popup/pages/auth/Unlock';
import UnlockSuccess from '@/entrypoints/popup/pages/auth/UnlockSuccess';
import Upgrade from '@/entrypoints/popup/pages/auth/Upgrade';
@@ -23,17 +21,31 @@ import CredentialsList from '@/entrypoints/popup/pages/credentials/CredentialsLi
import EmailDetails from '@/entrypoints/popup/pages/emails/EmailDetails';
import EmailsList from '@/entrypoints/popup/pages/emails/EmailsList';
import Index from '@/entrypoints/popup/pages/Index';
import PasskeyAuthenticate from '@/entrypoints/popup/pages/passkeys/PasskeyAuthenticate';
import PasskeyCreate from '@/entrypoints/popup/pages/passkeys/PasskeyCreate';
import Reinitialize from '@/entrypoints/popup/pages/Reinitialize';
import AutofillSettings from '@/entrypoints/popup/pages/settings/AutofillSettings';
import AutoLockSettings from '@/entrypoints/popup/pages/settings/AutoLockSettings';
import ClipboardSettings from '@/entrypoints/popup/pages/settings/ClipboardSettings';
import ContextMenuSettings from '@/entrypoints/popup/pages/settings/ContextMenuSettings';
import LanguageSettings from '@/entrypoints/popup/pages/settings/LanguageSettings';
import PasskeySettings from '@/entrypoints/popup/pages/settings/PasskeySettings';
import Settings from '@/entrypoints/popup/pages/settings/Settings';
import { useMinDurationLoading } from '@/hooks/useMinDurationLoading';
import '@/entrypoints/popup/style.css';
import { clearPendingRedirectUrl } from './hooks/useVaultLockRedirect';
/**
* Available layout types for different page contexts.
*/
enum LayoutType {
/** Default layout with header, footer navigation, and full UI */
DEFAULT = 'default',
/** Minimal layout for passkey operations - logo only, no footer */
PASSKEY = 'passkey',
}
/**
* Route configuration.
@@ -43,6 +55,81 @@ type RouteConfig = {
element: React.ReactNode;
showBackButton?: boolean;
title?: string;
/** Layout type to use for this route. Defaults to LayoutType.DEFAULT if not specified. */
layout?: LayoutType;
};
/**
* AppContent - Wrapper component that switches between different layout types
*/
const AppContent: React.FC<{
routes: RouteConfig[];
isLoading: boolean;
message: string | null;
headerButtons: React.ReactNode;
}> = ({ routes, isLoading, message, headerButtons }) => {
const location = useLocation();
// Find the current route configuration
const currentRoute = routes.find(route => {
const pattern = route.path.replace(/:\w+/g, '[^/]+');
const regex = new RegExp(`^${pattern}$`);
return regex.test(location.pathname);
});
// Get layout type, defaulting to DEFAULT if not specified
const layoutType = currentRoute?.layout ?? LayoutType.DEFAULT;
// Common loading overlay
const loadingOverlay = isLoading && (
<div className="fixed inset-0 bg-white dark:bg-gray-900 z-50 flex items-center justify-center">
<LoadingSpinner />
</div>
);
// Common routes component
const routesComponent = (
<Routes>
{routes.map((route) => (
<Route
key={route.path}
path={route.path}
element={route.element}
/>
))}
</Routes>
);
// Render based on layout type
switch (layoutType) {
case LayoutType.PASSKEY:
// Passkey layout - minimal UI with just logo header
return (
<PasskeyLayout>
{loadingOverlay}
{message && (
<p className="text-red-500 mb-4">{message}</p>
)}
{routesComponent}
</PasskeyLayout>
);
case LayoutType.DEFAULT:
default:
// Default layout with full header, footer, navigation
return (
<>
{loadingOverlay}
<DefaultLayout
routes={routes}
headerButtons={headerButtons}
message={message}
>
{routesComponent}
</DefaultLayout>
</>
);
}
};
/**
@@ -50,7 +137,7 @@ type RouteConfig = {
*/
const App: React.FC = () => {
const { t } = useTranslation();
const authContext = useAuth();
const app = useApp();
const { isInitialLoading } = useLoading();
const [isLoading, setIsLoading] = useMinDurationLoading(true, 150);
const [message, setMessage] = useState<string | null>(null);
@@ -69,6 +156,8 @@ const App: React.FC = () => {
{ path: '/credentials/add', element: <CredentialAddEdit />, showBackButton: true, title: t('credentials.addCredential') },
{ path: '/credentials/:id', element: <CredentialDetails />, showBackButton: true, title: t('credentials.credentialDetails') },
{ path: '/credentials/:id/edit', element: <CredentialAddEdit />, showBackButton: true, title: t('credentials.editCredential') },
{ path: '/passkeys/create', element: <PasskeyCreate />, layout: LayoutType.PASSKEY },
{ path: '/passkeys/authenticate', element: <PasskeyAuthenticate />, layout: LayoutType.PASSKEY },
{ path: '/emails', element: <EmailsList />, showBackButton: false },
{ path: '/emails/:id', element: <EmailDetails />, showBackButton: true, title: t('emails.title') },
{ path: '/settings', element: <Settings />, showBackButton: false },
@@ -77,7 +166,7 @@ const App: React.FC = () => {
{ path: '/settings/clipboard', element: <ClipboardSettings />, showBackButton: true, title: t('settings.clipboardSettings') },
{ path: '/settings/language', element: <LanguageSettings />, showBackButton: true, title: t('settings.language') },
{ path: '/settings/auto-lock', element: <AutoLockSettings />, showBackButton: true, title: t('settings.autoLockTimeout') },
{ path: '/logout', element: <Logout />, showBackButton: false },
{ path: '/settings/passkeys', element: <PasskeySettings />, showBackButton: true, title: t('settings.passkeySettings') },
], [t]);
useEffect(() => {
@@ -109,57 +198,36 @@ const App: React.FC = () => {
};
}, []);
/**
* On initial load, clear any stale pending redirect URL if popup was not opened with a specific hash path.
*/
useEffect(() => {
const hasHashPath = window.location.hash && window.location.hash !== '#/' && window.location.hash !== '#';
if (!hasHashPath) {
clearPendingRedirectUrl();
}
}, []);
/**
* Print global message if it exists.
*/
useEffect(() => {
if (authContext.globalMessage) {
setMessage(authContext.globalMessage);
if (app.globalMessage) {
setMessage(app.globalMessage);
} else {
setMessage(null);
}
}, [authContext, authContext.globalMessage]);
}, [app, app.globalMessage]);
return (
<Router>
<NavigationProvider>
<div className="min-h-screen min-w-[350px] bg-white dark:bg-gray-900 flex flex-col max-h-[600px]">
{isLoading && (
<div className="fixed inset-0 bg-white dark:bg-gray-900 z-50 flex items-center justify-center">
<LoadingSpinner />
</div>
)}
<ClipboardCountdownBar />
<Header
routes={routes}
rightButtons={headerButtons}
/>
<main
className="flex-1 overflow-y-auto bg-gray-100 dark:bg-gray-900"
style={{
paddingTop: '64px',
height: 'calc(100% - 120px)',
}}
>
<div className="p-4 mb-16">
{message && (
<p className="text-red-500 mb-4">{message}</p>
)}
<Routes>
{routes.map((route) => (
<Route
key={route.path}
path={route.path}
element={route.element}
/>
))}
</Routes>
</div>
</main>
<BottomNav />
</div>
<AppContent
routes={routes}
isLoading={isLoading}
message={message}
headerButtons={headerButtons}
/>
</NavigationProvider>
</Router>
);

View File

@@ -0,0 +1,31 @@
import React from 'react';
type AlertVariant = 'info' | 'warning' | 'error' | 'success';
interface IAlertProps {
variant: AlertVariant;
children: React.ReactNode;
className?: string;
}
/**
* Reusable alert component with consistent styling
*/
const Alert: React.FC<IAlertProps> = ({ variant, children, className = '' }) => {
const variantStyles = {
info: 'bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800 text-blue-800 dark:text-blue-200',
warning: 'bg-yellow-50 dark:bg-yellow-900/20 border-yellow-200 dark:border-yellow-800 text-yellow-800 dark:text-yellow-200',
error: 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800 text-red-800 dark:text-red-200',
success: 'bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800 text-green-800 dark:text-green-200'
};
return (
<div className={`p-3 border rounded-lg ${variantStyles[variant]} ${className}`}>
<p className="text-sm">
{children}
</p>
</div>
);
};
export default Alert;

View File

@@ -1,7 +1,8 @@
import React from 'react';
import React, { forwardRef } from 'react';
type ButtonProps = {
onClick?: () => void;
id?: string;
children: React.ReactNode;
type?: 'button' | 'submit' | 'reset';
variant?: 'primary' | 'secondary';
@@ -10,12 +11,13 @@ type ButtonProps = {
/**
* Button component
*/
const Button: React.FC<ButtonProps> = ({
const Button = forwardRef<HTMLButtonElement, ButtonProps>(({
onClick,
id,
children,
type = 'button',
variant = 'primary'
}) => {
}, ref) => {
const colorClasses = {
primary: 'bg-primary-500 hover:bg-primary-600',
secondary: 'bg-gray-500 hover:bg-gray-600'
@@ -23,13 +25,17 @@ const Button: React.FC<ButtonProps> = ({
return (
<button
ref={ref}
className={`${colorClasses[variant]} text-white font-medium rounded-lg px-4 py-2 text-sm w-full`}
onClick={onClick}
type={type}
id={id}
>
{children}
</button>
);
};
});
Button.displayName = 'Button';
export default Button;

View File

@@ -1,54 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { FormInputCopyToClipboard } from '@/entrypoints/popup/components/FormInputCopyToClipboard';
import type { Credential } from '@/utils/dist/shared/models/vault';
type LoginCredentialsBlockProps = {
credential: Credential;
}
/**
* Render the login credentials block.
*/
const LoginCredentialsBlock: React.FC<LoginCredentialsBlockProps> = ({ credential }) => {
const { t } = useTranslation();
const email = credential.Alias?.Email?.trim();
const username = credential.Username?.trim();
const password = credential.Password?.trim();
if (!email && !username && !password) {
return null;
}
return (
<div className="space-y-2">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">{t('common.loginCredentials')}</h2>
{email && (
<FormInputCopyToClipboard
id="email"
label={t('common.email')}
value={email}
/>
)}
{username && (
<FormInputCopyToClipboard
id="username"
label={t('common.username')}
value={username}
/>
)}
{password && (
<FormInputCopyToClipboard
id="password"
label={t('common.password')}
value={password}
type="password"
/>
)}
</div>
);
};
export default LoginCredentialsBlock;

View File

@@ -69,8 +69,38 @@ const CredentialCard: React.FC<CredentialCardProps> = ({ credential }) => {
target.src = '/assets/images/service-placeholder.webp';
}}
/>
<div className="text-left">
<p className="font-medium text-gray-900 dark:text-white">{getCredentialServiceName(credential)}</p>
<div className="text-left flex-1">
<div className="flex items-center gap-1.5">
<p className="font-medium text-gray-900 dark:text-white">{getCredentialServiceName(credential)}</p>
{credential.HasPasskey && (
<svg
className="w-3.5 h-3.5 text-gray-500 dark:text-gray-400"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-label="Has passkey"
>
<path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4" />
</svg>
)}
{credential.HasAttachment && (
<svg
className="w-3.5 h-3.5 text-gray-500 dark:text-gray-400"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-label="Has attachments"
>
<path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" />
</svg>
)}
</div>
<p className="text-sm text-gray-600 dark:text-gray-400">{getDisplayText(credential)}</p>
</div>
</button>

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { FormInputCopyToClipboard } from '@/entrypoints/popup/components/FormInputCopyToClipboard';
import { FormInputCopyToClipboard } from '@/entrypoints/popup/components/Forms/FormInputCopyToClipboard';
import { IdentityHelperUtils } from '@/utils/dist/shared/identity-generator';
import type { Credential } from '@/utils/dist/shared/models/vault';

View File

@@ -27,7 +27,7 @@ const EmailBlock: React.FC<EmailBlockProps> = ({ email }) => {
const privateDomains = vaultMetadata?.privateEmailDomains ?? [];
return [...publicDomains, ...privateDomains].some(supportedDomain =>
domain === supportedDomain || domain.endsWith(`.${supportedDomain}`)
domain === supportedDomain.toLowerCase()
);
};

View File

@@ -0,0 +1,93 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { FormInputCopyToClipboard } from '@/entrypoints/popup/components/Forms/FormInputCopyToClipboard';
import type { Credential } from '@/utils/dist/shared/models/vault';
type LoginCredentialsBlockProps = {
credential: Credential;
}
/**
* Render the login credentials block.
*/
const LoginCredentialsBlock: React.FC<LoginCredentialsBlockProps> = ({ credential }) => {
const { t } = useTranslation();
const email = credential.Alias?.Email?.trim();
const username = credential.Username?.trim();
const password = credential.Password?.trim();
if (!email && !username && !password && !credential.HasPasskey) {
return null;
}
return (
<div className="space-y-2">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">{t('common.loginCredentials')}</h2>
{email && (
<FormInputCopyToClipboard
id="email"
label={t('common.email')}
value={email}
/>
)}
{username && (
<FormInputCopyToClipboard
id="username"
label={t('common.username')}
value={username}
/>
)}
{credential.HasPasskey && (
<div className="p-3 rounded bg-gray-50 dark:bg-gray-900 border border-gray-200 dark:border-gray-700">
<div className="flex items-start gap-2">
<svg
className="w-5 h-5 text-gray-600 dark:text-gray-400 mt-0.5 flex-shrink-0"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4" />
</svg>
<div className="flex-1">
<div className="mb-1">
<span className="text-sm font-medium text-gray-900 dark:text-white">{t('passkeys.passkey')}</span>
</div>
<div className="space-y-1 mb-2">
{credential.PasskeyRpId && (
<div>
<span className="text-xs text-gray-500 dark:text-gray-400">{t('passkeys.site')}: </span>
<span className="text-sm text-gray-900 dark:text-white">{credential.PasskeyRpId}</span>
</div>
)}
{credential.PasskeyDisplayName && (
<div>
<span className="text-xs text-gray-500 dark:text-gray-400">{t('passkeys.displayName')}: </span>
<span className="text-sm text-gray-900 dark:text-white">{credential.PasskeyDisplayName}</span>
</div>
)}
</div>
<p className="text-xs text-gray-600 dark:text-gray-400">
{t('passkeys.helpText')}
</p>
</div>
</div>
</div>
)}
{password && (
<FormInputCopyToClipboard
id="password"
label={t('common.password')}
value={password}
type="password"
/>
)}
</div>
);
};
export default LoginCredentialsBlock;

View File

@@ -39,7 +39,7 @@ const HelpModal: React.FC<HelpModalProps> = ({ titleKey, contentKey, className =
</button>
{showModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50 p-4">
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-sm w-full">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">

View File

@@ -37,7 +37,7 @@ const Modal: React.FC<IModalProps> = ({
return (
<div className="fixed inset-0 z-50 overflow-y-auto">
{/* Backdrop */}
<div className="fixed inset-0 bg-black bg-opacity-60 transition-opacity" onClick={onClose} />
<div className="fixed inset-0 bg-black bg-opacity-80 transition-opacity" onClick={onClose} />
{/* Modal */}
<div className="fixed inset-0 flex items-center justify-center p-4">

View File

@@ -0,0 +1,60 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import Button from '../Button';
type PasskeyBypassDialogProps = {
origin: string;
onChoice: (choice: 'once' | 'always') => void;
onCancel: () => void;
};
/**
* Dialog for choosing how to bypass AliasVault passkey provider
*/
const PasskeyBypassDialog: React.FC<PasskeyBypassDialogProps> = ({
origin,
onChoice,
onCancel
}) => {
const { t } = useTranslation();
return (
<div className="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50 p-4">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full p-6">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">
{t('passkeys.bypass.title')}
</h2>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-6">
{t('passkeys.bypass.description', { origin })}
</p>
<div className="space-y-3">
<Button
variant="primary"
onClick={() => onChoice('once')}
>
{t('passkeys.bypass.thisTimeOnly')}
</Button>
<Button
variant="secondary"
onClick={() => onChoice('always')}
>
{t('passkeys.bypass.alwaysForSite')}
</Button>
<Button
variant="secondary"
onClick={onCancel}
>
{t('common.cancel')}
</Button>
</div>
</div>
</div>
);
};
export default PasskeyBypassDialog;

View File

@@ -57,7 +57,7 @@ export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) => {
const isPublicDomain = async (emailAddress: string): Promise<boolean> => {
// Get metadata from storage
const publicEmailDomains = await storage.getItem('session:publicEmailDomains') as string[] ?? [];
return publicEmailDomains.some(domain => emailAddress.toLowerCase().endsWith(domain));
return publicEmailDomains.some(domain => emailAddress.toLowerCase().endsWith(`@${domain.toLowerCase()}`));
};
/**
@@ -66,7 +66,7 @@ export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) => {
const isPrivateDomain = async (emailAddress: string): Promise<boolean> => {
// Get metadata from storage
const privateEmailDomains = await storage.getItem('session:privateEmailDomains') as string[] ?? [];
return privateEmailDomains.some(domain => emailAddress.toLowerCase().endsWith(domain));
return privateEmailDomains.some(domain => emailAddress.toLowerCase().endsWith(`@${domain.toLowerCase()}`));
};
useEffect(() => {

View File

@@ -101,7 +101,8 @@ const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
}
}
}
}, [value, privateEmailDomains, showPrivateDomains, selectedDomain]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [value, privateEmailDomains, showPrivateDomains]);
// Handle local part changes
const handleLocalPartChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {

View File

@@ -1,13 +1,12 @@
import React, { useState, useEffect, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import PasswordConfigDialog from '@/entrypoints/popup/components/Dialogs/PasswordConfigDialog';
import { useDb } from '@/entrypoints/popup/context/DbContext';
import type { PasswordSettings } from '@/utils/dist/shared/models/vault';
import { CreatePasswordGenerator } from '@/utils/dist/shared/password-generator';
import PasswordConfigDialog from './PasswordConfigDialog';
interface IPasswordFieldProps {
id: string;
label: string;

View File

@@ -34,7 +34,7 @@ const BottomNav: React.FC = () => {
};
// Auth pages that don't show bottom navigation but still show header
const authPages = ['/login', '/auth-settings', '/unlock', '/unlock-success', '/upgrade'];
const authPages = ['/', '/login', '/auth-settings', '/unlock', '/unlock-success', '/upgrade'];
const isAuthPage = authPages.includes(location.pathname);
if (isAuthPage) {

View File

@@ -0,0 +1,72 @@
import React from 'react';
import { Routes, Route } from 'react-router-dom';
import { ClipboardCountdownBar } from '@/entrypoints/popup/components/ClipboardCountdownBar';
import BottomNav from '@/entrypoints/popup/components/Layout/BottomNav';
import Header from '@/entrypoints/popup/components/Layout/Header';
/**
* Route configuration type.
*/
type RouteConfig = {
path: string;
element: React.ReactNode;
showBackButton?: boolean;
title?: string;
};
/**
* DefaultLayout props.
*/
type DefaultLayoutProps = {
routes: RouteConfig[];
headerButtons: React.ReactNode;
message?: string | null;
children?: React.ReactNode;
};
/**
* DefaultLayout - Standard layout with full header, footer navigation, and complete UI.
* This is the main layout used for most pages in the extension.
*/
const DefaultLayout: React.FC<DefaultLayoutProps> = ({ routes, headerButtons, message, children }) => {
return (
<div className="min-h-screen min-w-[350px] bg-white dark:bg-gray-900 flex flex-col max-h-[600px]">
<ClipboardCountdownBar />
<Header
routes={routes}
rightButtons={headerButtons}
/>
<main
className="flex-1 overflow-y-auto bg-gray-100 dark:bg-gray-900"
style={{
paddingTop: '64px',
height: 'calc(100% - 120px)',
}}
>
<div className="p-4 mb-16">
{message && (
<p className="text-red-500 mb-4">{message}</p>
)}
{children || (
<Routes>
{routes.map((route) => (
<Route
key={route.path}
path={route.path}
element={route.element}
/>
))}
</Routes>
)}
</div>
</main>
<BottomNav />
</div>
);
};
export default DefaultLayout;

View File

@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
import { useNavigate, useLocation } from 'react-router-dom';
import Logo from '@/entrypoints/popup/components/Logo';
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
import { useApp } from '@/entrypoints/popup/context/AppContext';
/**
* Header props.
@@ -25,7 +25,7 @@ const Header: React.FC<HeaderProps> = ({
rightButtons
}) => {
const { t } = useTranslation();
const authContext = useAuth();
const app = useApp();
const navigate = useNavigate();
const location = useLocation();
@@ -54,7 +54,7 @@ const Header: React.FC<HeaderProps> = ({
}
// If logged in, navigate to credentials.
if (authContext.isLoggedIn) {
if (app.isLoggedIn) {
navigate('/credentials');
} else {
// If not logged in, navigate to index.
@@ -105,7 +105,7 @@ const Header: React.FC<HeaderProps> = ({
<div className="flex-grow" />
<div className="flex items-center gap-2">
{!authContext.isLoggedIn ? (
{!app.isLoggedIn ? (
<>
{rightButtons}
<button

View File

@@ -0,0 +1,43 @@
import React from 'react';
import Logo from '@/entrypoints/popup/components/Logo';
/**
* PasskeyLayout - Minimal layout for passkey create/authenticate pages.
* Shows only the AliasVault logo header, no navigation, no footer.
*/
const PasskeyLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
return (
<div className="min-h-screen min-w-[350px] bg-white dark:bg-gray-900 flex flex-col max-h-[600px]">
{/* Minimal header with just logo */}
<header className="fixed z-30 w-full bg-white border-b border-gray-200 dark:bg-gray-800 dark:border-gray-700">
<div className="flex items-center justify-center h-16 px-4">
<Logo
width={125}
height={40}
showText={true}
className="text-gray-900 dark:text-white"
/>
{/* Hide beta badge on Safari as it's not allowed to show non-production badges */}
{!import.meta.env.SAFARI && (
<span className="text-primary-500 text-[10px] font-normal">BETA</span>
)}
</div>
</header>
{/* Main content without footer padding */}
<main
className="flex-1 overflow-y-auto bg-gray-100 dark:bg-gray-900"
style={{
paddingTop: '64px',
}}
>
<div className="p-4">
{children}
</div>
</main>
</div>
);
};
export default PasskeyLayout;

View File

@@ -0,0 +1,120 @@
import React, { createContext, useContext, useMemo, useCallback, useEffect, useState, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
import { useWebApi } from '@/entrypoints/popup/context/WebApiContext';
import { logoutEventEmitter } from '@/events/LogoutEventEmitter';
type AppContextType = {
isLoggedIn: boolean;
isInitialized: boolean;
username: string | null;
logout: (errorMessage?: string) => Promise<void>;
initializeAuth: () => Promise<boolean>;
setAuthTokens: (username: string, accessToken: string, refreshToken: string) => Promise<void>;
globalMessage: string | null;
clearGlobalMessage: () => void;
}
const AppContext = createContext<AppContextType | undefined>(undefined);
/**
* AppProvider that coordinates between auth, db, and webApi contexts.
*/
export const AppProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const auth = useAuth();
const webApi = useWebApi();
const [isLoggedIn, setIsLoggedIn] = useState(false);
const isLoggingOutRef = useRef(false);
const { t } = useTranslation();
/**
* Logout the user by revoking tokens and clearing the auth tokens from storage.
* Prevents recursive logout calls by tracking logout state.
*/
const logout = useCallback(async (errorMessage?: string): Promise<void> => {
if (isLoggingOutRef.current) {
return;
}
try {
isLoggingOutRef.current = true;
await webApi.revokeTokens();
await auth.clearAuth(errorMessage);
} catch (error) {
console.error('Error during logout:', error);
} finally {
isLoggingOutRef.current = false;
setIsLoggedIn(false);
}
}, [auth, webApi]);
/**
* Initialize the authentication state.
*
* @returns boolean indicating whether the user is logged in.
*/
const initializeAuth = useCallback(async () : Promise<boolean> => {
const isLoggedIn = await auth.initializeAuth();
setIsLoggedIn(isLoggedIn);
return isLoggedIn;
}, [auth]);
/**
* Subscribe to logout events from WebApiService.
*/
useEffect(() => {
const unsubscribe = logoutEventEmitter.subscribe(async (errorKey: string) => {
await logout(t(errorKey));
});
return unsubscribe;
}, [logout, t]);
/**
* Check for tokens in browser local storage on initial load when this context is mounted.
*/
useEffect(() => {
initializeAuth();
}, [initializeAuth]);
const contextValue = useMemo(() => ({
// Pass through auth state
isInitialized: auth.isInitialized,
username: auth.username,
globalMessage: auth.globalMessage,
// Wrap auth methods
logout,
initializeAuth,
setAuthTokens: auth.setAuthTokens,
clearGlobalMessage: auth.clearGlobalMessage,
isLoggedIn: isLoggedIn,
}), [
auth.isInitialized,
auth.username,
auth.globalMessage,
auth.setAuthTokens,
auth.clearGlobalMessage,
logout,
initializeAuth,
isLoggedIn,
]);
return (
<AppContext.Provider value={contextValue}>
{children}
</AppContext.Provider>
);
};
/**
* Hook to use the AppContext.
*/
export const useApp = (): AppContextType => {
const context = useContext(AppContext);
if (context === undefined) {
throw new Error('useApp must be used within an AppProvider');
}
return context;
};

View File

@@ -1,4 +1,4 @@
import React, { createContext, useContext, useState, useEffect, useMemo, useCallback } from 'react';
import React, { createContext, useContext, useState, useMemo, useCallback } from 'react';
import { sendMessage } from 'webext-bridge/popup';
import { useDb } from '@/entrypoints/popup/context/DbContext';
@@ -8,13 +8,11 @@ import { VAULT_LOCKED_DISMISS_UNTIL_KEY } from '@/utils/Constants';
import { storage } from '#imports';
type AuthContextType = {
isLoggedIn: boolean;
isInitialized: boolean;
username: string | null;
initializeAuth: () => Promise<{ isLoggedIn: boolean }>;
initializeAuth: () => Promise<boolean>;
setAuthTokens: (username: string, accessToken: string, refreshToken: string) => Promise<void>;
login: () => Promise<void>;
logout: (errorMessage?: string) => Promise<void>;
clearAuth: (errorMessage?: string) => Promise<void>;
globalMessage: string | null;
clearGlobalMessage: () => void;
}
@@ -28,7 +26,6 @@ const AuthContext = createContext<AuthContextType | undefined>(undefined);
* AuthProvider to provide the authentication state to the app that components can use.
*/
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [isInitialized, setIsInitialized] = useState(false);
const [username, setUsername] = useState<string | null>(null);
const [globalMessage, setGlobalMessage] = useState<string | null>(null);
@@ -37,30 +34,20 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
/**
* Initialize the authentication state.
*
* @returns object containing whether the user is logged in.
* @returns boolean indicating whether the user is logged in.
*/
const initializeAuth = useCallback(async () : Promise<{ isLoggedIn: boolean }> => {
let isLoggedIn = false;
const initializeAuth = useCallback(async () : Promise<boolean> => {
const accessToken = await storage.getItem('local:accessToken') as string;
const refreshToken = await storage.getItem('local:refreshToken') as string;
const username = await storage.getItem('local:username') as string;
setIsInitialized(true);
if (accessToken && refreshToken && username) {
setUsername(username);
setIsLoggedIn(true);
isLoggedIn = true;
return true;
}
setIsInitialized(true);
return { isLoggedIn };
}, [setUsername, setIsLoggedIn]);
/**
* Check for tokens in browser local storage on initial load when this context is mounted.
*/
useEffect(() => {
initializeAuth();
}, [initializeAuth]);
return false;
}, [setUsername]);
/**
* Set auth tokens in browser local storage as part of the login process. After db is initialized, the login method should be called as well.
@@ -70,23 +57,18 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
await storage.setItem('local:accessToken', accessToken);
await storage.setItem('local:refreshToken', refreshToken);
// Clear dismiss until (which can be enabled after user has dimissed vault is locked popup) to ensure popup is shown.
await storage.setItem(VAULT_LOCKED_DISMISS_UNTIL_KEY, 0);
setUsername(username);
}, []);
/**
* Set logged in status to true which refreshes the app.
* Clear authentication data and tokens from storage.
* This is called by AppContext after revoking tokens on the server.
*/
const login = useCallback(async () : Promise<void> => {
setIsLoggedIn(true);
// Clear dismiss until (which can be enabled after user has dimissed vault is locked popup) to ensure popup is shown.
await storage.setItem(VAULT_LOCKED_DISMISS_UNTIL_KEY, 0);
}, []);
/**
* Logout the user and clear the auth tokens from chrome storage.
*/
const logout = useCallback(async (errorMessage?: string) : Promise<void> => {
const clearAuth = useCallback(async (errorMessage?: string) : Promise<void> => {
// Clear vault from background worker and remove local storage tokens.
await sendMessage('CLEAR_VAULT', {}, 'background');
await storage.removeItems(['local:username', 'local:accessToken', 'local:refreshToken']);
dbContext?.clearDatabase();
@@ -97,7 +79,6 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
}
setUsername(null);
setIsLoggedIn(false);
}, [dbContext]);
/**
@@ -108,16 +89,14 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
}, []);
const contextValue = useMemo(() => ({
isLoggedIn,
isInitialized,
username,
initializeAuth,
setAuthTokens,
login,
logout,
clearAuth,
globalMessage,
clearGlobalMessage,
}), [isLoggedIn, isInitialized, username, initializeAuth, globalMessage, setAuthTokens, login, logout, clearGlobalMessage]);
}), [isInitialized, username, initializeAuth, globalMessage, setAuthTokens, clearAuth, clearGlobalMessage]);
return (
<AuthContext.Provider value={contextValue}>

View File

@@ -1,7 +1,7 @@
import React, { createContext, useContext, useEffect, useMemo, useCallback } from 'react';
import { useLocation } from 'react-router-dom';
import { useLocation, useNavigate } from 'react-router-dom';
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
import { useApp } from '@/entrypoints/popup/context/AppContext';
import { useDb } from '@/entrypoints/popup/context/DbContext';
import { storage } from '#imports';
@@ -29,21 +29,32 @@ const NavigationContext = createContext<NavigationContextType | undefined>(undef
*/
export const NavigationProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const location = useLocation();
const navigate = useNavigate();
// Auth and DB state
const { isInitialized: authInitialized, isLoggedIn } = useAuth();
const { dbInitialized, dbAvailable, upgradeRequired } = useDb();
const { isInitialized: authInitialized, isLoggedIn } = useApp();
const { dbInitialized, dbAvailable } = useDb();
// Derived state
const isFullyInitialized = authInitialized && dbInitialized;
const requiresAuth = isFullyInitialized && (!isLoggedIn || (!dbAvailable && !upgradeRequired));
const requiresAuth = isFullyInitialized && (!isLoggedIn || !dbAvailable);
/**
* Store the current page path, timestamp, and navigation history in storage.
*/
const storeCurrentPage = useCallback(async (): Promise<void> => {
// Pages that are not allowed to be stored as these are auth conditional pages.
const notAllowedPaths = ['/', '/reinitialize', '/login', '/unlock', '/unlock-success', '/auth-settings', '/upgrade', '/logout'];
// Pages that are not allowed to be stored as these are auth conditional pages or dedicated popup pages.
const notAllowedPaths = [
'/',
'/reinitialize',
'/login',
'/unlock',
'/unlock-success',
'/auth-settings',
'/upgrade',
'/passkeys/create',
'/passkeys/authenticate'
];
// Only store the page if we're fully initialized and don't need auth
if (isFullyInitialized && !requiresAuth && !notAllowedPaths.includes(location.pathname)) {
@@ -55,7 +66,7 @@ export const NavigationProvider: React.FC<{ children: React.ReactNode }> = ({ ch
let currentPath = '';
for (let i = 0; i < segments.length; i++) {
currentPath += '/' + segments[i];
/*
* For settings subpages, include both /settings and the subpage
* For email details, include both /emails and the specific email
@@ -82,6 +93,15 @@ export const NavigationProvider: React.FC<{ children: React.ReactNode }> = ({ ch
}
}, [location.pathname, location.search, location.hash, isFullyInitialized, storeCurrentPage]);
// Listen on isloggedin state to redirect to login page if not logged in
useEffect(() => {
if (isFullyInitialized && !isLoggedIn) {
navigate('/login', { replace: true });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isFullyInitialized, isLoggedIn]);
// Return the context value
const contextValue = useMemo(() => ({
storeCurrentPage,
isFullyInitialized,

View File

@@ -1,7 +1,5 @@
import React, { createContext, useContext, useEffect, useState } from 'react';
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
import { WebApiService } from '@/utils/WebApiService';
const WebApiContext = createContext<WebApiService | null>(null);
@@ -10,24 +8,15 @@ const WebApiContext = createContext<WebApiService | null>(null);
* WebApiProvider to provide the WebApiService to the app that components can use.
*/
export const WebApiProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const { logout } = useAuth();
const [webApiService, setWebApiService] = useState<WebApiService | null>(null);
/**
* Initialize WebApiService
*/
useEffect(() : void => {
const service = new WebApiService(
(statusError: string | null) => {
if (statusError) {
logout(statusError);
} else {
logout();
}
}
);
const service = new WebApiService();
setWebApiService(service);
}, [logout]);
}, []);
if (!webApiService) {
return null;

View File

@@ -0,0 +1,62 @@
import { useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { useDb } from '@/entrypoints/popup/context/DbContext';
import { PENDING_REDIRECT_URL_KEY } from '@/utils/Constants';
import { storage } from '#imports';
/**
* Hook to handle vault lock redirects.
* Automatically redirects to unlock page if vault is locked,
* preserving the current URL for restoration after unlock.
*/
export function useVaultLockRedirect(options: { enabled?: boolean } = {}): { isLocked: boolean } {
const { enabled = true } = options;
const location = useLocation();
const navigate = useNavigate();
const { dbInitialized, dbAvailable } = useDb();
useEffect(() => {
if (!enabled || !dbInitialized) {
return;
}
// Check if vault is locked
if (!dbAvailable) {
// Store the full current URL (pathname + search) for restoration after unlock
const currentUrl = `${location.pathname}${location.search}`;
storage.setItem(PENDING_REDIRECT_URL_KEY, currentUrl);
// Navigate to unlock without redirect in URL - we use storage instead
navigate('/unlock');
}
}, [enabled, dbInitialized, dbAvailable, location, navigate]);
return {
isLocked: dbInitialized && !dbAvailable
};
}
/**
* Get and clear the pending redirect URL from storage.
* Used by Reinitialize page to restore user's intended destination after unlock.
*
* @returns The pending redirect URL, or null if none exists
*/
export async function consumePendingRedirectUrl(): Promise<string | null> {
const url = await storage.getItem<string>(PENDING_REDIRECT_URL_KEY);
if (url) {
await storage.removeItem(PENDING_REDIRECT_URL_KEY);
}
return url;
}
/**
* Clear the pending redirect URL from storage.
* Used when popup is opened without a specific hash path to clear stale redirects.
*/
export async function clearPendingRedirectUrl(): Promise<void> {
await storage.removeItem(PENDING_REDIRECT_URL_KEY);
}

View File

@@ -43,55 +43,52 @@ export function useVaultMutate() : {
setSyncStatus(t('common.uploadingVaultToServer'));
try {
// Upload the updated vault to the server.
const base64Vault = dbContext.sqliteClient!.exportToBase64();
// Upload the updated vault to the server.
const base64Vault = dbContext.sqliteClient!.exportToBase64();
// Get encryption key from background worker
const encryptionKey = await sendMessage('GET_ENCRYPTION_KEY', {}, 'background') as string;
// Get encryption key from background worker
const encryptionKey = await sendMessage('GET_ENCRYPTION_KEY', {}, 'background') as string;
// Encrypt the vault.
const encryptedVaultBlob = await EncryptionUtility.symmetricEncrypt(
base64Vault,
encryptionKey
);
// Encrypt the vault.
const encryptedVaultBlob = await EncryptionUtility.symmetricEncrypt(
base64Vault,
encryptionKey
);
const request: UploadVaultRequest = {
vaultBlob: encryptedVaultBlob,
};
const request: UploadVaultRequest = {
vaultBlob: encryptedVaultBlob,
};
const response = await sendMessage('UPLOAD_VAULT', request, 'background') as messageVaultUploadResponse;
const response = await sendMessage('UPLOAD_VAULT', request, 'background') as messageVaultUploadResponse;
/*
* If we get here, it means we have a valid connection to the server.
* TODO: offline mode is not implemented for browser extension yet.
* authContext.setOfflineMode(false);
*/
/*
* If we get here, it means we have a valid connection to the server.
* TODO: offline mode is not implemented for browser extension yet.
* authContext.setOfflineMode(false);
*/
if (response.status === 0 && response.newRevisionNumber) {
await dbContext.setCurrentVaultRevisionNumber(response.newRevisionNumber);
options.onSuccess?.();
} else if (response.status === 1) {
// Note: vault merge is no longer allowed by the API as of 0.20.0, updates with the same revision number are rejected. So this check can be removed later.
throw new Error('Vault merge required. Please login via the web app to merge the multiple pending updates to your vault.');
} else if (response.status === 2) {
throw new Error('Your vault is outdated. Please login on the AliasVault website and follow the steps.');
} else {
throw new Error('Failed to upload vault to server. Please try again by re-opening the app.');
}
} catch (error) {
// Check if it's a network error
if (error instanceof Error && (error.message.includes('network') || error.message.includes('timeout'))) {
/*
* Network error, mark as offline and track pending changes
* TODO: offline mode is not implemented for browser extension yet.
* authContext.setOfflineMode(true);
*/
options.onError?.(new Error('Network error'));
return;
}
throw error;
if (response.status === 0 && response.newRevisionNumber) {
await dbContext.setCurrentVaultRevisionNumber(response.newRevisionNumber);
options.onSuccess?.();
} else if (response.status === 1) {
// Note: vault merge is no longer allowed by the API as of 0.20.0, updates with the same revision number are rejected. So this check can be removed later.
throw new Error('Vault merge required. Please login via the web app to merge the multiple pending updates to your vault.');
} else if (response.status === 2) {
throw new Error(t('common.errors.failedToUploadVault'));
} else {
throw new Error(t('common.errors.failedToUploadVault'));
}
// Check if it's a network error
/*
* if (error instanceof Error && (error.message.includes('network') || error.message.includes('timeout'))) {
*
* // Network error, mark as offline and track pending changes - TODO: offline mode is not implemented for browser extension yet.
* // authContext.setOfflineMode(true);
*options.onError?.(new Error('Network error'));
*return;
*}
*/
}, [dbContext, t]);
/**
@@ -130,28 +127,12 @@ export function useVaultMutate() : {
* Handle error during vault sync.
*/
onError: (error) => {
/**
*Toast.show({
*type: 'error',
*text1: 'Failed to sync vault',
*text2: error,
*position: 'bottom'
*});
*/
options.onError?.(new Error(error));
}
});
} catch (error) {
console.error('Error during vault mutation:', error);
/*
* Toast.show({
*type: 'error',
*text1: 'Operation failed',
*text2: error instanceof Error ? error.message : 'Unknown error',
*position: 'bottom'
*});
*/
options.onError?.(error instanceof Error ? error : new Error('Unknown error'));
options.onError?.(error instanceof Error ? error : new Error(t('common.errors.unknownError')));
} finally {
setIsLoading(false);
setSyncStatus('');

View File

@@ -2,12 +2,13 @@ import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { sendMessage } from 'webext-bridge/popup';
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
import { useApp } from '@/entrypoints/popup/context/AppContext';
import { useDb } from '@/entrypoints/popup/context/DbContext';
import { useWebApi } from '@/entrypoints/popup/context/WebApiContext';
import type { EncryptionKeyDerivationParams } from '@/utils/dist/shared/models/metadata';
import type { VaultResponse } from '@/utils/dist/shared/models/webapi';
import { VaultVersionIncompatibleError } from '@/utils/types/errors/VaultVersionIncompatibleError';
/**
* Utility function to ensure a minimum time has elapsed for an operation
@@ -49,7 +50,7 @@ export const useVaultSync = () : {
syncVault: (options?: VaultSyncOptions) => Promise<boolean>;
} => {
const { t } = useTranslation();
const authContext = useAuth();
const app = useApp();
const dbContext = useDb();
const webApi = useWebApi();
@@ -60,7 +61,7 @@ export const useVaultSync = () : {
const enableDelay = initialSync;
try {
const { isLoggedIn } = await authContext.initializeAuth();
const isLoggedIn = await app.initializeAuth();
if (!isLoggedIn) {
// Not authenticated, return false immediately
@@ -73,7 +74,9 @@ export const useVaultSync = () : {
// Check if server is actually available, 0.0.0 indicates connection error which triggers offline mode.
if (statusResponse.serverVersion === '0.0.0') {
// Offline mode is not implemented for browser extension yet, let it fail below due to the validateStatusResponse check.
// Offline mode is not implemented for browser extension yet, so logout the user.
onError?.(t('common.errors.serverNotAvailable'));
return false;
}
const statusError = webApi.validateStatusResponse(statusResponse);
@@ -91,7 +94,7 @@ export const useVaultSync = () : {
* longer valid and the user needs to re-authenticate. We trigger a logout but do not revoke tokens
* as these were already revoked by the server upon password change.
*/
await webApi.logout(t('common.errors.passwordChanged'));
await app.logout(t('common.errors.passwordChanged'));
return false;
}
@@ -109,24 +112,6 @@ export const useVaultSync = () : {
onStatus?.(t('common.syncingUpdatedVault'));
const vaultResponseJson = await withMinimumDelay(() => webApi.get<VaultResponse>('Vault'), 1000, enableDelay);
const vaultError = webApi.validateVaultResponse(vaultResponseJson as VaultResponse, t);
if (vaultError) {
// Only logout if it's an authentication error, not a network error
if (vaultError.includes('authentication') || vaultError.includes('unauthorized')) {
await webApi.logout(vaultError);
onError?.(vaultError);
return false;
}
/*
* TODO: browser extension does not support offline mode yet.
* For other errors, go into offline mode
* authContext.setOfflineMode(true);
*/
return false;
}
try {
// Get encryption key from background worker
const encryptionKey = await sendMessage('GET_ENCRYPTION_KEY', {}, 'background') as string;
@@ -142,9 +127,8 @@ export const useVaultSync = () : {
return true;
} catch (error) {
// Check if it's a version-related error (app needs to be updated)
if (error instanceof Error && error.message.includes('This browser extension is outdated')) {
await webApi.logout(error.message);
onError?.(error.message);
if (error instanceof VaultVersionIncompatibleError) {
await app.logout(error.message);
return false;
}
// Vault could not be decrypted, throw an error
@@ -165,9 +149,8 @@ export const useVaultSync = () : {
console.error('Vault sync error:', err);
// Check if it's a version-related error (app needs to be updated)
if (errorMessage.includes('This browser extension is outdated')) {
await webApi.logout(errorMessage);
onError?.(errorMessage);
if (err instanceof VaultVersionIncompatibleError) {
await app.logout(errorMessage);
return false;
}
@@ -185,7 +168,7 @@ export const useVaultSync = () : {
onError?.(errorMessage);
return false;
}
}, [authContext, dbContext, webApi, t]);
}, [app, dbContext, webApi, t]);
return { syncVault };
};

View File

@@ -1,6 +1,7 @@
import ReactDOM from 'react-dom/client';
import App from '@/entrypoints/popup/App';
import { AppProvider } from '@/entrypoints/popup/context/AppContext';
import { AuthProvider } from '@/entrypoints/popup/context/AuthContext';
import { DbProvider } from '@/entrypoints/popup/context/DbContext';
import { HeaderButtonsProvider } from '@/entrypoints/popup/context/HeaderButtonsContext';
@@ -17,17 +18,19 @@ const renderApp = (): void => {
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
<DbProvider>
<AuthProvider>
<WebApiProvider>
<LoadingProvider>
<HeaderButtonsProvider>
<ThemeProvider>
<App />
</ThemeProvider>
</HeaderButtonsProvider>
</LoadingProvider>
</WebApiProvider>
</AuthProvider>
<WebApiProvider>
<AuthProvider>
<AppProvider>
<LoadingProvider>
<HeaderButtonsProvider>
<ThemeProvider>
<App />
</ThemeProvider>
</HeaderButtonsProvider>
</LoadingProvider>
</AppProvider>
</AuthProvider>
</WebApiProvider>
</DbProvider>
);
};

View File

@@ -2,9 +2,10 @@ import React, { useCallback, useEffect, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { sendMessage } from 'webext-bridge/popup';
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
import { useApp } from '@/entrypoints/popup/context/AppContext';
import { useDb } from '@/entrypoints/popup/context/DbContext';
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
import { consumePendingRedirectUrl } from '@/entrypoints/popup/hooks/useVaultLockRedirect';
import { useVaultSync } from '@/entrypoints/popup/hooks/useVaultSync';
import { storage } from '#imports';
@@ -31,7 +32,7 @@ const Reinitialize: React.FC = () => {
const hasInitialized = useRef(false);
// Auth and DB state
const { isInitialized: authInitialized, isLoggedIn } = useAuth();
const { isInitialized: authInitialized, isLoggedIn } = useApp();
const { dbInitialized, dbAvailable } = useDb();
// Derived state
@@ -78,11 +79,20 @@ const Reinitialize: React.FC = () => {
}, [navigate]);
useEffect(() => {
// Check for inline unlock mode
const urlParams = new URLSearchParams(window.location.search);
const inlineUnlock = urlParams.get('mode') === 'inline_unlock';
/**
* Handle initialization and redirect logic
*/
const handleInitialization = async (): Promise<void> => {
// Check for inline unlock mode
const urlParams = new URLSearchParams(window.location.search);
const inlineUnlock = urlParams.get('mode') === 'inline_unlock';
if (isFullyInitialized) {
// Check for pending redirect URL in storage (set by useVaultLockRedirect hook)
const pendingRedirectUrl = await consumePendingRedirectUrl();
if (!isFullyInitialized) {
return;
}
// Prevent multiple vault syncs (only run sync once)
const shouldRunSync = !hasInitialized.current;
@@ -110,6 +120,10 @@ const Reinitialize: React.FC = () => {
if (inlineUnlock) {
setIsInitialLoading(false);
navigate('/unlock-success', { replace: true });
} else if (pendingRedirectUrl) {
// If there's a pending redirect URL in storage, use it (most reliable)
setIsInitialLoading(false);
navigate(pendingRedirectUrl, { replace: true });
} else {
await restoreLastPage();
}
@@ -138,7 +152,9 @@ const Reinitialize: React.FC = () => {
setIsInitialLoading(false);
restoreLastPage();
}
}
};
handleInitialization();
}, [isFullyInitialized, requiresAuth, isLoggedIn, dbAvailable, navigate, setIsInitialLoading, syncVault, restoreLastPage]);
// This component doesn't render anything visible - it just handles initialization

View File

@@ -172,76 +172,105 @@ const AuthSettings: React.FC = () => {
};
return (
<div className="p-4">
{/* Language Settings Section */}
<div className="mb-6">
<div className="flex flex-col gap-2">
<p className="font-medium text-gray-900 dark:text-white">{t('common.language')}</p>
<LanguageSwitcher variant="dropdown" size="sm" />
<div className="p-4 space-y-6">
{/* Server Configuration Section */}
<div className="space-y-4 pb-6 border-b border-gray-200 dark:border-gray-700">
<div>
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-1">
{t('settings.serverConfiguration', 'Server Configuration')}
</h2>
<p className="text-sm text-gray-500 dark:text-gray-400">
{t('settings.serverConfigurationDescription', 'Configure the AliasVault server URL for self-hosted instances')}
</p>
</div>
<div>
<label htmlFor="api-connection" className="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-2">
{t('settings.serverUrl')}
</label>
<select
id="api-connection"
value={selectedOption}
onChange={handleOptionChange}
className="w-full bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white"
>
{DEFAULT_OPTIONS.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
{selectedOption === 'custom' && (
<div className="space-y-4 pl-4 border-l-2 border-primary-500">
<div>
<label htmlFor="custom-api-url" className="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-2">
{t('settings.customApiUrl', 'API URL')}
</label>
<input
id="custom-api-url"
type="text"
value={customUrl}
onChange={handleCustomUrlChange}
placeholder="https://vault.example.com/api"
className={`w-full bg-gray-50 border ${errors.apiUrl ? 'border-red-500' : 'border-gray-300'} text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white`}
/>
{errors.apiUrl && (
<p className="mt-1 text-sm text-red-500">{errors.apiUrl}</p>
)}
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{t('settings.apiUrlHint', 'The API endpoint URL (usually client URL + /api)')}
</p>
</div>
<div>
<label htmlFor="custom-client-url" className="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-2">
{t('settings.customClientUrl', 'Client URL')}
</label>
<input
id="custom-client-url"
type="text"
value={customClientUrl}
onChange={handleCustomClientUrlChange}
placeholder="https://vault.example.com"
className={`w-full bg-gray-50 border ${errors.clientUrl ? 'border-red-500' : 'border-gray-300'} text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white`}
/>
{errors.clientUrl && (
<p className="mt-1 text-sm text-red-500">{errors.clientUrl}</p>
)}
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{t('settings.clientUrlHint', 'The web interface URL of your self-hosted instance')}
</p>
</div>
</div>
)}
</div>
<div className="mb-6">
<label htmlFor="api-connection" className="block font-medium text-gray-700 dark:text-gray-200 mb-2">
{t('settings.serverUrl')}
</label>
<select
value={selectedOption}
onChange={handleOptionChange}
className="w-full bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white"
>
{DEFAULT_OPTIONS.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
{/* Autofill Settings Section */}
<div className="space-y-4 pb-6 border-b border-gray-200 dark:border-gray-700">
<div>
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-1">
{t('settings.autofillSettings', 'Autofill Settings')}
</h2>
<p className="text-sm text-gray-500 dark:text-gray-400">
{t('settings.autofillSettingsDescription', 'Enable or disable the autofill popup on web pages')}
</p>
</div>
{selectedOption === 'custom' && (
<>
<div className="mb-6">
<label htmlFor="custom-client-url" className="block font-medium text-gray-700 dark:text-gray-200 mb-2">
Custom client URL
</label>
<input
id="custom-client-url"
type="text"
value={customClientUrl}
onChange={handleCustomClientUrlChange}
placeholder="https://my-aliasvault-instance.com"
className={`w-full bg-gray-50 border ${errors.clientUrl ? 'border-red-500' : 'border-gray-300'} text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white`}
/>
{errors.clientUrl && (
<p className="mt-1 text-sm text-red-500">{errors.clientUrl}</p>
)}
<div className="flex items-center justify-between gap-4">
<div>
<p className="text-sm font-medium text-gray-900 dark:text-white">{t('settings.autofillEnabled')}</p>
<p className="text-xs text-gray-500 dark:text-gray-400">
{isGloballyEnabled
? t('settings.autofillEnabledDescription', 'Autofill suggestions will appear on login forms')
: t('settings.autofillDisabledDescription', 'Autofill suggestions are disabled globally')
}
</p>
</div>
<div className="mb-6">
<label htmlFor="custom-api-url" className="block font-medium text-gray-700 dark:text-gray-200 mb-2">
Custom API URL
</label>
<input
id="custom-api-url"
type="text"
value={customUrl}
onChange={handleCustomUrlChange}
placeholder="https://my-aliasvault-instance.com/api"
className={`w-full bg-gray-50 border ${errors.apiUrl ? 'border-red-500' : 'border-gray-300'} text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white`}
/>
{errors.apiUrl && (
<p className="mt-1 text-sm text-red-500">{errors.apiUrl}</p>
)}
</div>
</>
)}
{/* Autofill Popup Settings Section */}
<div className="mb-6">
<div className="flex flex-col gap-2">
<p className="font-medium text-gray-900 dark:text-white">{t('settings.autofillEnabled')}</p>
<button
onClick={toggleGlobalPopup}
className={`px-4 py-2 rounded-md transition-colors ${
className={`px-4 py-2 rounded-md transition-colors font-medium text-sm ${
isGloballyEnabled
? 'bg-green-200 text-green-800 hover:bg-green-300 dark:bg-green-900/30 dark:text-green-400 dark:hover:bg-green-900/50'
: 'bg-red-200 text-red-800 hover:bg-red-300 dark:bg-red-900/30 dark:text-red-400 dark:hover:bg-red-900/50'
@@ -252,7 +281,21 @@ const AuthSettings: React.FC = () => {
</div>
</div>
<div className="text-center text-gray-400 dark:text-gray-600">
{/* Language Settings Section */}
<div className="space-y-4 pb-6">
<div>
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-1">
{t('settings.languageSettings', 'Language')}
</h2>
</div>
<div>
<LanguageSwitcher variant="dropdown" size="sm" />
</div>
</div>
{/* Version Info */}
<div className="text-center text-xs text-gray-400 dark:text-gray-600 pt-4 border-t border-gray-200 dark:border-gray-700">
{t('settings.version')}: {AppInfo.VERSION}
</div>
</div>

View File

@@ -8,7 +8,7 @@ import Button from '@/entrypoints/popup/components/Button';
import HeaderButton from '@/entrypoints/popup/components/HeaderButton';
import { HeaderIcon, HeaderIconType } from '@/entrypoints/popup/components/Icons/HeaderIcons';
import LoginServerInfo from '@/entrypoints/popup/components/LoginServerInfo';
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
import { useApp } from '@/entrypoints/popup/context/AppContext';
import { useDb } from '@/entrypoints/popup/context/DbContext';
import { useHeaderButtons } from '@/entrypoints/popup/context/HeaderButtonsContext';
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
@@ -30,7 +30,7 @@ import { storage } from '#imports';
const Login: React.FC = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const authContext = useAuth();
const app = useApp();
const dbContext = useDb();
const { setHeaderButtons } = useHeaderButtons();
const [credentials, setCredentials] = useState({
@@ -65,15 +65,8 @@ const Login: React.FC = () => {
'Authorization': `Bearer ${token}`
} });
const vaultError = webApi.validateVaultResponse(vaultResponseJson, t);
if (vaultError) {
setError(vaultError);
hideLoading();
return;
}
// All is good. Store auth info which is required to make requests to the web API.
await authContext.setAuthTokens(username, token, refreshToken);
await app.setAuthTokens(username, token, refreshToken);
// Store the encryption key and derivation params separately
await dbContext.storeEncryptionKey(passwordHashBase64);
@@ -86,9 +79,6 @@ const Login: React.FC = () => {
// Initialize the SQLite context with the new vault data.
const sqliteClient = await dbContext.initializeDatabase(vaultResponseJson, passwordHashBase64);
// Set logged in status to true which refreshes the app.
await authContext.login();
// If there are pending migrations, redirect to the upgrade page.
try {
if (await sqliteClient.hasPendingMigrations()) {
@@ -97,7 +87,7 @@ const Login: React.FC = () => {
return;
}
} catch (err) {
await authContext.logout();
await app.logout();
setError(err instanceof Error ? err.message : t('auth.errors.migrationError'));
hideLoading();
return;
@@ -157,7 +147,7 @@ const Login: React.FC = () => {
showLoading();
// Clear global message if set with every login attempt.
authContext.clearGlobalMessage();
app.clearGlobalMessage();
// Use the srpUtil instance instead of the imported singleton
const loginResponse = await srpUtil.initiateLogin(ConversionUtility.normalizeUsername(credentials.username));
@@ -233,7 +223,7 @@ const Login: React.FC = () => {
showLoading();
if (!passwordHashString || !passwordHashBase64 || !loginResponse) {
throw new Error(t('auth.errors.loginDataMissing'));
throw new Error(t('common.errors.unknownError'));
}
// Validate that 2FA code is a 6-digit number

View File

@@ -1,33 +0,0 @@
import React, { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
import { useWebApi } from '@/entrypoints/popup/context/WebApiContext';
/**
* Logout page.
*/
const Logout: React.FC = () => {
const authContext = useAuth();
const webApi = useWebApi();
const navigate = useNavigate();
/**
* Logout and navigate to home page.
*/
useEffect(() => {
/**
* Perform logout via async method to ensure logout is completed before navigating to home page.
*/
const performLogout = async () : Promise<void> => {
await webApi.logout();
navigate('/login');
};
performLogout();
}, [authContext, navigate, webApi]);
// Return null since this is just a functional component that handles logout.
return null;
};
export default Logout;

View File

@@ -7,6 +7,7 @@ import { useNavigate } from 'react-router-dom';
import Button from '@/entrypoints/popup/components/Button';
import HeaderButton from '@/entrypoints/popup/components/HeaderButton';
import { HeaderIcon, HeaderIconType } from '@/entrypoints/popup/components/Icons/HeaderIcons';
import { useApp } from '@/entrypoints/popup/context/AppContext';
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
import { useDb } from '@/entrypoints/popup/context/DbContext';
import { useHeaderButtons } from '@/entrypoints/popup/context/HeaderButtonsContext';
@@ -18,6 +19,7 @@ import SrpUtility from '@/entrypoints/popup/utils/SrpUtility';
import { VAULT_LOCKED_DISMISS_UNTIL_KEY } from '@/utils/Constants';
import type { VaultResponse } from '@/utils/dist/shared/models/webapi';
import EncryptionUtility from '@/utils/EncryptionUtility';
import { VaultVersionIncompatibleError } from '@/utils/types/errors/VaultVersionIncompatibleError';
import { storage } from '#imports';
@@ -26,6 +28,7 @@ import { storage } from '#imports';
*/
const Unlock: React.FC = () => {
const { t } = useTranslation();
const app = useApp();
const authContext = useAuth();
const dbContext = useDb();
const navigate = useNavigate();
@@ -39,22 +42,32 @@ const Unlock: React.FC = () => {
const [error, setError] = useState<string | null>(null);
const { showLoading, hideLoading, setIsInitialLoading } = useLoading();
useEffect(() => {
/**
* Make status call to API which acts as health check.
*/
const checkStatus = async () : Promise<void> => {
const statusResponse = await webApi.getStatus();
const statusError = webApi.validateStatusResponse(statusResponse);
if (statusError !== null) {
await webApi.logout(t('common.errors.' + statusError));
navigate('/logout');
}
setIsInitialLoading(false);
};
/**
* Make status call to API which acts as health check.
* This runs only once during component mount.
*/
const checkStatus = async () : Promise<boolean> => {
const statusResponse = await webApi.getStatus();
const statusError = webApi.validateStatusResponse(statusResponse);
if (statusResponse.serverVersion === '0.0.0') {
setError(t('common.errors.serverNotAvailable'));
return false;
}
if (statusError !== null) {
await app.logout(t('common.errors.' + statusError));
return false;
}
setIsInitialLoading(false);
return true;
};
useEffect(() => {
checkStatus();
}, [webApi, authContext, setIsInitialLoading, navigate, t]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // Only run once on mount
// Set header buttons on mount and clear on unmount
useEffect((): (() => void) => {
@@ -81,6 +94,12 @@ const Unlock: React.FC = () => {
setError(null);
showLoading();
const isStatusOk = await checkStatus();
if (!isStatusOk) {
hideLoading();
return;
}
try {
// 1. Initiate login to get salt and server ephemeral
const loginResponse = await srpUtil.initiateLogin(authContext.username!);
@@ -96,13 +115,6 @@ const Unlock: React.FC = () => {
// Make API call to get latest vault
const vaultResponseJson = await webApi.get<VaultResponse>('Vault');
const vaultError = webApi.validateVaultResponse(vaultResponseJson, t);
if (vaultError) {
setError(t('common.apiErrors.' + vaultError));
hideLoading();
return;
}
// Get the derived key as base64 string required for decryption.
const passwordHashBase64 = Buffer.from(passwordHash).toString('base64');
@@ -110,15 +122,26 @@ const Unlock: React.FC = () => {
await dbContext.storeEncryptionKey(passwordHashBase64);
// Initialize the SQLite context with the new vault data.
await dbContext.initializeDatabase(vaultResponseJson, passwordHashBase64);
const sqliteClient = await dbContext.initializeDatabase(vaultResponseJson, passwordHashBase64);
// Check if there are pending migrations
if (await sqliteClient.hasPendingMigrations()) {
navigate('/upgrade', { replace: true });
hideLoading();
return;
}
// Clear dismiss until (which can be enabled after user has dimissed vault is locked popup) to ensure popup is shown.
await storage.setItem(VAULT_LOCKED_DISMISS_UNTIL_KEY, 0);
// Redirect to reinitialize page
navigate('/reinitialize', { replace: true });
} catch (err) {
setError(t('auth.errors.wrongPassword'));
// Check if it's a version incompatibility error
if (err instanceof VaultVersionIncompatibleError) {
await app.logout(err.message);
} else {
setError(t('auth.errors.wrongPassword'));
}
console.error('Unlock error:', err);
} finally {
hideLoading();
@@ -129,7 +152,7 @@ const Unlock: React.FC = () => {
* Handle logout
*/
const handleLogout = () : void => {
navigate('/logout', { replace: true });
app.logout();
};
return (

View File

@@ -3,11 +3,11 @@ import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import Button from '@/entrypoints/popup/components/Button';
import Modal from '@/entrypoints/popup/components/Dialogs/Modal';
import HeaderButton from '@/entrypoints/popup/components/HeaderButton';
import { HeaderIconType } from '@/entrypoints/popup/components/Icons/HeaderIcons';
import LoadingSpinner from '@/entrypoints/popup/components/LoadingSpinner';
import Modal from '@/entrypoints/popup/components/Modal';
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
import { useApp } from '@/entrypoints/popup/context/AppContext';
import { useDb } from '@/entrypoints/popup/context/DbContext';
import { useHeaderButtons } from '@/entrypoints/popup/context/HeaderButtonsContext';
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
@@ -24,7 +24,7 @@ import { VaultSqlGenerator } from '@/utils/dist/shared/vault-sql';
*/
const Upgrade: React.FC = () => {
const { t } = useTranslation();
const { username } = useAuth();
const { username, logout } = useApp();
const dbContext = useDb();
const { sqliteClient } = dbContext;
const { setHeaderButtons } = useHeaderButtons();
@@ -65,7 +65,7 @@ const Upgrade: React.FC = () => {
const loadVersionInfo = useCallback(async () => {
try {
if (sqliteClient) {
const current = sqliteClient.getDatabaseVersion();
const current = await sqliteClient.getDatabaseVersion();
const latest = await sqliteClient.getLatestDatabaseVersion();
setCurrentVersion(current);
setLatestVersion(latest);
@@ -165,7 +165,7 @@ const Upgrade: React.FC = () => {
console.debug('executeVaultMutation done?');
} catch (error) {
console.error('Upgrade failed:', error);
setError(error instanceof Error ? error.message : t('upgrade.alerts.unknownErrorDuringUpgrade'));
setError(error instanceof Error ? error.message : t('common.errors.unknownError'));
} finally {
setIsLoading(false);
}
@@ -206,7 +206,7 @@ const Upgrade: React.FC = () => {
* Handle the logout.
*/
const handleLogout = async (): Promise<void> => {
navigate('/logout');
logout();
};
/**
@@ -296,7 +296,7 @@ const Upgrade: React.FC = () => {
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600 dark:text-gray-400">{t('upgrade.yourVault')}</span>
<span className="text-sm font-bold text-orange-600 dark:text-orange-400">
{currentVersion?.releaseVersion ?? '...'}
{currentVersion?.compatibleUpToVersion ?? '...'}
</span>
</div>
<div className="flex justify-between items-center">
@@ -312,6 +312,7 @@ const Upgrade: React.FC = () => {
<div className="flex flex-col w-full space-y-2">
<Button
type="button"
id="upgrade-button"
onClick={handleUpgrade}
>
{isLoading || isVaultMutationLoading ? (syncStatus || t('upgrade.upgrading')) : t('upgrade.upgrade')}

View File

@@ -8,15 +8,15 @@ import { useNavigate, useParams } from 'react-router-dom';
import { sendMessage } from 'webext-bridge/popup';
import * as Yup from 'yup';
import AttachmentUploader from '@/entrypoints/popup/components/CredentialDetails/AttachmentUploader';
import EmailDomainField from '@/entrypoints/popup/components/EmailDomainField';
import { FormInput } from '@/entrypoints/popup/components/FormInput';
import AttachmentUploader from '@/entrypoints/popup/components/Credentials/Details/AttachmentUploader';
import Modal from '@/entrypoints/popup/components/Dialogs/Modal';
import EmailDomainField from '@/entrypoints/popup/components/Forms/EmailDomainField';
import { FormInput } from '@/entrypoints/popup/components/Forms/FormInput';
import PasswordField from '@/entrypoints/popup/components/Forms/PasswordField';
import UsernameField from '@/entrypoints/popup/components/Forms/UsernameField';
import HeaderButton from '@/entrypoints/popup/components/HeaderButton';
import { HeaderIconType } from '@/entrypoints/popup/components/Icons/HeaderIcons';
import LoadingSpinner from '@/entrypoints/popup/components/LoadingSpinner';
import Modal from '@/entrypoints/popup/components/Modal';
import PasswordField from '@/entrypoints/popup/components/PasswordField';
import UsernameField from '@/entrypoints/popup/components/UsernameField';
import { useDb } from '@/entrypoints/popup/context/DbContext';
import { useHeaderButtons } from '@/entrypoints/popup/context/HeaderButtonsContext';
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
@@ -92,6 +92,7 @@ const CredentialAddEdit: React.FC = () => {
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [attachments, setAttachments] = useState<Attachment[]>([]);
const [originalAttachmentIds, setOriginalAttachmentIds] = useState<string[]>([]);
const [passkeyMarkedForDeletion, setPasskeyMarkedForDeletion] = useState(false);
const webApi = useWebApi();
// Track last generated values to avoid overwriting manual entries
@@ -550,6 +551,11 @@ const CredentialAddEdit: React.FC = () => {
if (isEditMode) {
await dbContext.sqliteClient!.updateCredentialById(data, originalAttachmentIds, attachments);
// Delete passkeys if marked for deletion
if (passkeyMarkedForDeletion) {
await dbContext.sqliteClient!.deletePasskeysByCredentialId(data.Id);
}
} else {
const credentialId = await dbContext.sqliteClient!.createCredential(data, attachments);
data.Id = credentialId.toString();
@@ -570,7 +576,7 @@ const CredentialAddEdit: React.FC = () => {
}
},
});
}, [isEditMode, dbContext.sqliteClient, executeVaultMutation, navigate, mode, watch, generateRandomAlias, webApi, clearPersistedValues, originalAttachmentIds, attachments]);
}, [isEditMode, dbContext.sqliteClient, executeVaultMutation, navigate, mode, watch, generateRandomAlias, webApi, clearPersistedValues, originalAttachmentIds, attachments, passkeyMarkedForDeletion]);
// Set header buttons on mount and clear on unmount
useEffect((): (() => void) => {
@@ -695,30 +701,167 @@ const CredentialAddEdit: React.FC = () => {
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg border border-gray-200 dark:border-gray-700">
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">{t('credentials.loginCredentials')}</h2>
<div className="space-y-4">
<EmailDomainField
id="email"
label={t('common.email')}
value={watch('Alias.Email') ?? ''}
onChange={(value: string) => setValue('Alias.Email', value)}
error={errors.Alias?.Email?.message}
/>
<UsernameField
id="username"
label={t('common.username')}
value={watch('Username') ?? ''}
onChange={(value) => setValue('Username', value)}
error={errors.Username?.message}
onRegenerate={generateRandomUsername}
/>
<PasswordField
id="password"
label={t('common.password')}
value={watch('Password') ?? ''}
onChange={(value) => setValue('Password', value)}
error={errors.Password?.message}
showPassword={showPassword}
onShowPasswordChange={setShowPassword}
/>
{watch('HasPasskey') ? (
<>
{/* When passkey exists: username, passkey, email, password */}
<UsernameField
id="username"
label={t('common.username')}
value={watch('Username') ?? ''}
onChange={(value) => setValue('Username', value)}
error={errors.Username?.message}
onRegenerate={generateRandomUsername}
/>
{!passkeyMarkedForDeletion && (
<div className="p-3 rounded bg-gray-50 dark:bg-gray-900 border border-gray-200 dark:border-gray-700">
<div className="flex items-start gap-2">
<svg
className="w-5 h-5 text-gray-600 dark:text-gray-400 mt-0.5 flex-shrink-0"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4" />
</svg>
<div className="flex-1">
<div className="mb-1 flex items-center justify-between">
<span className="text-sm font-medium text-gray-900 dark:text-white">{t('passkeys.passkey')}</span>
<button
type="button"
onClick={() => setPasskeyMarkedForDeletion(true)}
className="text-red-600 dark:text-red-400 hover:text-red-700 dark:hover:text-red-300"
title="Delete passkey"
>
<svg
className="w-4 h-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<polyline points="3 6 5 6 21 6" />
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
<line x1="10" y1="11" x2="10" y2="17" />
<line x1="14" y1="11" x2="14" y2="17" />
</svg>
</button>
</div>
<div className="space-y-1 mb-2">
{watch('PasskeyRpId') && (
<div>
<span className="text-xs text-gray-500 dark:text-gray-400">{t('passkeys.site')}: </span>
<span className="text-sm text-gray-900 dark:text-white">{watch('PasskeyRpId')}</span>
</div>
)}
{watch('PasskeyDisplayName') && (
<div>
<span className="text-xs text-gray-500 dark:text-gray-400">{t('passkeys.displayName')}: </span>
<span className="text-sm text-gray-900 dark:text-white">{watch('PasskeyDisplayName')}</span>
</div>
)}
</div>
<p className="text-xs text-gray-600 dark:text-gray-400">
{t('passkeys.helpText')}
</p>
</div>
</div>
</div>
)}
{passkeyMarkedForDeletion && (
<div className="p-3 rounded bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800">
<div className="flex items-start gap-2">
<svg
className="w-5 h-5 text-red-600 dark:text-red-400 mt-0.5 flex-shrink-0"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4" />
</svg>
<div className="flex-1">
<div className="mb-1 flex items-center justify-between">
<span className="text-sm font-medium text-red-900 dark:text-red-100">{t('passkeys.passkeyMarkedForDeletion')}</span>
<button
type="button"
onClick={() => setPasskeyMarkedForDeletion(false)}
className="text-gray-600 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300"
title="Undo"
>
<svg
className="w-4 h-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M3 7v6h6" />
<path d="M21 17a9 9 0 00-9-9 9 9 0 00-6 2.3L3 13" />
</svg>
</button>
</div>
<p className="text-xs text-red-800 dark:text-red-200">
{t('passkeys.passkeyWillBeDeleted')}
</p>
</div>
</div>
</div>
)}
<EmailDomainField
id="email"
label={t('common.email')}
value={watch('Alias.Email') ?? ''}
onChange={(value: string) => setValue('Alias.Email', value)}
error={errors.Alias?.Email?.message}
/>
<PasswordField
id="password"
label={t('common.password')}
value={watch('Password') ?? ''}
onChange={(value) => setValue('Password', value)}
error={errors.Password?.message}
showPassword={showPassword}
onShowPasswordChange={setShowPassword}
/>
</>
) : (
<>
{/* When no passkey: email, username, password */}
<EmailDomainField
id="email"
label={t('common.email')}
value={watch('Alias.Email') ?? ''}
onChange={(value: string) => setValue('Alias.Email', value)}
error={errors.Alias?.Email?.message}
/>
<UsernameField
id="username"
label={t('common.username')}
value={watch('Username') ?? ''}
onChange={(value) => setValue('Username', value)}
error={errors.Username?.message}
onRegenerate={generateRandomUsername}
/>
<PasswordField
id="password"
label={t('common.password')}
value={watch('Password') ?? ''}
onChange={(value) => setValue('Password', value)}
error={errors.Password?.message}
showPassword={showPassword}
onShowPasswordChange={setShowPassword}
/>
</>
)}
</div>
</div>

View File

@@ -10,7 +10,7 @@ import {
AliasBlock,
NotesBlock,
AttachmentBlock
} from '@/entrypoints/popup/components/CredentialDetails';
} from '@/entrypoints/popup/components/Credentials/Details';
import HeaderButton from '@/entrypoints/popup/components/HeaderButton';
import { HeaderIconType } from '@/entrypoints/popup/components/Icons/HeaderIcons';
import { useDb } from '@/entrypoints/popup/context/DbContext';

View File

@@ -2,15 +2,15 @@ import React, { useState, useEffect, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import CredentialCard from '@/entrypoints/popup/components/CredentialCard';
import CredentialCard from '@/entrypoints/popup/components/Credentials/CredentialCard';
import HeaderButton from '@/entrypoints/popup/components/HeaderButton';
import { HeaderIconType } from '@/entrypoints/popup/components/Icons/HeaderIcons';
import LoadingSpinner from '@/entrypoints/popup/components/LoadingSpinner';
import ReloadButton from '@/entrypoints/popup/components/ReloadButton';
import { useApp } from '@/entrypoints/popup/context/AppContext';
import { useDb } from '@/entrypoints/popup/context/DbContext';
import { useHeaderButtons } from '@/entrypoints/popup/context/HeaderButtonsContext';
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
import { useWebApi } from '@/entrypoints/popup/context/WebApiContext';
import { useVaultSync } from '@/entrypoints/popup/hooks/useVaultSync';
import { PopoutUtility } from '@/entrypoints/popup/utils/PopoutUtility';
@@ -18,18 +18,64 @@ import type { Credential } from '@/utils/dist/shared/models/vault';
import { useMinDurationLoading } from '@/hooks/useMinDurationLoading';
type FilterType = 'all' | 'passkeys' | 'aliases' | 'userpass' | 'attachments';
const FILTER_STORAGE_KEY = 'credentials-filter';
const FILTER_EXPIRY_MS = 5 * 60 * 1000; // 5 minutes
/**
* Get stored filter from localStorage if not expired
*/
const getStoredFilter = (): FilterType => {
try {
const stored = localStorage.getItem(FILTER_STORAGE_KEY);
if (!stored) {
return 'all';
}
const { filter, timestamp } = JSON.parse(stored);
const now = Date.now();
// Check if expired (5 minutes)
if (now - timestamp > FILTER_EXPIRY_MS) {
localStorage.removeItem(FILTER_STORAGE_KEY);
return 'all';
}
return filter as FilterType;
} catch {
return 'all';
}
};
/**
* Store filter in localStorage with timestamp
*/
const storeFilter = (filter: FilterType): void => {
try {
localStorage.setItem(FILTER_STORAGE_KEY, JSON.stringify({
filter,
timestamp: Date.now()
}));
} catch {
// Ignore storage errors
}
};
/**
* Credentials list page.
*/
const CredentialsList: React.FC = () => {
const { t } = useTranslation();
const dbContext = useDb();
const webApi = useWebApi();
const app = useApp();
const navigate = useNavigate();
const { syncVault } = useVaultSync();
const { setHeaderButtons } = useHeaderButtons();
const [credentials, setCredentials] = useState<Credential[]>([]);
const [searchTerm, setSearchTerm] = useState('');
const [filterType, setFilterType] = useState<FilterType>(getStoredFilter());
const [showFilterMenu, setShowFilterMenu] = useState(false);
const { setIsInitialLoading } = useLoading();
/**
@@ -72,16 +118,13 @@ const CredentialsList: React.FC = () => {
*/
onError: async (error) => {
console.error('Error syncing vault:', error);
await webApi.logout('Error while syncing vault, please re-authenticate.');
navigate('/logout');
},
});
} catch (err) {
console.error('Error refreshing credentials:', err);
await webApi.logout('Error while syncing vault, please re-authenticate.');
navigate('/logout');
await app.logout('Error while syncing vault, please re-authenticate.');
}
}, [dbContext, webApi, syncVault, navigate]);
}, [dbContext, app, syncVault]);
/**
* Get latest vault from server and refresh the credentials list.
@@ -135,8 +178,67 @@ const CredentialsList: React.FC = () => {
refreshCredentials();
}, [dbContext?.sqliteClient, setIsLoading, setIsInitialLoading]);
const filteredCredentials = credentials.filter(credential => {
const searchLower = searchTerm.toLowerCase();
/**
* Get the title based on the active filter
*/
const getFilterTitle = () : string => {
switch (filterType) {
case 'passkeys':
return t('credentials.filters.passkeys');
case 'aliases':
return t('credentials.filters.aliases');
case 'userpass':
return t('credentials.filters.userpass');
case 'attachments':
return t('credentials.filters.attachments');
default:
return t('credentials.title');
}
};
const filteredCredentials = credentials.filter((credential: Credential) => {
// First apply type filter
let passesTypeFilter = true;
if (filterType === 'passkeys') {
passesTypeFilter = credential.HasPasskey === true;
} else if (filterType === 'aliases') {
// Check for non-empty alias fields (excluding email which is used everywhere)
passesTypeFilter = !!(
(credential.Alias?.FirstName && credential.Alias.FirstName.trim()) ||
(credential.Alias?.LastName && credential.Alias.LastName.trim()) ||
(credential.Alias?.NickName && credential.Alias.NickName.trim()) ||
(credential.Alias?.Gender && credential.Alias.Gender.trim()) ||
(credential.Alias?.BirthDate && credential.Alias.BirthDate.trim() && credential.Alias.BirthDate.trim().startsWith('0001-01-01') !== true)
);
} else if (filterType === 'userpass') {
// Show only credentials that have username/password AND do NOT have alias fields AND do NOT have passkey
const hasAliasFields = !!(
(credential.Alias?.FirstName && credential.Alias.FirstName.trim()) ||
(credential.Alias?.LastName && credential.Alias.LastName.trim()) ||
(credential.Alias?.NickName && credential.Alias.NickName.trim()) ||
(credential.Alias?.Gender && credential.Alias.Gender.trim()) ||
(credential.Alias?.BirthDate && credential.Alias.BirthDate.trim() && credential.Alias.BirthDate.trim().startsWith('0001-01-01') !== true)
);
const hasUsernameOrPassword = !!(
(credential.Username && credential.Username.trim()) ||
(credential.Password && credential.Password.trim())
);
passesTypeFilter = hasUsernameOrPassword && !credential.HasPasskey && !hasAliasFields;
} else if (filterType === 'attachments') {
passesTypeFilter = credential.HasAttachment === true;
}
if (!passesTypeFilter) {
return false;
}
// Then apply search filter
const searchLower = searchTerm.toLowerCase().trim();
if (!searchLower) {
return true; // No search term, include all
}
/**
* We filter credentials by searching in the following fields:
@@ -147,13 +249,20 @@ const CredentialsList: React.FC = () => {
* - Notes
*/
const searchableFields = [
credential.ServiceName?.toLowerCase(),
credential.Username?.toLowerCase(),
credential.Alias?.Email?.toLowerCase(),
credential.ServiceUrl?.toLowerCase(),
credential.Notes?.toLowerCase(),
credential.ServiceName?.toLowerCase() || '',
credential.Username?.toLowerCase() || '',
credential.Alias?.Email?.toLowerCase() || '',
credential.ServiceUrl?.toLowerCase() || '',
credential.Notes?.toLowerCase() || '',
];
return searchableFields.some(field => field?.includes(searchLower));
// Split search term into words for AND search
const searchWords = searchLower.split(/\s+/).filter(word => word.length > 0);
// All search words must be found (each in at least one field)
return searchWords.every(word =>
searchableFields.some(field => field.includes(word))
);
});
if (isLoading) {
@@ -167,7 +276,106 @@ const CredentialsList: React.FC = () => {
return (
<div>
<div className="flex justify-between items-center mb-4">
<h2 className="text-gray-900 dark:text-white text-xl">{t('credentials.title')}</h2>
<div className="relative">
<button
onClick={() => setShowFilterMenu(!showFilterMenu)}
className="flex items-center gap-1 text-gray-900 dark:text-white text-xl hover:text-gray-700 dark:hover:text-gray-300 focus:outline-none"
>
<h2 className="flex items-baseline gap-1.5">
{getFilterTitle()}
<span className="text-sm text-gray-500 dark:text-gray-400">({filteredCredentials.length})</span>
</h2>
<svg
className="w-4 h-4 mt-1"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<polyline points="6 9 12 15 18 9" />
</svg>
</button>
{showFilterMenu && (
<>
<div
className="fixed inset-0 z-10"
onClick={() => setShowFilterMenu(false)}
/>
<div className="absolute left-0 mt-2 w-56 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg z-20">
<div className="py-1">
<button
onClick={() => {
const newFilter = 'all';
setFilterType(newFilter);
storeFilter(newFilter);
setShowFilterMenu(false);
}}
className={`w-full text-left px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 ${
filterType === 'all' ? 'bg-orange-50 dark:bg-orange-900/20 text-orange-600 dark:text-orange-400' : 'text-gray-700 dark:text-gray-300'
}`}
>
{t('credentials.filters.all')}
</button>
<button
onClick={() => {
const newFilter = 'passkeys';
setFilterType(newFilter);
storeFilter(newFilter);
setShowFilterMenu(false);
}}
className={`w-full text-left px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 ${
filterType === 'passkeys' ? 'bg-orange-50 dark:bg-orange-900/20 text-orange-600 dark:text-orange-400' : 'text-gray-700 dark:text-gray-300'
}`}
>
{t('credentials.filters.passkeys')}
</button>
<button
onClick={() => {
const newFilter = 'aliases';
setFilterType(newFilter);
storeFilter(newFilter);
setShowFilterMenu(false);
}}
className={`w-full text-left px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 ${
filterType === 'aliases' ? 'bg-orange-50 dark:bg-orange-900/20 text-orange-600 dark:text-orange-400' : 'text-gray-700 dark:text-gray-300'
}`}
>
{t('credentials.filters.aliases')}
</button>
<button
onClick={() => {
const newFilter = 'userpass';
setFilterType(newFilter);
storeFilter(newFilter);
setShowFilterMenu(false);
}}
className={`w-full text-left px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 ${
filterType === 'userpass' ? 'bg-orange-50 dark:bg-orange-900/20 text-orange-600 dark:text-orange-400' : 'text-gray-700 dark:text-gray-300'
}`}
>
{t('credentials.filters.userpass')}
</button>
<button
onClick={() => {
const newFilter = 'attachments';
setFilterType(newFilter);
storeFilter(newFilter);
setShowFilterMenu(false);
}}
className={`w-full text-left px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 ${
filterType === 'attachments' ? 'bg-orange-50 dark:bg-orange-900/20 text-orange-600 dark:text-orange-400' : 'text-gray-700 dark:text-gray-300'
}`}
>
{t('credentials.filters.attachments')}
</button>
</div>
</div>
</>
)}
</div>
<ReloadButton onClick={syncVaultAndRefresh} />
</div>
@@ -195,6 +403,17 @@ const CredentialsList: React.FC = () => {
{t('credentials.welcomeDescription')}
</p>
</div>
) : filteredCredentials.length === 0 ? (
<div className="text-gray-500 dark:text-gray-400 space-y-2 mb-10">
<p>
{filterType === 'passkeys'
? t('credentials.noPasskeysFound')
: filterType === 'attachments'
? t('credentials.noAttachmentsFound')
: t('credentials.noMatchingCredentials')
}
</p>
</div>
) : (
<ul className="space-y-2">
{filteredCredentials.map(cred => (

View File

@@ -2,8 +2,8 @@ import React, { useEffect, useState, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams, useNavigate } from 'react-router-dom';
import Modal from '@/entrypoints/popup/components/Dialogs/Modal';
import LoadingSpinner from '@/entrypoints/popup/components/LoadingSpinner';
import Modal from '@/entrypoints/popup/components/Modal';
import { useDb } from '@/entrypoints/popup/context/DbContext';
import { useHeaderButtons } from '@/entrypoints/popup/context/HeaderButtonsContext';
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';

View File

@@ -0,0 +1,451 @@
import React, { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useLocation } from 'react-router-dom';
import { sendMessage } from 'webext-bridge/popup';
import { extractDomain, extractRootDomain } from '@/entrypoints/contentScript/Filter';
import Button from '@/entrypoints/popup/components/Button';
import PasskeyBypassDialog from '@/entrypoints/popup/components/Dialogs/PasskeyBypassDialog';
import LoadingSpinner from '@/entrypoints/popup/components/LoadingSpinner';
import { useDb } from '@/entrypoints/popup/context/DbContext';
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
import { useVaultLockRedirect } from '@/entrypoints/popup/hooks/useVaultLockRedirect';
import { PASSKEY_DISABLED_SITES_KEY } from '@/utils/Constants';
import { PasskeyAuthenticator } from '@/utils/passkey/PasskeyAuthenticator';
import { PasskeyHelper } from '@/utils/passkey/PasskeyHelper';
import type { GetRequest, PasskeyGetCredentialResponse, PendingPasskeyGetRequest, StoredPasskeyRecord } from '@/utils/passkey/types';
import { storage } from "#imports";
/**
* PasskeyAuthenticate
*/
const PasskeyAuthenticate: React.FC = () => {
const { t } = useTranslation();
const location = useLocation();
const { setIsInitialLoading } = useLoading();
const dbContext = useDb();
const [request, setRequest] = useState<PendingPasskeyGetRequest | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [availablePasskeys, setAvailablePasskeys] = useState<Array<{ id: string; displayName: string; rpId: string; serviceName?: string | null }>>([]);
const [showBypassDialog, setShowBypassDialog] = useState(false);
const { isLocked } = useVaultLockRedirect();
const firstPasskeyRef = useRef<HTMLDivElement>(null);
useEffect(() => {
/**
* fetchRequestData
*/
const fetchRequestData = async () : Promise<void> => {
// Wait for DB to be initialized
if (!dbContext.dbInitialized) {
return;
}
// If vault is locked, the hook will handle redirect, we just return
if (isLocked) {
return;
}
// Get the requestId from URL
const params = new URLSearchParams(location.search);
const requestId = params.get('requestId');
if (requestId) {
try {
// Fetch the full request data from background
const data = await sendMessage('GET_REQUEST_DATA', { requestId }, 'background') as unknown as PendingPasskeyGetRequest;
if (data && data.type === 'get') {
setRequest(data);
// Get passkeys for this rpId from the vault
const rpId = data.publicKey.rpId || new URL(data.origin).hostname;
const passkeys = dbContext.sqliteClient!.getPasskeysByRpId(rpId);
// Filter by allowCredentials if specified
let filteredPasskeys = passkeys;
if (data.publicKey.allowCredentials && data.publicKey.allowCredentials.length > 0) {
// Convert the RP's base64url credential IDs to GUIDs for comparison
const allowedGuids = new Set(
data.publicKey.allowCredentials.map(c => {
try {
return PasskeyHelper.base64urlToGuid(c.id);
} catch (e) {
console.warn('Failed to convert credential ID to GUID:', c.id, e);
return null;
}
}).filter((id): id is string => id !== null)
);
filteredPasskeys = passkeys.filter(pk => allowedGuids.has(pk.Id));
}
// Map to display format
setAvailablePasskeys(filteredPasskeys.map(pk => ({
id: pk.Id,
displayName: pk.DisplayName,
serviceName: pk.ServiceName,
rpId: pk.RpId,
username: pk.Username
})));
}
} catch (error) {
console.error('Failed to fetch request data:', error);
setError(t('common.errors.unknownError'));
}
}
// Mark initial loading as complete
setIsInitialLoading(false);
};
fetchRequestData();
}, [location, setIsInitialLoading, dbContext.dbInitialized, isLocked, dbContext.sqliteClient, t]);
// Auto-focus first passkey
useEffect(() => {
if (availablePasskeys.length > 0 && firstPasskeyRef.current) {
firstPasskeyRef.current.focus();
}
}, [availablePasskeys.length]);
// Handle Enter key to select first passkey
useEffect(() => {
/**
* Handle Enter key to select first passkey
*/
const handleKeyDown = (e: KeyboardEvent) : void => {
if (e.key === 'Enter' && !loading && availablePasskeys.length > 0) {
handleUsePasskey(availablePasskeys[0].id);
}
};
/**
* Handle Enter key to select first passkey
*/
window.addEventListener('keydown', handleKeyDown);
return () : void => window.removeEventListener('keydown', handleKeyDown);
/**
* Handle Enter key to select first passkey
*/
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [loading, availablePasskeys]);
/**
* Handle passkey authentication
*/
const handleUsePasskey = async (passkeyId: string) : Promise<void> => {
if (!request || !dbContext.sqliteClient) {
return;
}
setLoading(true);
setError(null);
try {
// Get the stored passkey from vault
const storedPasskey = dbContext.sqliteClient.getPasskeyById(passkeyId);
if (!storedPasskey) {
throw new Error(t('common.errors.unknownError'));
}
// Parse the stored keys
const publicKey = JSON.parse(storedPasskey.PublicKey) as JsonWebKey;
const privateKey = JSON.parse(storedPasskey.PrivateKey) as JsonWebKey;
// Extract PRF secret from PrfKey if available
let prfSecret: string | undefined;
if (storedPasskey.PrfKey) {
try {
// Convert PrfKey bytes to base64url string
prfSecret = PasskeyHelper.bytesToBase64url(storedPasskey.PrfKey);
} catch (e) {
console.warn('Failed to convert PrfKey to base64url', e);
}
}
/**
* Build the stored record for the provider
* Convert UserHandle from byte array to base64 string for serialization
*/
let userIdBase64: string | null = null;
if (storedPasskey.UserHandle) {
try {
const userHandleBytes = storedPasskey.UserHandle instanceof Uint8Array ? storedPasskey.UserHandle : new Uint8Array(storedPasskey.UserHandle);
userIdBase64 = PasskeyHelper.bytesToBase64url(userHandleBytes);
} catch (e) {
console.warn('Failed to convert UserHandle to base64', e);
}
}
const storedRecord: StoredPasskeyRecord = {
rpId: storedPasskey.RpId,
credentialId: PasskeyHelper.guidToBase64url(storedPasskey.Id),
publicKey,
privateKey,
userId: userIdBase64,
userName: storedPasskey.Username ?? undefined,
userDisplayName: storedPasskey.ServiceName ?? undefined,
prfSecret
};
// Build the GetRequest
const getRequest: GetRequest = {
origin: request.origin,
requestId: request.requestId,
publicKey: {
rpId: request.publicKey.rpId,
challenge: request.publicKey.challenge,
userVerification: request.publicKey.userVerification
}
};
// Extract PRF inputs if requested
let prfInputs: { first: ArrayBuffer | Uint8Array; second?: ArrayBuffer | Uint8Array } | undefined;
if (request.publicKey.extensions?.prf?.eval) {
// Handle numeric object format (serialized Uint8Array through events)
const firstInput = request.publicKey.extensions.prf.eval.first;
let firstBytes: Uint8Array;
if (typeof firstInput === 'object' && firstInput !== null && !Array.isArray(firstInput)) {
// Numeric object format: {0: 68, 1: 204, ...}
const keys = Object.keys(firstInput).map(Number).sort((a, b) => a - b);
firstBytes = new Uint8Array(keys.length);
for (let i = 0; i < keys.length; i++) {
firstBytes[i] = (firstInput as unknown as Record<string, number>)[i];
}
} else if (typeof firstInput === 'string') {
// Base64 string format
const firstDecoded = atob(firstInput);
firstBytes = new Uint8Array(firstDecoded.length);
for (let i = 0; i < firstDecoded.length; i++) {
firstBytes[i] = firstDecoded.charCodeAt(i);
}
} else {
throw new Error('Unknown PRF input format');
}
prfInputs = { first: firstBytes };
if (request.publicKey.extensions.prf.eval.second) {
const secondInput = request.publicKey.extensions.prf.eval.second;
let secondBytes: Uint8Array;
if (typeof secondInput === 'object' && secondInput !== null && !Array.isArray(secondInput)) {
const keys = Object.keys(secondInput).map(Number).sort((a, b) => a - b);
secondBytes = new Uint8Array(keys.length);
for (let i = 0; i < keys.length; i++) {
secondBytes[i] = (secondInput as unknown as Record<string, number>)[i];
}
} else if (typeof secondInput === 'string') {
const secondDecoded = atob(secondInput);
secondBytes = new Uint8Array(secondDecoded.length);
for (let i = 0; i < secondDecoded.length; i++) {
secondBytes[i] = secondDecoded.charCodeAt(i);
}
} else {
console.error('[PasskeyAuth] Unknown PRF second input type:', typeof secondInput);
throw new Error('Unknown PRF second input format');
}
prfInputs.second = secondBytes;
}
}
// Get the assertion using the static method
const assertion = await PasskeyAuthenticator.getAssertion(getRequest, storedRecord, {
uvPerformed: true, // TODO: implement explicit user verification check
includeBEBS: true, // Backup eligible/state - defaults to true
prfInputs
});
// Convert PRF results to base64 for transport
let prfResults: { first: string; second?: string } | undefined;
if (assertion.prfResults) {
prfResults = {
first: PasskeyHelper.arrayBufferToBase64(assertion.prfResults.first)
};
if (assertion.prfResults.second) {
prfResults.second = PasskeyHelper.arrayBufferToBase64(assertion.prfResults.second);
}
}
const credential: PasskeyGetCredentialResponse = {
id: assertion.id,
rawId: assertion.rawId,
clientDataJSON: assertion.clientDataJSON,
authenticatorData: assertion.authenticatorData,
signature: assertion.signature,
userHandle: assertion.userHandle,
prfResults
};
/*
* Send response back
* The background script will close the window (Safari-compatible)
*/
await sendMessage('PASSKEY_POPUP_RESPONSE', {
requestId: request.requestId,
credential
}, 'background');
} catch (error) {
console.error('PasskeyAuthenticate: Error during authentication', error);
setLoading(false);
setError(t('common.errors.unknownError'));
}
};
/**
* Handle fallback - show bypass dialog first
*/
const handleFallback = async () : Promise<void> => {
setShowBypassDialog(true);
};
/**
* Handle bypass choice
*/
const handleBypassChoice = async (choice: 'once' | 'always') : Promise<void> => {
if (!request) {
return;
}
if (choice === 'always') {
// Add to permanent disabled list
const hostname = new URL(request.origin).hostname;
const baseDomain = extractRootDomain(extractDomain(hostname));
const disabledSites = await storage.getItem(PASSKEY_DISABLED_SITES_KEY) as string[] ?? [];
if (!disabledSites.includes(baseDomain)) {
disabledSites.push(baseDomain);
await storage.setItem(PASSKEY_DISABLED_SITES_KEY, disabledSites);
}
}
// For 'once', we don't store anything - just bypass this one time
/*
* Tell background to use native implementation
* The background script will close the window (Safari-compatible)
*/
await sendMessage('PASSKEY_POPUP_RESPONSE', {
requestId: request.requestId,
fallback: true
}, 'background');
};
/**
* Handle cancel
*/
const handleCancel = async () : Promise<void> => {
if (!request) {
return;
}
/*
* Tell background user cancelled
* The background script will close the window (Safari-compatible)
*/
await sendMessage('PASSKEY_POPUP_RESPONSE', {
requestId: request.requestId,
cancelled: true
}, 'background');
};
if (!request) {
return (
<div className="flex justify-center py-8">
<LoadingSpinner />
</div>
);
}
return (
<>
{showBypassDialog && request && (
<PasskeyBypassDialog
origin={new URL(request.origin).hostname}
onChoice={handleBypassChoice}
onCancel={() => setShowBypassDialog(false)}
/>
)}
<div className="space-y-6">
<div className="text-center">
<h1 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">
{t('passkeys.authenticate.title')}
</h1>
<p className="text-sm text-gray-600 dark:text-gray-400">
{t('passkeys.authenticate.signInFor')} <strong>{request.origin}</strong>
</p>
</div>
{error && (
<div className="p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
<p className="text-sm text-red-800 dark:text-red-200">{error}</p>
</div>
)}
<div className="space-y-4">
{availablePasskeys && availablePasskeys.length > 0 ? (
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
{t('passkeys.authenticate.selectPasskey')}
</label>
<div className="space-y-2 max-h-48 overflow-y-auto border rounded-lg p-2 bg-gray-50 dark:bg-gray-800">
{availablePasskeys.map((pk, index) => (
<div
key={pk.id}
ref={index === 0 ? firstPasskeyRef : null}
tabIndex={0}
className="p-3 rounded-lg border cursor-pointer transition-colors bg-white border-gray-200 hover:bg-gray-100 hover:border-gray-300 dark:bg-gray-700 dark:border-gray-600 dark:hover:bg-gray-600 dark:hover:border-gray-500 focus:outline-none focus:ring-1 focus:ring-primary-500 focus:border-primary-500"
onClick={() => !loading && handleUsePasskey(pk.id)}
onKeyDown={(e) => {
if (e.key === 'Enter' && !loading) {
handleUsePasskey(pk.id);
}
}}
>
<div className="font-medium text-gray-900 dark:text-white text-sm truncate">
{pk.serviceName}
</div>
<div className="flex items-center justify-between text-xs text-gray-600 dark:text-gray-400">
<span className="truncate">{pk.displayName}</span>
</div>
</div>
))}
</div>
</div>
) : (
<div className="text-center py-8 bg-gray-50 dark:bg-gray-800 rounded-lg">
<p className="text-gray-600 dark:text-gray-400">
{t('passkeys.authenticate.noPasskeysFound')}
</p>
</div>
)}
</div>
<div className="space-y-3">
<Button
variant="secondary"
onClick={handleFallback}
>
{t('passkeys.authenticate.useBrowserPasskey')}
</Button>
<Button
variant="secondary"
onClick={handleCancel}
>
{t('common.cancel')}
</Button>
</div>
</div>
</>
);
};
export default PasskeyAuthenticate;

View File

@@ -0,0 +1,653 @@
import React, { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useLocation } from 'react-router-dom';
import { sendMessage } from 'webext-bridge/popup';
import { extractDomain, extractRootDomain } from '@/entrypoints/contentScript/Filter';
import Alert from '@/entrypoints/popup/components/Alert';
import Button from '@/entrypoints/popup/components/Button';
import PasskeyBypassDialog from '@/entrypoints/popup/components/Dialogs/PasskeyBypassDialog';
import { FormInput } from '@/entrypoints/popup/components/Forms/FormInput';
import LoadingSpinner from '@/entrypoints/popup/components/LoadingSpinner';
import { useDb } from '@/entrypoints/popup/context/DbContext';
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
import { useWebApi } from '@/entrypoints/popup/context/WebApiContext';
import { useVaultLockRedirect } from '@/entrypoints/popup/hooks/useVaultLockRedirect';
import { useVaultMutate } from '@/entrypoints/popup/hooks/useVaultMutate';
import { PASSKEY_DISABLED_SITES_KEY } from '@/utils/Constants';
import type { Passkey } from '@/utils/dist/shared/models/vault';
import { PasskeyAuthenticator } from '@/utils/passkey/PasskeyAuthenticator';
import { PasskeyHelper } from '@/utils/passkey/PasskeyHelper';
import type { CreateRequest, PasskeyCreateCredentialResponse, PendingPasskeyCreateRequest } from '@/utils/passkey/types';
import { storage } from "#imports";
/**
* PasskeyCreate
*/
const PasskeyCreate: React.FC = () => {
const { t } = useTranslation();
const location = useLocation();
const { setIsInitialLoading } = useLoading();
const dbContext = useDb();
const webApi = useWebApi();
const { executeVaultMutation, isLoading: isMutating, syncStatus } = useVaultMutate();
const [request, setRequest] = useState<PendingPasskeyCreateRequest | null>(null);
const [displayName, setDisplayName] = useState('');
const [error, setError] = useState<string | null>(null);
const { isLocked } = useVaultLockRedirect();
const [existingPasskeys, setExistingPasskeys] = useState<Array<Passkey & { Username?: string | null; ServiceName?: string | null }>>([]);
const [selectedPasskeyToReplace, setSelectedPasskeyToReplace] = useState<string | null>(null);
const [showCreateForm, setShowCreateForm] = useState(false);
const [localLoading, setLocalLoading] = useState(false);
const [showBypassDialog, setShowBypassDialog] = useState(false);
const createNewButtonRef = useRef<HTMLButtonElement>(null);
const displayNameInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
/**
* fetchRequestData
*/
const fetchRequestData = async () : Promise<void> => {
// Wait for DB to be initialized
if (!dbContext.dbInitialized) {
return;
}
// If vault is locked, the hook will handle redirect, we just return
if (isLocked) {
return;
}
// Get the requestId from URL
const params = new URLSearchParams(location.search);
const requestId = params.get('requestId');
if (requestId) {
try {
// Fetch the full request data from background
const data = await sendMessage('GET_REQUEST_DATA', { requestId }, 'background') as unknown as PendingPasskeyCreateRequest;
if (data && data.type === 'create') {
setRequest(data);
/**
* Set default displayName: use rp.name if available, otherwise use rpId
* This aligns with iOS/Android behavior
*/
const defaultName = data.publicKey?.rp?.name || data.publicKey?.rp?.id || 'Passkey';
setDisplayName(defaultName);
// Check for existing passkeys for this RP ID and user
if (dbContext.sqliteClient && data.publicKey?.rp?.id) {
const allPasskeysForRpId = dbContext.sqliteClient.getPasskeysByRpId(data.publicKey.rp.id);
/**
* Filter by user ID and/or username if provided
* This allows for multiple users on the same site
*/
let filtered = allPasskeysForRpId;
if (data.publicKey.user?.id || data.publicKey.user?.name) {
filtered = allPasskeysForRpId.filter(passkey => {
/**
* Match by user handle if both are available
* The request has base64url encoded user.id, passkey has UserHandle as byte array
* Convert request's user.id to bytes for comparison
*/
if (data.publicKey.user?.id && passkey.UserHandle) {
try {
const requestUserIdBytes = PasskeyHelper.base64urlToBytes(data.publicKey.user.id);
const passkeyUserHandle = passkey.UserHandle instanceof Uint8Array ? passkey.UserHandle : new Uint8Array(passkey.UserHandle);
// Compare byte arrays
if (requestUserIdBytes.length === passkeyUserHandle.length &&
requestUserIdBytes.every((byte, idx) => byte === passkeyUserHandle[idx])) {
return true;
}
} catch {
// If conversion fails, skip this passkey
}
}
// Also match by username if available (from the credential)
if (data.publicKey.user?.name && passkey.Username) {
if (passkey.Username === data.publicKey.user.name) {
return true;
}
}
// If neither user ID nor username match, exclude this passkey
return false;
});
}
setExistingPasskeys(filtered);
// If no existing passkeys for this user, go straight to create form
if (filtered.length === 0) {
setShowCreateForm(true);
}
}
}
} catch (error) {
console.error('Failed to fetch request data:', error);
setError(t('common.errors.unknownError'));
}
}
setIsInitialLoading(false);
};
fetchRequestData();
}, [location, setIsInitialLoading, dbContext.dbInitialized, dbContext.sqliteClient, isLocked, t]);
// Auto-focus create new button or input field
useEffect(() => {
if (showCreateForm && displayNameInputRef.current) {
displayNameInputRef.current.focus();
} else if (!showCreateForm && existingPasskeys.length > 0 && createNewButtonRef.current) {
createNewButtonRef.current.focus();
}
}, [showCreateForm, existingPasskeys.length]);
// Handle Enter key to submit
useEffect(() => {
/**
* Handle Enter key to submit
*/
const handleKeyDown = (e: KeyboardEvent) : void => {
if (e.key === 'Enter' && !localLoading && !isMutating) {
if (showCreateForm) {
handleCreate();
}
}
};
window.addEventListener('keydown', handleKeyDown);
return () : void => window.removeEventListener('keydown', handleKeyDown);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [showCreateForm, localLoading, isMutating]);
/**
* Handle when user clicks "Create New Passkey" button
*/
const handleCreateNew = () : void => {
setSelectedPasskeyToReplace(null);
setShowCreateForm(true);
};
/**
* Handle when user selects an existing passkey to replace
*/
const handleSelectReplace = (passkeyId: string) : void => {
setSelectedPasskeyToReplace(passkeyId);
setShowCreateForm(true);
};
/**
* Handle passkey creation
*/
const handleCreate = async () : Promise<void> => {
if (!request || !dbContext.sqliteClient) {
return;
}
setError(null);
try {
// Extract favicon from origin URL
let faviconLogo: Uint8Array | undefined = undefined;
if (request.origin) {
setLocalLoading(true);
try {
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Favicon extraction timed out')), 5000)
);
const faviconPromise = webApi.get<{ image: string }>('Favicon/Extract?url=' + request.origin);
const faviconResponse = await Promise.race([faviconPromise, timeoutPromise]) as { image: string };
if (faviconResponse?.image) {
// Use browser-compatible base64 decoding
const binaryString = atob(faviconResponse.image);
const decodedImage = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
decodedImage[i] = binaryString.charCodeAt(i);
}
faviconLogo = decodedImage;
}
} catch {
// Favicon extraction failed or timed out, this is not a critical error so we can ignore it.
}
}
// Build the CreateRequest
const createRequest: CreateRequest = {
origin: request.origin,
requestId: request.requestId,
publicKey: {
rp: request.publicKey.rp,
user: request.publicKey.user,
challenge: request.publicKey.challenge,
pubKeyCredParams: request.publicKey.pubKeyCredParams,
attestation: request.publicKey.attestation,
authenticatorSelection: request.publicKey.authenticatorSelection
}
};
/**
* Generate a new GUID for the passkey which will be embedded in the passkey
* metadata and send back to the RP as the credential.id and credential.rawId.
*/
const newPasskeyGuid = crypto.randomUUID().toUpperCase();
const newPasskeyGuidBytes = PasskeyHelper.guidToBytes(newPasskeyGuid);
const newPasskeyGuidBase64url = PasskeyHelper.guidToBase64url(newPasskeyGuid);
// Check if PRF evaluation is requested during registration
const prfExtension = request.publicKey?.extensions?.prf;
const enablePrf = !!prfExtension;
const prfEvalInputs = prfExtension?.eval;
// Create passkey using static method (generates keys and credential ID)
const result = await PasskeyAuthenticator.createPasskey(newPasskeyGuidBytes, createRequest, {
uvPerformed: true,
credentialIdBytes: 16,
enablePrf,
prfInputs: prfEvalInputs // Pass PRF evaluation salts if provided
});
const { credential, stored, prfEnabled, prfResults } = result;
// Use vault mutation to store both credential and passkey
await executeVaultMutation(
async () => {
if (selectedPasskeyToReplace) {
// Replace existing passkey: update the credential and passkey
const existingPasskey = dbContext.sqliteClient!.getPasskeyById(selectedPasskeyToReplace);
if (existingPasskey) {
// Update the parent credential with new favicon and user-provided display name
await dbContext.sqliteClient!.updateCredentialById(
{
Id: existingPasskey.CredentialId,
ServiceName: displayName,
ServiceUrl: request.origin,
Username: request.publicKey.user.name,
Password: '',
Notes: '',
Logo: faviconLogo ?? undefined,
Alias: {
FirstName: '',
LastName: '',
NickName: '',
BirthDate: '0001-01-01 00:00:00',
Gender: '',
Email: ''
},
},
[],
[]
);
// Delete the old passkey
await dbContext.sqliteClient!.deletePasskeyById(selectedPasskeyToReplace);
/**
* Create new passkey with same credential
* Convert userId from base64 string to byte array for database storage
*/
let userHandleBytes: Uint8Array | null = null;
if (stored.userId) {
try {
userHandleBytes = PasskeyHelper.base64urlToBytes(stored.userId);
} catch {
// If conversion fails, store as null
userHandleBytes = null;
}
}
await dbContext.sqliteClient!.createPasskey({
Id: newPasskeyGuid,
CredentialId: existingPasskey.CredentialId,
RpId: stored.rpId,
UserHandle: userHandleBytes,
PublicKey: JSON.stringify(stored.publicKey),
PrivateKey: JSON.stringify(stored.privateKey),
DisplayName: request.publicKey.user.displayName || request.publicKey.user.name || '',
PrfKey: stored.prfSecret ? PasskeyHelper.base64urlToBytes(stored.prfSecret) : undefined,
AdditionalData: null
});
}
} else {
// Create new credential and passkey
const credentialId = await dbContext.sqliteClient!.createCredential(
{
Id: '',
ServiceName: displayName,
ServiceUrl: request.origin,
Username: request.publicKey.user.name,
Password: '',
Notes: '',
Logo: faviconLogo ?? undefined,
Alias: {
FirstName: '',
LastName: '',
NickName: '',
BirthDate: '0001-01-01 00:00:00', // TODO: once birthdate is made nullable in datamodel refactor, remove this.
Gender: '',
Email: ''
}
},
[]
);
/**
* Create the Passkey linked to the credential
* Note: We let the database generate a GUID for Id, which we'll convert to base64url for the RP
* Convert userId from base64 string to byte array for database storage
*/
let userHandleBytes: Uint8Array | null = null;
if (stored.userId) {
try {
userHandleBytes = PasskeyHelper.base64urlToBytes(stored.userId);
} catch {
// If conversion fails, store as null
userHandleBytes = null;
}
}
await dbContext.sqliteClient!.createPasskey({
Id: newPasskeyGuid,
CredentialId: credentialId,
RpId: stored.rpId,
UserHandle: userHandleBytes,
PublicKey: JSON.stringify(stored.publicKey),
PrivateKey: JSON.stringify(stored.privateKey),
DisplayName: request.publicKey.user.displayName || request.publicKey.user.name || '',
PrfKey: stored.prfSecret ? PasskeyHelper.base64urlToBytes(stored.prfSecret) : undefined,
AdditionalData: null
});
}
},
{
/**
* Wait for vault mutation to have synced with server, then send passkey create success response
* with the GUID-based credential ID.
*/
onSuccess: async () => {
// Prepare PRF extension response if PRF was enabled
let prfExtensionResponse;
if (prfEnabled) {
prfExtensionResponse = {
prf: {
enabled: true,
results: prfResults ? {
first: PasskeyHelper.bytesToBase64url(new Uint8Array(prfResults.first)),
second: prfResults.second ? PasskeyHelper.bytesToBase64url(new Uint8Array(prfResults.second)) : undefined
} : undefined
}
};
}
// Use the GUID-based credential ID instead of the random one from the provider
const flattenedCredential: PasskeyCreateCredentialResponse = {
id: newPasskeyGuidBase64url,
rawId: newPasskeyGuidBase64url,
clientDataJSON: credential.response.clientDataJSON,
attestationObject: credential.response.attestationObject,
extensions: prfExtensionResponse
};
/*
* Send response back to background
* The background script will close the window (Safari-compatible)
*/
await sendMessage('PASSKEY_POPUP_RESPONSE', {
requestId: request.requestId,
credential: flattenedCredential
}, 'background');
},
/**
* onError
*/
onError: (err) => {
console.error('PasskeyCreate: Error storing passkey', err);
setError(t('common.errors.unknownError'));
}
}
);
} catch (error) {
console.error('PasskeyCreate: Error creating passkey', error);
setError(t('common.errors.unknownError'));
}
};
/**
* Handle fallback - show bypass dialog first
*/
const handleFallback = async () : Promise<void> => {
setShowBypassDialog(true);
};
/**
* Handle bypass choice
*/
const handleBypassChoice = async (choice: 'once' | 'always') : Promise<void> => {
if (!request) {
return;
}
if (choice === 'always') {
// Add to permanent disabled list
const hostname = new URL(request.origin).hostname;
const baseDomain = extractRootDomain(extractDomain(hostname));
const disabledSites = await storage.getItem(PASSKEY_DISABLED_SITES_KEY) as string[] ?? [];
if (!disabledSites.includes(baseDomain)) {
disabledSites.push(baseDomain);
await storage.setItem(PASSKEY_DISABLED_SITES_KEY, disabledSites);
}
}
// For 'once', we don't store anything - just bypass this one time
/*
* Tell background to use native implementation
* The background script will close the window (Safari-compatible)
*/
await sendMessage('PASSKEY_POPUP_RESPONSE', {
requestId: request.requestId,
fallback: true
}, 'background');
};
/**
* Handle cancel
*/
const handleCancel = async () : Promise<void> => {
if (!request) {
return;
}
/*
* Tell background user cancelled
* The background script will close the window (Safari-compatible)
*/
await sendMessage('PASSKEY_POPUP_RESPONSE', {
requestId: request.requestId,
cancelled: true
}, 'background');
};
if (!request) {
return (
<div className="flex justify-center py-8">
<LoadingSpinner />
</div>
);
}
return (
<>
{showBypassDialog && request && (
<PasskeyBypassDialog
origin={new URL(request.origin).hostname}
onChoice={handleBypassChoice}
onCancel={() => setShowBypassDialog(false)}
/>
)}
{(localLoading || isMutating) && (
<div className="fixed inset-0 flex flex-col justify-center items-center bg-white dark:bg-gray-900 bg-opacity-90 dark:bg-opacity-90 z-50">
<LoadingSpinner />
<div className="text-sm text-gray-500 mt-2">
{syncStatus}
</div>
</div>
)}
<div className="space-y-6">
<div className="text-center">
<h1 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">
{t('passkeys.create.title')}
</h1>
<p className="text-sm text-gray-600 dark:text-gray-400">
{t('passkeys.create.createFor')} <strong>{request.origin}</strong>
</p>
</div>
{error && (
<Alert variant="error">
{error}
</Alert>
)}
{/* Step 1: Show existing passkeys selection or create new option */}
{!showCreateForm && existingPasskeys.length > 0 && (
<div className="space-y-4">
<Button
variant="primary"
onClick={handleCreateNew}
ref={createNewButtonRef}
>
{t('passkeys.create.createNewPasskey')}
</Button>
<Button
variant="secondary"
onClick={handleFallback}
>
{t('passkeys.create.useBrowserPasskey')}
</Button>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-300 dark:border-gray-600" />
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400">
{t('common.or')}
</span>
</div>
</div>
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
{t('passkeys.create.selectPasskeyToReplace')}
</label>
<div className="space-y-2 max-h-48 overflow-y-auto border rounded-lg p-2 bg-gray-50 dark:bg-gray-800">
{existingPasskeys.map((passkey) => (
<button
key={passkey.Id}
onClick={() => handleSelectReplace(passkey.Id)}
className="w-full p-3 text-left rounded-lg border cursor-pointer transition-colors bg-white border-gray-200 hover:bg-gray-100 hover:border-gray-300 dark:bg-gray-700 dark:border-gray-600 dark:hover:bg-gray-600 dark:hover:border-gray-500 focus:outline-none focus:ring-1 focus:ring-primary-500 focus:border-primary-500"
>
<div className="flex items-center justify-between">
<div className="flex-1">
<div className="font-medium text-gray-900 dark:text-white text-sm truncate">
{passkey.ServiceName}
</div>
<div className="flex items-center justify-between text-xs text-gray-600 dark:text-gray-400">
<span className="truncate">{passkey.DisplayName}</span>
</div>
</div>
<svg className="w-5 h-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</div>
</button>
))}
</div>
</div>
<Button
variant="secondary"
onClick={handleCancel}
>
{t('common.cancel')}
</Button>
</div>
)}
{/* Step 2: Show create form with display name */}
{showCreateForm && (
<div className="space-y-4">
{selectedPasskeyToReplace && (
<Alert variant="warning">
{t('passkeys.create.replacingPasskey', {
displayName: existingPasskeys.find(p => p.Id === selectedPasskeyToReplace)?.DisplayName || ''
})}
</Alert>
)}
<FormInput
id="displayName"
label={t('passkeys.create.titleLabel')}
value={displayName}
onChange={setDisplayName}
placeholder={t('passkeys.create.titlePlaceholder')}
ref={displayNameInputRef}
/>
<div className="space-y-3">
<Button
variant="primary"
onClick={handleCreate}
>
{selectedPasskeyToReplace ? t('passkeys.create.confirmReplace') : t('passkeys.create.createButton')}
</Button>
{existingPasskeys.length > 0 ? (
<Button
variant="secondary"
onClick={() => {
setShowCreateForm(false);
setSelectedPasskeyToReplace(null);
}}
>
{t('common.back')}
</Button>
) : (
<>
<Button
variant="secondary"
onClick={handleFallback}
>
{t('passkeys.create.useBrowserPasskey')}
</Button>
<Button
variant="secondary"
onClick={handleCancel}
>
{t('common.cancel')}
</Button>
</>
)}
</div>
</div>
)}
</div>
</>
);
};
export default PasskeyCreate;

View File

@@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { sendMessage } from 'webext-bridge/popup';
import HelpModal from '@/entrypoints/popup/components/HelpModal';
import HelpModal from '@/entrypoints/popup/components/Dialogs/HelpModal';
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
import { AUTO_LOCK_TIMEOUT_KEY } from '@/utils/Constants';

View File

@@ -0,0 +1,200 @@
import React, { useEffect, useState, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { extractDomain, extractRootDomain } from '@/entrypoints/contentScript/Filter';
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
import {
PASSKEY_PROVIDER_ENABLED_KEY,
PASSKEY_DISABLED_SITES_KEY
} from '@/utils/Constants';
import { storage, browser } from "#imports";
/**
* Passkey settings type.
*/
type PasskeySettingsType = {
disabledUrls: string[];
currentUrl: string;
isEnabled: boolean;
isGloballyEnabled: boolean;
}
/**
* Passkey settings page component.
*/
const PasskeySettings: React.FC = () => {
const { t } = useTranslation();
const { setIsInitialLoading } = useLoading();
const [settings, setSettings] = useState<PasskeySettingsType>({
disabledUrls: [],
currentUrl: '',
isEnabled: true,
isGloballyEnabled: true
});
/**
* Get current tab in browser.
*/
const getCurrentTab = async () : Promise<chrome.tabs.Tab> => {
const queryOptions = { active: true, currentWindow: true };
const [tab] = await browser.tabs.query(queryOptions);
return tab;
};
/**
* Load settings.
*/
const loadSettings = useCallback(async () : Promise<void> => {
const tab = await getCurrentTab();
const hostname = new URL(tab.url ?? '').hostname;
const baseDomain = extractRootDomain(extractDomain(hostname));
// Load settings from local storage
const disabledUrls = await storage.getItem(PASSKEY_DISABLED_SITES_KEY) as string[] ?? [];
const isGloballyEnabled = await storage.getItem(PASSKEY_PROVIDER_ENABLED_KEY) !== false; // Default to true if not set
// Check if current base domain is disabled
const isEnabled = !disabledUrls.includes(baseDomain);
setSettings({
disabledUrls,
currentUrl: baseDomain,
isEnabled,
isGloballyEnabled
});
setIsInitialLoading(false);
}, [setIsInitialLoading]);
useEffect(() => {
loadSettings();
}, [loadSettings]);
/**
* Toggle current site.
*/
const toggleCurrentSite = async () : Promise<void> => {
const { currentUrl, disabledUrls, isEnabled } = settings;
let newDisabledUrls = [...disabledUrls];
if (isEnabled) {
// When disabling, add to permanent disabled list
if (!newDisabledUrls.includes(currentUrl)) {
newDisabledUrls.push(currentUrl);
}
} else {
// When enabling, remove from disabled list
newDisabledUrls = newDisabledUrls.filter(url => url !== currentUrl);
}
await storage.setItem(PASSKEY_DISABLED_SITES_KEY, newDisabledUrls);
setSettings(prev => ({
...prev,
disabledUrls: newDisabledUrls,
isEnabled: !isEnabled
}));
};
/**
* Reset settings.
*/
const resetSettings = async () : Promise<void> => {
await storage.setItem(PASSKEY_DISABLED_SITES_KEY, []);
setSettings(prev => ({
...prev,
disabledUrls: [],
isEnabled: true
}));
};
/**
* Toggle global passkey provider.
*/
const toggleGlobalPasskeyProvider = async () : Promise<void> => {
const newGloballyEnabled = !settings.isGloballyEnabled;
await storage.setItem(PASSKEY_PROVIDER_ENABLED_KEY, newGloballyEnabled);
setSettings(prev => ({
...prev,
isGloballyEnabled: newGloballyEnabled
}));
};
return (
<div className="space-y-6">
{/* Global Settings Section */}
<section>
<h3 className="text-md font-semibold text-gray-900 dark:text-white mb-3">{t('settings.globalSettings')}</h3>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
<div className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="font-medium text-gray-900 dark:text-white">{t('passkeys.settings.passkeyProvider')}</p>
<p className={`text-sm mt-1 ${settings.isGloballyEnabled ? 'text-gray-600 dark:text-gray-400' : 'text-red-600 dark:text-red-400'}`}>
{settings.isGloballyEnabled ? t('settings.activeOnAllSites') : t('settings.disabledOnAllSites')}
</p>
</div>
<button
onClick={toggleGlobalPasskeyProvider}
className={`px-4 py-2 rounded-md transition-colors ${
settings.isGloballyEnabled
? 'bg-green-500 hover:bg-green-600 text-white'
: 'bg-red-500 hover:bg-red-600 text-white'
}`}
>
{settings.isGloballyEnabled ? t('settings.enabled') : t('settings.disabled')}
</button>
</div>
</div>
</div>
</section>
{/* Site-Specific Settings Section */}
{settings.isGloballyEnabled && (
<section>
<h3 className="text-md font-semibold text-gray-900 dark:text-white mb-3">{t('settings.siteSpecificSettings')}</h3>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
<div className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="font-medium text-gray-900 dark:text-white">{t('passkeys.settings.passkeyProviderOn')}{settings.currentUrl}</p>
<p className={`text-sm mt-1 ${settings.isEnabled ? 'text-gray-600 dark:text-gray-400' : 'text-red-600 dark:text-red-400'}`}>
{settings.isEnabled ? t('settings.enabledForThisSite') : t('settings.disabledForThisSite')}
</p>
</div>
{settings.isGloballyEnabled && (
<button
onClick={toggleCurrentSite}
className={`px-4 py-2 ml-1 rounded-md transition-colors ${
settings.isEnabled
? 'bg-green-500 hover:bg-green-600 text-white'
: 'bg-red-500 hover:bg-red-600 text-white'
}`}
>
{settings.isEnabled ? t('settings.enabled') : t('settings.disabled')}
</button>
)}
</div>
<div className="mt-4">
<button
onClick={resetSettings}
className="w-full px-4 py-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 rounded-md text-gray-700 dark:text-gray-300 transition-colors text-sm"
>
{t('settings.resetAllSiteSettings')}
</button>
</div>
</div>
</div>
</section>
)}
</div>
);
};
export default PasskeySettings;

View File

@@ -4,7 +4,7 @@ import { useNavigate } from 'react-router-dom';
import HeaderButton from '@/entrypoints/popup/components/HeaderButton';
import { HeaderIconType } from '@/entrypoints/popup/components/Icons/HeaderIcons';
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
import { useApp } from '@/entrypoints/popup/context/AppContext';
import { useHeaderButtons } from '@/entrypoints/popup/context/HeaderButtonsContext';
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
import { useTheme } from '@/entrypoints/popup/context/ThemeContext';
@@ -21,7 +21,7 @@ import { browser } from "#imports";
const Settings: React.FC = () => {
const { t } = useTranslation();
const { theme, setTheme } = useTheme();
const authContext = useAuth();
const app = useApp();
const { setHeaderButtons } = useHeaderButtons();
const { setIsInitialLoading } = useLoading();
const { loadApiUrl, getDisplayUrl } = useApiUrl();
@@ -109,7 +109,7 @@ const Settings: React.FC = () => {
* Handle logout.
*/
const handleLogout = async () : Promise<void> => {
navigate('/logout', { replace: true });
app.logout();
};
/**
@@ -147,6 +147,13 @@ const Settings: React.FC = () => {
navigate('/settings/context-menu');
};
/**
* Navigate to passkey settings.
*/
const navigateToPasskeySettings = () : void => {
navigate('/settings/passkeys');
};
return (
<div className="space-y-6">
<div className="flex justify-between items-center mb-4">
@@ -162,13 +169,13 @@ const Settings: React.FC = () => {
<div className="flex-shrink-0">
<div className="w-10 h-10 rounded-full bg-primary-100 dark:bg-primary-900 flex items-center justify-center">
<span className="text-primary-600 dark:text-primary-400 text-lg font-medium">
{authContext.username?.[0]?.toUpperCase() || '?'}
{app.username?.[0]?.toUpperCase() || '?'}
</span>
</div>
</div>
<div>
<p className="text font-medium text-gray-900 dark:text-white">
{authContext.username}
{app.username}
</p>
<p className="text-sm text-gray-500 dark:text-gray-400">
{t('settings.loggedIn')}
@@ -237,6 +244,38 @@ const Settings: React.FC = () => {
</svg>
</button>
{/* Passkey Settings */}
<button
onClick={navigateToPasskeySettings}
className="w-full px-4 py-3 flex items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
<div className="flex items-center">
<svg
className="w-5 h-5 mr-3 text-gray-600 dark:text-gray-400"
fill="none"
stroke="currentColor"
strokeWidth={2}
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"
/>
</svg>
<span className="text-gray-900 dark:text-white">{t('settings.passkeySettings')}</span>
</div>
<svg
className="w-4 h-4 text-gray-400"
fill="none"
stroke="currentColor"
strokeWidth={2}
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
</svg>
</button>
{/* Context Menu Settings */}
<button
onClick={navigateToContextMenuSettings}

View File

@@ -0,0 +1,599 @@
/**
* WebAuthn override injection script - included in web_accessible_resources as "webauthn.js"
* and runs in page context to override the browser's built-in credentials API.
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
import type {
WebAuthnCreateEventDetail,
WebAuthnGetEventDetail,
WebAuthnCreateResponseDetail,
WebAuthnGetResponseDetail,
ProviderCreateCredential,
ProviderGetCredential
} from '@/utils/passkey/webauthn.types';
import { defineUnlistedScript } from '#imports';
export default defineUnlistedScript(() => {
// Only run once
if ((window as any).__aliasVaultWebAuthnIntercepted) {
return;
}
(window as any).__aliasVaultWebAuthnIntercepted = true;
// Get the original implementations from the reservation script or bind directly
const queue = (window as any).__aliasVaultWebAuthnQueue;
const originalCreate = queue?.originalCreate || navigator.credentials.create.bind(navigator.credentials);
const originalGet = queue?.originalGet || navigator.credentials.get.bind(navigator.credentials);
const pendingQueue = queue?.pendingQueue || [];
/**
* Helper to convert ArrayBuffer to base64
*/
function bufferToBase64(buffer: ArrayBuffer | ArrayBufferView): string {
const bytes = buffer instanceof ArrayBuffer
? new Uint8Array(buffer)
: new Uint8Array(buffer.buffer, (buffer as any).byteOffset, (buffer as any).byteLength);
let binary = '';
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
/**
* Helper to convert ArrayBuffer to base64
*/
function base64ToBuffer(base64: string): ArrayBuffer {
// Handle both base64 and base64url formats
const base64Standard = base64.replace(/-/g, '+').replace(/_/g, '/');
// Add padding if needed
const padded = base64Standard + '==='.slice((base64Standard.length + 3) % 4);
const binary = atob(padded);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes.buffer;
}
/**
* Override credentials.create (monkey patch)
*/
navigator.credentials.create = async function(options?: CredentialCreationOptions) : Promise<Credential | null> {
if (!options?.publicKey) {
return originalCreate(options);
}
// Send event to content script
const requestId = Math.random().toString(36).substr(2, 9);
// Serialize PRF extensions if present (convert ArrayBuffers to base64)
let serializedExtensions: any = undefined;
if (options.publicKey.extensions) {
serializedExtensions = { ...options.publicKey.extensions };
if (serializedExtensions.prf?.eval) {
const prfEval: any = { first: bufferToBase64(serializedExtensions.prf.eval.first) };
if (serializedExtensions.prf.eval.second) {
prfEval.second = bufferToBase64(serializedExtensions.prf.eval.second);
}
serializedExtensions.prf = { eval: prfEval };
}
}
const eventDetail: WebAuthnCreateEventDetail = {
requestId,
publicKey: {
...options.publicKey,
challenge: bufferToBase64(options.publicKey.challenge),
user: {
...options.publicKey.user,
id: bufferToBase64(options.publicKey.user.id)
},
excludeCredentials: options.publicKey.excludeCredentials?.map(cred => ({
...cred,
id: bufferToBase64(cred.id)
})),
extensions: serializedExtensions
},
origin: window.location.origin
};
const event = new CustomEvent<WebAuthnCreateEventDetail>('aliasvault:webauthn:create', {
detail: eventDetail
});
window.dispatchEvent(event);
// Wait for response
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
cleanup();
// Timeout - fall back to native
originalCreate(options).then(resolve).catch(reject);
}, 30000); // 30 second timeout
/**
* cleanup
*/
function cleanup() : void {
clearTimeout(timeout);
window.removeEventListener('aliasvault:webauthn:create:response', handler as EventListener);
}
/**
* handler
*/
function handler(e: CustomEvent<WebAuthnCreateResponseDetail>) : void {
if (e.detail.requestId !== requestId) {
return;
}
cleanup();
if (e.detail.fallback) {
// User chose to use native implementation
originalCreate(options).then(resolve).catch(reject);
} else if (e.detail.error) {
reject(new Error(e.detail.error));
} else if (e.detail.credential) {
// Create a proper credential object with required methods
const cred: ProviderCreateCredential = e.detail.credential;
try {
// Decode the attestation object to extract authenticator data
const attestationObjectBuffer = base64ToBuffer(cred.attestationObject);
const attObjBytes = new Uint8Array(attestationObjectBuffer);
/*
* Simple CBOR parser to extract authData
* CBOR map starts with 0xA3 (map with 3 items)
* Keys are: "fmt" (0x63), "attStmt" (0x67), "authData" (0x68)
*/
let authDataBuffer = new ArrayBuffer(0);
try {
// Find "authData" key (0x68 0x61 0x75 0x74 0x68 0x44 0x61 0x74 0x61)
const authDataKeyBytes = [0x68, 0x61, 0x75, 0x74, 0x68, 0x44, 0x61, 0x74, 0x61];
for (let i = 0; i < attObjBytes.length - authDataKeyBytes.length; i++) {
let match = true;
for (let j = 0; j < authDataKeyBytes.length; j++) {
if (attObjBytes[i + j] !== authDataKeyBytes[j]) {
match = false;
break;
}
}
if (match) {
// Found "authData" key, next byte is the type (0x58 = byte string)
const typeIdx = i + authDataKeyBytes.length;
if (attObjBytes[typeIdx] === 0x58) {
// Next byte is the length
const length = attObjBytes[typeIdx + 1];
authDataBuffer = attObjBytes.slice(typeIdx + 2, typeIdx + 2 + length).buffer;
}
break;
}
}
} catch {
// Ignore
}
// Create response object with proper prototype
const response = Object.create(AuthenticatorAttestationResponse.prototype);
const clientDataJSONBuffer = base64ToBuffer(cred.clientDataJSON);
Object.defineProperties(response, {
clientDataJSON: {
value: clientDataJSONBuffer,
writable: false,
enumerable: true,
configurable: true
},
attestationObject: {
value: attestationObjectBuffer,
writable: false,
enumerable: true,
configurable: true
},
getTransports: {
/**
* getTransports
*/
value: function() : string[] {
return ['internal'];
},
writable: true,
enumerable: true,
configurable: true
},
getAuthenticatorData: {
/**
* getAuthenticatorData
*/
value: function() : ArrayBuffer {
return authDataBuffer;
},
writable: true,
enumerable: true,
configurable: true
},
getPublicKey: {
/**
* getPublicKey
*/
value: function() : JsonWebKey | null {
return null;
},
writable: true,
enumerable: true,
configurable: true
},
getPublicKeyAlgorithm: {
/**
* getPublicKeyAlgorithm
*/
value: function() : number {
return -7; // ES256
},
writable: true,
enumerable: true,
configurable: true
}
});
// Create credential object with proper prototype chain
const credential = Object.create(PublicKeyCredential.prototype);
Object.defineProperties(credential, {
id: {
value: cred.id,
writable: false,
enumerable: true,
configurable: true
},
type: {
value: 'public-key',
writable: false,
enumerable: true,
configurable: true
},
rawId: {
value: base64ToBuffer(cred.rawId),
writable: false,
enumerable: true,
configurable: true
},
authenticatorAttachment: {
value: 'cross-platform',
writable: false,
enumerable: true,
configurable: true
},
response: {
value: response,
writable: false,
enumerable: true,
configurable: true
},
getClientExtensionResults: {
/**
* getClientExtensionResults
*/
value: function() : any {
const extensions: any = {};
if (cred.extensions?.prf) {
extensions.prf = { ...cred.extensions.prf };
// Convert PRF results from base64url to ArrayBuffer if present
if ((cred.extensions.prf as any).results) {
extensions.prf.results = {
first: base64ToBuffer((cred.extensions.prf as any).results.first)
};
if ((cred.extensions.prf as any).results.second) {
extensions.prf.results.second = base64ToBuffer((cred.extensions.prf as any).results.second);
}
}
}
return extensions;
},
writable: true,
enumerable: true,
configurable: true
}
});
// Ensure the credential is recognized as a PublicKeyCredential instance
Object.defineProperty(credential, Symbol.toStringTag, {
value: 'PublicKeyCredential',
writable: false,
enumerable: false,
configurable: true
});
resolve(credential);
} catch (error) {
reject(error);
}
} else {
// Cancelled
resolve(null);
}
}
window.addEventListener('aliasvault:webauthn:create:response', handler as EventListener);
});
};
/**
* Override credentials.get (monkey patch)
*/
navigator.credentials.get = async function(options?: CredentialRequestOptions) : Promise<Credential | null> {
if (!options?.publicKey) {
return originalGet(options);
}
// Send event to content script
const requestId = Math.random().toString(36).substr(2, 9);
// Serialize PRF extensions if present (convert ArrayBuffers to base64)
let serializedExtensions: any = undefined;
if (options.publicKey.extensions) {
serializedExtensions = { ...options.publicKey.extensions };
if (serializedExtensions.prf?.eval) {
const prfEval: any = { first: bufferToBase64(serializedExtensions.prf.eval.first) };
if (serializedExtensions.prf.eval.second) {
prfEval.second = bufferToBase64(serializedExtensions.prf.eval.second);
}
serializedExtensions.prf = { eval: prfEval };
}
}
const eventDetail: WebAuthnGetEventDetail = {
requestId,
publicKey: {
...options.publicKey,
challenge: bufferToBase64(options.publicKey.challenge),
allowCredentials: options.publicKey.allowCredentials?.map(cred => ({
...cred,
id: bufferToBase64(cred.id)
})),
extensions: serializedExtensions
},
origin: window.location.origin
};
const event = new CustomEvent<WebAuthnGetEventDetail>('aliasvault:webauthn:get', {
detail: eventDetail
});
window.dispatchEvent(event);
// Wait for response
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
cleanup();
// Timeout - fall back to native
originalGet(options).then(resolve).catch(reject);
}, 30000);
/**
* cleanup
*/
function cleanup() : void {
clearTimeout(timeout);
window.removeEventListener('aliasvault:webauthn:get:response', handler as EventListener);
}
/**
* handler
*/
function handler(e: CustomEvent<WebAuthnGetResponseDetail>) : void {
if (e.detail.requestId !== requestId) {
return;
}
cleanup();
if (e.detail.fallback) {
// User chose to use native implementation
originalGet(options).then(resolve).catch(reject);
} else if (e.detail.error) {
reject(new Error(e.detail.error));
} else if (e.detail.credential) {
// Create a proper credential object with required methods
const cred: ProviderGetCredential = e.detail.credential;
// Create response object with proper prototype
const response = Object.create(AuthenticatorAssertionResponse.prototype);
Object.defineProperties(response, {
clientDataJSON: {
value: base64ToBuffer(cred.clientDataJSON),
writable: false,
enumerable: true,
configurable: true
},
authenticatorData: {
value: base64ToBuffer(cred.authenticatorData),
writable: false,
enumerable: true,
configurable: true
},
signature: {
value: base64ToBuffer(cred.signature),
writable: false,
enumerable: true,
configurable: true
},
userHandle: {
value: cred.userHandle ? base64ToBuffer(cred.userHandle) : null,
writable: false,
enumerable: true,
configurable: true
}
});
// Create credential object with proper prototype chain
const credential = Object.create(PublicKeyCredential.prototype);
Object.defineProperties(credential, {
id: {
value: cred.id,
writable: false,
enumerable: true,
configurable: true
},
type: {
value: 'public-key',
writable: false,
enumerable: true,
configurable: true
},
rawId: {
value: base64ToBuffer(cred.rawId),
writable: false,
enumerable: true,
configurable: true
},
authenticatorAttachment: {
value: 'cross-platform',
writable: false,
enumerable: true,
configurable: true
},
response: {
value: response,
writable: false,
enumerable: true,
configurable: true
},
getClientExtensionResults: {
/**
* getClientExtensionResults
*/
value: function() : any {
const extensions: any = {};
if (cred.prfResults) {
extensions.prf = {
results: {
first: base64ToBuffer(cred.prfResults.first)
}
};
if (cred.prfResults.second) {
extensions.prf.results.second = base64ToBuffer(cred.prfResults.second);
}
}
return extensions;
},
writable: true,
enumerable: true,
configurable: true
}
});
// Ensure the credential is recognized as a PublicKeyCredential instance
Object.defineProperty(credential, Symbol.toStringTag, {
value: 'PublicKeyCredential',
writable: false,
enumerable: false,
configurable: true
});
resolve(credential);
} else {
// Cancelled
resolve(null);
}
}
window.addEventListener('aliasvault:webauthn:get:response', handler as EventListener);
});
};
/*
* Store references to our override functions so we can re-apply them if needed.
* We need to capture these before any potential overwrites.
*/
const getOverrideRef = navigator.credentials.get;
const createOverrideRef = navigator.credentials.create;
// Add markers to our functions for easier verification
(getOverrideRef as any).__aliasVaultPatched = true;
(createOverrideRef as any).__aliasVaultPatched = true;
/**
* Apply or re-apply the monkey patches
*/
const applyPatches = (): void => {
const currentGet = navigator.credentials.get;
const currentCreate = navigator.credentials.create;
// Re-apply get if it's missing our marker
if (!(currentGet as any).__aliasVaultPatched) {
console.warn('[AliasVault] Re-applying credentials.get patch');
navigator.credentials.get = getOverrideRef;
}
// Re-apply create if it's missing our marker
if (!(currentCreate as any).__aliasVaultPatched) {
console.warn('[AliasVault] Re-applying credentials.create patch');
navigator.credentials.create = createOverrideRef;
}
};
/**
* Verification function to check if monkey patches are still in place
* @returns True if patches are verified, false otherwise
*/
const verifyPatches = (): boolean => {
const get = navigator.credentials.get;
const create = navigator.credentials.create;
// Check for our marker
if (!(get as any).__aliasVaultPatched || !(create as any).__aliasVaultPatched) {
console.error('[AliasVault] CRITICAL: Monkey patch markers missing!', {
hasGetMarker: !!(get as any).__aliasVaultPatched,
hasCreateMarker: !!(create as any).__aliasVaultPatched
});
return false;
}
return true;
};
// Verify immediately
if (!verifyPatches()) {
console.error('[AliasVault] Initial verification failed - re-applying patches');
applyPatches();
}
// Periodic verification for first 5 seconds (catches if something overwrites us)
let checkCount = 0;
const verifyInterval = setInterval(() => {
checkCount++;
if (!verifyPatches()) {
console.error('[AliasVault] Periodic verification failed - re-applying patches!');
applyPatches();
}
if (checkCount >= 10) {
clearInterval(verifyInterval);
}
}, 500);
/*
* Process any queued requests from the reservation script.
* This handles the case where the page called navigator.credentials
* before our full implementation finished loading.
*/
if (pendingQueue.length > 0) {
pendingQueue.forEach((request: any) => {
if (request.type === 'create') {
navigator.credentials.create(request.options)
.then(request.resolve)
.catch(request.reject);
} else if (request.type === 'get') {
navigator.credentials.get(request.options)
.then(request.resolve)
.catch(request.reject);
}
});
// Clear the queue
pendingQueue.length = 0;
}
// Clean up the reservation script globals
delete (window as any).__aliasVaultWebAuthnQueue;
delete (window as any).__aliasVaultWebAuthnReserved;
});

View File

@@ -0,0 +1,38 @@
type LogoutListener = (errorMessage: string) => void | Promise<void>;
/**
* Simple event emitter for logout events to avoid circular dependencies
* between WebApiService and Auth contexts.
*/
class LogoutEventEmitter {
private listeners: Set<LogoutListener> = new Set();
/**
* Subscribe to logout events.
* Returns an unsubscribe function.
*/
public subscribe(listener: LogoutListener): () => void {
this.listeners.add(listener);
return () => {
this.listeners.delete(listener);
};
}
/**
* Emit a logout event to all listeners.
*
* @param errorKey - The translation key of the error message to emit.
*/
public emit(errorTranslationKey: string): void {
this.listeners.forEach(listener => {
try {
listener(errorTranslationKey);
} catch (error) {
console.error('Error in logout listener:', error);
}
});
}
}
// Export singleton instance
export const logoutEventEmitter = new LogoutEventEmitter();

View File

@@ -9,6 +9,9 @@ import fiTranslations from './locales/fi.json';
import heTranslations from './locales/he.json';
import itTranslations from './locales/it.json';
import nlTranslations from './locales/nl.json';
import plTranslations from './locales/pl.json';
import ptTranslations from './locales/pt.json';
import ruTranslations from './locales/ru.json';
import ukTranslations from './locales/uk.json';
import zhTranslations from './locales/zh.json';
@@ -35,6 +38,15 @@ export const LANGUAGE_RESOURCES = {
nl: {
translation: nlTranslations
},
pl: {
translation: plTranslations
},
pt: {
translation: ptTranslations
},
ru: {
translation: ruTranslations
},
uk: {
translation: ukTranslations
},
@@ -84,6 +96,24 @@ export const AVAILABLE_LANGUAGES: ILanguageConfig[] = [
nativeName: 'Nederlands',
flag: '🇳🇱'
},
{
code: 'pl',
name: 'Polish',
nativeName: 'Polski',
flag: '🇵🇱'
},
{
code: 'pt',
name: 'Portuguese Brazilian',
nativeName: 'Português Brasileiro',
flag: '🇧🇷'
},
{
code: 'ru',
name: 'Russian',
nativeName: 'Русский',
flag: '🇷🇺'
},
{
code: 'uk',
name: 'Ukrainian',

View File

@@ -38,7 +38,7 @@
"wrongPassword": "Incorrect password. Please try again.",
"accountLocked": "Account temporarily locked due to too many failed attempts.",
"networkError": "Network error. Please check your connection and try again.",
"loginDataMissing": "Login session expired. Please try again."
"sessionExpired": "Your session has expired. Please log in again."
}
},
"menu": {
@@ -52,8 +52,10 @@
"error": "Error",
"success": "Success",
"cancel": "Cancel",
"back": "Back",
"use": "Utilitza",
"delete": "Suprimeix",
"or": "Or",
"close": "Tanca",
"copied": "Copied!",
"openInNewWindow": "Open in new window",
@@ -89,12 +91,11 @@
"executingOperation": "Executing operation...",
"loadMore": "Load more",
"errors": {
"VaultOutdated": "Your vault is outdated. Please login on the AliasVault website and follow the steps.",
"serverNotAvailable": "The AliasVault server is not available. Please try again later or contact support if the problem persists.",
"clientVersionNotSupported": "This version of the AliasVault browser extension is not supported by the server anymore. Please update your browser extension to the latest version.",
"browserExtensionOutdated": "This browser extension is outdated and cannot be used to access this vault. Please update this browser extension to continue.",
"serverVersionNotSupported": "The AliasVault server needs to be updated to a newer version in order to use this browser extension. Please contact support if you need help.",
"unknownError": "An unknown error occurred",
"failedToStoreVault": "Failed to store vault",
"vaultNotAvailable": "Vault not available",
"failedToRetrieveData": "Failed to retrieve data",
"vaultIsLocked": "Vault is locked",
@@ -198,6 +199,9 @@
"searchPlaceholder": "Search credentials...",
"welcomeTitle": "Welcome to AliasVault!",
"welcomeDescription": "To use the AliasVault browser extension: navigate to a website and use the AliasVault autofill popup to create a new credential.",
"noPasskeysFound": "No passkeys have been created yet. Passkeys are created by visiting a website that offers passkeys as an authentication method.",
"noAttachmentsFound": "No credentials with attachments found",
"noMatchingCredentials": "No matching credentials found",
"createdAt": "Created",
"updatedAt": "Last updated",
"autofill": "Autofill",
@@ -213,6 +217,13 @@
"saveCredential": "Save credential",
"deleteCredentialTitle": "Delete Credential",
"deleteCredentialConfirm": "Are you sure you want to delete this credential? This action cannot be undone.",
"filters": {
"all": "(All) Credentials",
"passkeys": "Passkeys",
"aliases": "Aliases",
"userpass": "Passwords",
"attachments": "Attachments"
},
"randomAlias": "Random Alias",
"manual": "Manual",
"service": "Service",
@@ -345,11 +356,23 @@
"autofillSettings": "Autofill Settings",
"clipboardSettings": "Clipboard Settings",
"contextMenuSettings": "Context Menu Settings",
"passkeySettings": "Passkey Settings",
"contextMenu": "Context Menu",
"contextMenuEnabled": "Context menu is enabled",
"contextMenuDisabled": "Context menu is disabled",
"contextMenuDescription": "Right-click on input fields to access AliasVault options",
"selectLanguage": "Select Language",
"serverConfiguration": "Server Configuration",
"serverConfigurationDescription": "Configure the AliasVault server URL for self-hosted instances",
"customApiUrl": "API URL",
"customClientUrl": "Client URL",
"apiUrlHint": "The API endpoint URL (usually client URL + /api)",
"clientUrlHint": "The web interface URL of your self-hosted instance",
"autofillSettingsDescription": "Enable or disable the autofill popup on web pages",
"autofillEnabledDescription": "Autofill suggestions will appear on login forms",
"autofillDisabledDescription": "Autofill suggestions are disabled globally",
"languageSettings": "Language",
"languageSettingsDescription": "Choose your preferred language",
"validation": {
"apiUrlRequired": "API URL is required",
"apiUrlInvalid": "Please enter a valid API URL",
@@ -357,12 +380,52 @@
"clientUrlInvalid": "Please enter a valid client URL"
}
},
"passkeys": {
"passkey": "Passkey",
"site": "Site",
"displayName": "Name",
"helpText": "Passkeys are created on the website when prompted. They cannot be manually edited. To remove this passkey, you can delete it from this credential. To replace this passkey or create a new one, visit the website and follow its prompts.",
"passkeyMarkedForDeletion": "Passkey marked for deletion",
"passkeyWillBeDeleted": "This passkey will be deleted when you save this credential.",
"bypass": {
"title": "Use Browser Passkey",
"description": "How long would you like to use the browser's passkey provider for {{origin}}?",
"thisTimeOnly": "This time only",
"alwaysForSite": "Always for this site"
},
"authenticate": {
"title": "Sign in with Passkey",
"signInFor": "Sign in with passkey for",
"selectPasskey": "Select a passkey to sign in:",
"noPasskeysFound": "No passkeys found for this site",
"useBrowserPasskey": "Use Browser Passkey"
},
"create": {
"title": "Create Passkey",
"createFor": "Create a new passkey for",
"titleLabel": "Title",
"titlePlaceholder": "Enter a name for this passkey",
"createButton": "Create Passkey",
"creatingButton": "Creating...",
"useBrowserPasskey": "Use Browser Passkey",
"selectPasskeyToReplace": "Select a passkey to replace:",
"createNewPasskey": "Create New Passkey",
"replacingPasskey": "Replacing passkey: {{displayName}}",
"confirmReplace": "Confirm Replace"
},
"settings": {
"passkeyProvider": "Passkey Provider",
"passkeyProviderOn": "Passkey Provider on ",
"enable": "Enable AliasVault as passkey provider",
"description": "When enabled, AliasVault will handle passkey requests from websites. When a website requests a passkey, the AliasVault popup will be shown instead of the native browser or OS passkey interface."
}
},
"upgrade": {
"title": "Upgrade Vault",
"subtitle": "AliasVault has updated and your vault needs to be upgraded. This should only take a few seconds.",
"versionInformation": "Version Information",
"yourVault": "Your vault:",
"newVersion": "New version:",
"yourVault": "Your vault version:",
"newVersion": "New available version:",
"upgrade": "Upgrade Vault",
"upgrading": "Upgrading...",
"logout": "Logout",
@@ -386,8 +449,7 @@
"cancel": "Cancel",
"continueUpgrade": "Continue Upgrade",
"upgradeFailed": "Upgrade Failed",
"failedToApplyMigration": "Failed to apply migration ({{current}} of {{total}})",
"unknownErrorDuringUpgrade": "An unknown error occurred during the upgrade. Please try again."
"failedToApplyMigration": "Failed to apply migration ({{current}} of {{total}})"
}
}
}

View File

@@ -38,7 +38,7 @@
"wrongPassword": "Falsches Passwort. Bitte versuche es erneut.",
"accountLocked": "Das Konto wurde wegen zu vieler fehlgeschlagener Anmeldeversuche vorübergehend gesperrt.",
"networkError": "Netzwerkfehler. Bitte überprüfe Deine Verbindung und versuche es erneut.",
"loginDataMissing": "Deine Anmelde-Sitzung ist abgelaufen. Bitte versuche es erneut."
"sessionExpired": "Deine Sitzung ist abgelaufen. Bitte melde Dich erneut an."
}
},
"menu": {
@@ -52,8 +52,10 @@
"error": "Fehler",
"success": "Aktion erfolgreich",
"cancel": "Abbrechen",
"back": "Back",
"use": "Benutzen",
"delete": "Löschen",
"or": "Or",
"close": "Schließen",
"copied": "Kopiert!",
"openInNewWindow": "In neuem Fenster öffnen",
@@ -89,12 +91,11 @@
"executingOperation": "Vorgang wird ausgeführt...",
"loadMore": "Mehr laden",
"errors": {
"VaultOutdated": "Dein Tresor ist veraltet. Bitte melde Dich auf der AliasVault-Webseite an und folge den Anweisungen.",
"serverNotAvailable": "Der AliasVault-Server konnte nicht erreicht werden. Bitte versuche es später noch einmal oder kontaktiere den Support, falls das Problem weiterhin besteht.",
"clientVersionNotSupported": "Diese Version der AliasVault-Browser-Erweiterung wird vom Server nicht mehr unterstützt. Bitte aktualisiere Deine Browser-Erweiterung auf die neueste Version.",
"browserExtensionOutdated": "Diese Browser-Erweiterung ist veraltet und kann nicht verwendet werden, um auf diesen Tresor zuzugreifen. Bitte aktualisiere die Browser-Erweiterung, um fortzufahren.",
"serverVersionNotSupported": "Der AliasVault-Server muss auf eine neuere Version aktualisiert werden, um diese Browser-Erweiterung nutzen zu können. Bitte kontaktiere den Support, falls Du Hilfe benötigst.",
"unknownError": "Ein unbekannter Fehler ist aufgetreten",
"failedToStoreVault": "Fehler beim Speichern des Tresors",
"vaultNotAvailable": "Tresor nicht verfügbar",
"failedToRetrieveData": "Abruf der Daten fehlgeschlagen",
"vaultIsLocked": "Der Tresor ist gesperrt.",
@@ -198,6 +199,9 @@
"searchPlaceholder": "Zugangsdaten suchen...",
"welcomeTitle": "Willkommen bei AliasVault!",
"welcomeDescription": "Du möchtest die AliasVault-Browser-Erweiterung verwenden? Navigiere zu einer Website und verwende das AliasVault-Popup-Fenster um einen neuen Zugang zu erstellen.",
"noPasskeysFound": "No passkeys have been created yet. Passkeys are created by visiting a website that offers passkeys as an authentication method.",
"noAttachmentsFound": "No credentials with attachments found",
"noMatchingCredentials": "No matching credentials found",
"createdAt": "Erstellt",
"updatedAt": "Zuletzt aktualisiert",
"autofill": "Autofill",
@@ -213,6 +217,13 @@
"saveCredential": "Zugang speichern",
"deleteCredentialTitle": "Zugang löschen",
"deleteCredentialConfirm": "Bist Du sicher, dass Du diesen Zugang löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.",
"filters": {
"all": "(All) Credentials",
"passkeys": "Passkeys",
"aliases": "Aliases",
"userpass": "Passwords",
"attachments": "Attachments"
},
"randomAlias": "Zufälliger Alias",
"manual": "Manuell",
"service": "Dienst",
@@ -345,11 +356,23 @@
"autofillSettings": "Autofill-Einstellungen",
"clipboardSettings": "Zwischenablage-Einstellungen",
"contextMenuSettings": "Kontextmenü-Einstellungen",
"passkeySettings": "Passkey Settings",
"contextMenu": "Kontextmenü",
"contextMenuEnabled": "Kontextmenü ist aktiviert",
"contextMenuDisabled": "Kontextmenü ist deaktiviert",
"contextMenuDescription": "Rechtsklicke auf Eingabefelder, um auf AliasVault-Optionen zuzugreifen",
"selectLanguage": "Sprache auswählen",
"serverConfiguration": "Server Configuration",
"serverConfigurationDescription": "Configure the AliasVault server URL for self-hosted instances",
"customApiUrl": "API URL",
"customClientUrl": "Client URL",
"apiUrlHint": "The API endpoint URL (usually client URL + /api)",
"clientUrlHint": "The web interface URL of your self-hosted instance",
"autofillSettingsDescription": "Enable or disable the autofill popup on web pages",
"autofillEnabledDescription": "Autofill suggestions will appear on login forms",
"autofillDisabledDescription": "Autofill suggestions are disabled globally",
"languageSettings": "Language",
"languageSettingsDescription": "Choose your preferred language",
"validation": {
"apiUrlRequired": "API-URL ist erforderlich",
"apiUrlInvalid": "Bitte gib eine gültige API-URL ein",
@@ -357,12 +380,52 @@
"clientUrlInvalid": "Bitte gib eine gültige Client-URL ein"
}
},
"passkeys": {
"passkey": "Passkey",
"site": "Site",
"displayName": "Name",
"helpText": "Passkeys are created on the website when prompted. They cannot be manually edited. To remove this passkey, you can delete it from this credential. To replace this passkey or create a new one, visit the website and follow its prompts.",
"passkeyMarkedForDeletion": "Passkey marked for deletion",
"passkeyWillBeDeleted": "This passkey will be deleted when you save this credential.",
"bypass": {
"title": "Use Browser Passkey",
"description": "How long would you like to use the browser's passkey provider for {{origin}}?",
"thisTimeOnly": "This time only",
"alwaysForSite": "Always for this site"
},
"authenticate": {
"title": "Sign in with Passkey",
"signInFor": "Sign in with passkey for",
"selectPasskey": "Select a passkey to sign in:",
"noPasskeysFound": "No passkeys found for this site",
"useBrowserPasskey": "Use Browser Passkey"
},
"create": {
"title": "Create Passkey",
"createFor": "Create a new passkey for",
"titleLabel": "Title",
"titlePlaceholder": "Enter a name for this passkey",
"createButton": "Create Passkey",
"creatingButton": "Creating...",
"useBrowserPasskey": "Use Browser Passkey",
"selectPasskeyToReplace": "Select a passkey to replace:",
"createNewPasskey": "Create New Passkey",
"replacingPasskey": "Replacing passkey: {{displayName}}",
"confirmReplace": "Confirm Replace"
},
"settings": {
"passkeyProvider": "Passkey Provider",
"passkeyProviderOn": "Passkey Provider on ",
"enable": "Enable AliasVault as passkey provider",
"description": "When enabled, AliasVault will handle passkey requests from websites. When a website requests a passkey, the AliasVault popup will be shown instead of the native browser or OS passkey interface."
}
},
"upgrade": {
"title": "Tresor aktualisieren",
"subtitle": "AliasVault wurde aktualisiert. Dadurch muss auch Dein Tresor aktualisiert werden. Dies sollte nur wenige Sekunden dauern.",
"versionInformation": "Versionsinformationen",
"yourVault": "Dein Tresor:",
"newVersion": "Neue Version:",
"yourVault": "Version Deines Tresors:",
"newVersion": "Neue verfügbare Version:",
"upgrade": "Tresor aktualisieren",
"upgrading": "Aktualisieren...",
"logout": "Abmelden",
@@ -386,8 +449,7 @@
"cancel": "Abbrechen",
"continueUpgrade": "Aktualisierung fortsetzen",
"upgradeFailed": "Aktualisierung fehlgeschlagen",
"failedToApplyMigration": "Migration fehlgeschlagen ({{current}} von {{total}})",
"unknownErrorDuringUpgrade": "Bei der Aktualisierung ist ein unbekannter Fehler aufgetreten. Bitte versuche es erneut."
"failedToApplyMigration": "Migration fehlgeschlagen ({{current}} von {{total}})"
}
}
}

View File

@@ -38,7 +38,7 @@
"wrongPassword": "Incorrect password. Please try again.",
"accountLocked": "Account temporarily locked due to too many failed attempts.",
"networkError": "Network error. Please check your connection and try again.",
"loginDataMissing": "Login session expired. Please try again."
"sessionExpired": "Your session has expired. Please log in again."
}
},
"menu": {
@@ -52,8 +52,10 @@
"error": "Error",
"success": "Success",
"cancel": "Cancel",
"back": "Back",
"use": "Use",
"delete": "Delete",
"or": "Or",
"close": "Close",
"copied": "Copied!",
"openInNewWindow": "Open in new window",
@@ -89,12 +91,11 @@
"executingOperation": "Executing operation...",
"loadMore": "Load more",
"errors": {
"VaultOutdated": "Your vault is outdated. Please login on the AliasVault website and follow the steps.",
"serverNotAvailable": "The AliasVault server is not available. Please try again later or contact support if the problem persists.",
"clientVersionNotSupported": "This version of the AliasVault browser extension is not supported by the server anymore. Please update your browser extension to the latest version.",
"browserExtensionOutdated": "This browser extension is outdated and cannot be used to access this vault. Please update this browser extension to continue.",
"serverVersionNotSupported": "The AliasVault server needs to be updated to a newer version in order to use this browser extension. Please contact support if you need help.",
"unknownError": "An unknown error occurred",
"failedToStoreVault": "Failed to store vault",
"vaultNotAvailable": "Vault not available",
"failedToRetrieveData": "Failed to retrieve data",
"vaultIsLocked": "Vault is locked",
@@ -198,6 +199,9 @@
"searchPlaceholder": "Search credentials...",
"welcomeTitle": "Welcome to AliasVault!",
"welcomeDescription": "To use the AliasVault browser extension: navigate to a website and use the AliasVault autofill popup to create a new credential.",
"noPasskeysFound": "No passkeys have been created yet. Passkeys are created by visiting a website that offers passkeys as an authentication method.",
"noAttachmentsFound": "No credentials with attachments found",
"noMatchingCredentials": "No matching credentials found",
"createdAt": "Created",
"updatedAt": "Last updated",
"autofill": "Autofill",
@@ -213,6 +217,13 @@
"saveCredential": "Save credential",
"deleteCredentialTitle": "Delete Credential",
"deleteCredentialConfirm": "Are you sure you want to delete this credential? This action cannot be undone.",
"filters": {
"all": "(All) Credentials",
"passkeys": "Passkeys",
"aliases": "Aliases",
"userpass": "Passwords",
"attachments": "Attachments"
},
"randomAlias": "Random Alias",
"manual": "Manual",
"service": "Service",
@@ -345,11 +356,23 @@
"autofillSettings": "Autofill Settings",
"clipboardSettings": "Clipboard Settings",
"contextMenuSettings": "Context Menu Settings",
"passkeySettings": "Passkey Settings",
"contextMenu": "Context Menu",
"contextMenuEnabled": "Context menu is enabled",
"contextMenuDisabled": "Context menu is disabled",
"contextMenuDescription": "Right-click on input fields to access AliasVault options",
"selectLanguage": "Select Language",
"serverConfiguration": "Server Configuration",
"serverConfigurationDescription": "Configure the AliasVault server URL for self-hosted instances",
"customApiUrl": "API URL",
"customClientUrl": "Client URL",
"apiUrlHint": "The API endpoint URL (usually client URL + /api)",
"clientUrlHint": "The web interface URL of your self-hosted instance",
"autofillSettingsDescription": "Enable or disable the autofill popup on web pages",
"autofillEnabledDescription": "Autofill suggestions will appear on login forms",
"autofillDisabledDescription": "Autofill suggestions are disabled globally",
"languageSettings": "Language",
"languageSettingsDescription": "Choose your preferred language",
"validation": {
"apiUrlRequired": "API URL is required",
"apiUrlInvalid": "Please enter a valid API URL",
@@ -357,12 +380,52 @@
"clientUrlInvalid": "Please enter a valid client URL"
}
},
"passkeys": {
"passkey": "Passkey",
"site": "Site",
"displayName": "Name",
"helpText": "Passkeys are created on the website when prompted. They cannot be manually edited. To remove this passkey, you can delete it from this credential. To replace this passkey or create a new one, visit the website and follow its prompts.",
"passkeyMarkedForDeletion": "Passkey marked for deletion",
"passkeyWillBeDeleted": "This passkey will be deleted when you save this credential.",
"bypass": {
"title": "Use Browser Passkey",
"description": "How long would you like to use the browser's passkey provider for {{origin}}?",
"thisTimeOnly": "This time only",
"alwaysForSite": "Always for this site"
},
"authenticate": {
"title": "Sign in with Passkey",
"signInFor": "Sign in with passkey for",
"selectPasskey": "Select a passkey to sign in:",
"noPasskeysFound": "No passkeys found for this site",
"useBrowserPasskey": "Use Browser Passkey"
},
"create": {
"title": "Create Passkey",
"createFor": "Create a new passkey for",
"titleLabel": "Title",
"titlePlaceholder": "Enter a name for this passkey",
"createButton": "Create Passkey",
"creatingButton": "Creating...",
"useBrowserPasskey": "Use Browser Passkey",
"selectPasskeyToReplace": "Select a passkey to replace:",
"createNewPasskey": "Create New Passkey",
"replacingPasskey": "Replacing passkey: {{displayName}}",
"confirmReplace": "Confirm Replace"
},
"settings": {
"passkeyProvider": "Passkey Provider",
"passkeyProviderOn": "Passkey Provider on ",
"enable": "Enable AliasVault as passkey provider",
"description": "When enabled, AliasVault will handle passkey requests from websites. When a website requests a passkey, the AliasVault popup will be shown instead of the native browser or OS passkey interface."
}
},
"upgrade": {
"title": "Upgrade Vault",
"subtitle": "AliasVault has updated and your vault needs to be upgraded. This should only take a few seconds.",
"versionInformation": "Version Information",
"yourVault": "Your vault:",
"newVersion": "New version:",
"yourVault": "Your vault version:",
"newVersion": "New available version:",
"upgrade": "Upgrade Vault",
"upgrading": "Upgrading...",
"logout": "Logout",
@@ -386,8 +449,7 @@
"cancel": "Cancel",
"continueUpgrade": "Continue Upgrade",
"upgradeFailed": "Upgrade Failed",
"failedToApplyMigration": "Failed to apply migration ({{current}} of {{total}})",
"unknownErrorDuringUpgrade": "An unknown error occurred during the upgrade. Please try again."
"failedToApplyMigration": "Failed to apply migration ({{current}} of {{total}})"
}
}
}

View File

@@ -38,7 +38,7 @@
"wrongPassword": "Incorrect password. Please try again.",
"accountLocked": "Account temporarily locked due to too many failed attempts.",
"networkError": "Network error. Please check your connection and try again.",
"loginDataMissing": "Login session expired. Please try again."
"sessionExpired": "Your session has expired. Please log in again."
}
},
"menu": {
@@ -52,8 +52,10 @@
"error": "Error",
"success": "Success",
"cancel": "Cancel",
"back": "Back",
"use": "Use",
"delete": "Delete",
"or": "Or",
"close": "Close",
"copied": "Copied!",
"openInNewWindow": "Open in new window",
@@ -89,12 +91,11 @@
"executingOperation": "Executing operation...",
"loadMore": "Load more",
"errors": {
"VaultOutdated": "Your vault is outdated. Please login on the AliasVault website and follow the steps.",
"serverNotAvailable": "The AliasVault server is not available. Please try again later or contact support if the problem persists.",
"clientVersionNotSupported": "This version of the AliasVault browser extension is not supported by the server anymore. Please update your browser extension to the latest version.",
"browserExtensionOutdated": "This browser extension is outdated and cannot be used to access this vault. Please update this browser extension to continue.",
"serverVersionNotSupported": "The AliasVault server needs to be updated to a newer version in order to use this browser extension. Please contact support if you need help.",
"unknownError": "An unknown error occurred",
"failedToStoreVault": "Failed to store vault",
"vaultNotAvailable": "Vault not available",
"failedToRetrieveData": "Failed to retrieve data",
"vaultIsLocked": "Vault is locked",
@@ -198,6 +199,9 @@
"searchPlaceholder": "Search credentials...",
"welcomeTitle": "Welcome to AliasVault!",
"welcomeDescription": "To use the AliasVault browser extension: navigate to a website and use the AliasVault autofill popup to create a new credential.",
"noPasskeysFound": "No passkeys have been created yet. Passkeys are created by visiting a website that offers passkeys as an authentication method.",
"noAttachmentsFound": "No credentials with attachments found",
"noMatchingCredentials": "No matching credentials found",
"createdAt": "Created",
"updatedAt": "Last updated",
"autofill": "Autofill",
@@ -213,6 +217,13 @@
"saveCredential": "Save credential",
"deleteCredentialTitle": "Delete Credential",
"deleteCredentialConfirm": "Are you sure you want to delete this credential? This action cannot be undone.",
"filters": {
"all": "(All) Credentials",
"passkeys": "Passkeys",
"aliases": "Aliases",
"userpass": "Passwords",
"attachments": "Attachments"
},
"randomAlias": "Random Alias",
"manual": "Manual",
"service": "Service",
@@ -345,11 +356,23 @@
"autofillSettings": "Autofill Settings",
"clipboardSettings": "Clipboard Settings",
"contextMenuSettings": "Context Menu Settings",
"passkeySettings": "Passkey Settings",
"contextMenu": "Context Menu",
"contextMenuEnabled": "Context menu is enabled",
"contextMenuDisabled": "Context menu is disabled",
"contextMenuDescription": "Right-click on input fields to access AliasVault options",
"selectLanguage": "Select Language",
"serverConfiguration": "Server Configuration",
"serverConfigurationDescription": "Configure the AliasVault server URL for self-hosted instances",
"customApiUrl": "API URL",
"customClientUrl": "Client URL",
"apiUrlHint": "The API endpoint URL (usually client URL + /api)",
"clientUrlHint": "The web interface URL of your self-hosted instance",
"autofillSettingsDescription": "Enable or disable the autofill popup on web pages",
"autofillEnabledDescription": "Autofill suggestions will appear on login forms",
"autofillDisabledDescription": "Autofill suggestions are disabled globally",
"languageSettings": "Language",
"languageSettingsDescription": "Choose your preferred language",
"validation": {
"apiUrlRequired": "API URL is required",
"apiUrlInvalid": "Please enter a valid API URL",
@@ -357,12 +380,52 @@
"clientUrlInvalid": "Please enter a valid client URL"
}
},
"passkeys": {
"passkey": "Passkey",
"site": "Site",
"displayName": "Name",
"helpText": "Passkeys are created on the website when prompted. They cannot be manually edited. To remove this passkey, you can delete it from this credential. To replace this passkey or create a new one, visit the website and follow its prompts.",
"passkeyMarkedForDeletion": "Passkey marked for deletion",
"passkeyWillBeDeleted": "This passkey will be deleted when you save this credential.",
"bypass": {
"title": "Use Browser Passkey",
"description": "How long would you like to use the browser's passkey provider for {{origin}}?",
"thisTimeOnly": "This time only",
"alwaysForSite": "Always for this site"
},
"authenticate": {
"title": "Sign in with Passkey",
"signInFor": "Sign in with passkey for",
"selectPasskey": "Select a passkey to sign in:",
"noPasskeysFound": "No passkeys found for this site",
"useBrowserPasskey": "Use Browser Passkey"
},
"create": {
"title": "Create Passkey",
"createFor": "Create a new passkey for",
"titleLabel": "Title",
"titlePlaceholder": "Enter a name for this passkey",
"createButton": "Create Passkey",
"creatingButton": "Creating...",
"useBrowserPasskey": "Use Browser Passkey",
"selectPasskeyToReplace": "Select a passkey to replace:",
"createNewPasskey": "Create New Passkey",
"replacingPasskey": "Replacing passkey: {{displayName}}",
"confirmReplace": "Confirm Replace"
},
"settings": {
"passkeyProvider": "Passkey Provider",
"passkeyProviderOn": "Passkey Provider on ",
"enable": "Enable AliasVault as passkey provider",
"description": "When enabled, AliasVault will handle passkey requests from websites. When a website requests a passkey, the AliasVault popup will be shown instead of the native browser or OS passkey interface."
}
},
"upgrade": {
"title": "Upgrade Vault",
"subtitle": "AliasVault has updated and your vault needs to be upgraded. This should only take a few seconds.",
"versionInformation": "Version Information",
"yourVault": "Your vault:",
"newVersion": "New version:",
"yourVault": "Your vault version:",
"newVersion": "New available version:",
"upgrade": "Upgrade Vault",
"upgrading": "Upgrading...",
"logout": "Logout",
@@ -386,8 +449,7 @@
"cancel": "Cancel",
"continueUpgrade": "Continue Upgrade",
"upgradeFailed": "Upgrade Failed",
"failedToApplyMigration": "Failed to apply migration ({{current}} of {{total}})",
"unknownErrorDuringUpgrade": "An unknown error occurred during the upgrade. Please try again."
"failedToApplyMigration": "Failed to apply migration ({{current}} of {{total}})"
}
}
}

View File

@@ -6,43 +6,43 @@
"password": "Salasana",
"passwordPlaceholder": "Syötä salasanasi",
"rememberMe": "Muista minut",
"loginButton": "Kirjaudu",
"noAccount": "Eikö sinulla ole vielä tiliä?",
"loginButton": "Kirjaudu sisään",
"noAccount": "Eikö tiliä vielä ole?",
"createVault": "Luo uusi holvi",
"twoFactorTitle": "Ole hyvä ja syötä tunnistautumiskoodi tunnistautumissovelluksestasi.",
"twoFactorTitle": "Syötä tunnistautumiskoodi tunnistautumissovelluksestasi.",
"authCode": "Tunnistautumiskoodi",
"authCodePlaceholder": "Syötä 6-numeroinen koodi",
"verify": "Vahvista",
"cancel": "Peruuta",
"twoFactorNote": "Huomautus: jos sinulla ei ole pääsyä tunnistautumislaitteeseen, voit palauttaa 2FA:n palautuskoodilla kirjautumalla sisään sivuston kautta.",
"masterPassword": "Pääsalasana",
"unlockVault": "Avaa holvi",
"unlockTitle": "Avaa Holvisi",
"unlockVault": "Avaa holvin lukitus",
"unlockTitle": "Avaa holvisi lukitus",
"unlockDescription": "Syötä pääsalasanasi avataksesi holvisi lukituksen.",
"logout": "Kirjaudu ulos",
"logout": "Uloskirjautuminen",
"logoutConfirm": "Oletko varma, että haluat kirjautua ulos?",
"sessionExpired": "Istuntosi on vanhentunut. Ole hyvä ja kirjaudu uudelleen.",
"unlockSuccess": "Holvi avattu onnistuneesti!",
"unlockSuccessTitle": "Holvisi lukitus on onnistuneesti avattu",
"unlockSuccessDescription": "Voit nyt käyttää selaimessasi olevia kirjautumislomakkeita automaattisesti.",
"sessionExpired": "Istuntosi on vanhentunut. Kirjaudu sisään uudelleen.",
"unlockSuccess": "Holvin lukitus avattu!",
"unlockSuccessTitle": "Holvisi lukitus on avattu",
"unlockSuccessDescription": "Voit nyt käyttää automaattista täyttöä sisäänkirjautumislomakkeissa selaimessasi.",
"closePopup": "Sulje tämä ponnahdusikkuna",
"browseVault": "Selaa holvin sisältöä",
"connectingTo": "Yhdistetään palvelimeen",
"connectingTo": "Yhdistetään kohteeseen",
"switchAccounts": "Vaihdetaanko tiliä?",
"loggedIn": "Kirjautuneena",
"loggedIn": "Sisäänkirjautuneena",
"errors": {
"invalidCode": "Anna kelvollinen 6-numeroinen tunnistautumiskoodi.",
"invalidCode": "Syötä kelvollinen 6-numeroinen tunnistautumiskoodi.",
"serverError": "AliasVault-palvelimeen ei saatu yhteyttä. Yritä myöhemmin uudelleen tai ota yhteyttä tukeen, jos ongelma jatkuu.",
"noToken": "Kirjautuminen epäonnistui -- tunnusta ei palautettu",
"noToken": "Sisäänkirjautuminen epäonnistui -- polettia ei palautettu",
"migrationError": "Tapahtui virhe tarkistettaessa odottavia siirtoja.",
"wrongPassword": "Virheellinen salasana. Yritä uudelleen.",
"accountLocked": "Tili on tilapäisesti lukittu liian monen epäonnistuneen yrityksen vuoksi. Yritä myöhemmin uudelleen.",
"accountLocked": "Tili tilapäisesti lukittu liian monen epäonnistuneen yrityksen vuoksi.",
"networkError": "Verkkovirhe: tarkista yhteytesi ja yritä uudelleen.",
"loginDataMissing": "Kirjautumisistunto on vanhentunut. Yritä uudelleen."
"sessionExpired": "Istuntosi on vanhentunut. Kirjaudu sisään uudelleen."
}
},
"menu": {
"credentials": "Käyttäjätunnukset",
"credentials": "Tunnistetiedot",
"emails": "Sähköpostit",
"settings": "Asetukset"
},
@@ -52,13 +52,15 @@
"error": "Virhe",
"success": "Onnistui",
"cancel": "Peruuta",
"back": "Takaisin",
"use": "Käytä",
"delete": "Poista",
"or": "Tai",
"close": "Sulje",
"copied": "Kopioitu!",
"openInNewWindow": "Avaa uudessa ikkunassa",
"language": "Kieli",
"enabled": "Käytössä",
"enabled": "Otettu käyttöön",
"disabled": "Pois käytöstä",
"showPassword": "Näytä salasana",
"hidePassword": "Piilota salasana",
@@ -68,11 +70,11 @@
"attachments": "Liitteet",
"loadingAttachments": "Ladataan liitteitä...",
"settings": "Asetukset",
"recentEmails": "Viimeisimmät sähköpostit",
"loginCredentials": "Sisäänkirjautumistiedot",
"recentEmails": "Viimeaikaiset sähköpostit",
"loginCredentials": "Sisäänkirjautumistunnistetiedot",
"twoFactorAuthentication": "Kaksivaiheinen tunnistautuminen",
"alias": "Alias",
"notes": "Muistiinpanot",
"notes": "Huomautukset",
"fullName": "Koko nimi",
"firstName": "Etunimi",
"lastName": "Sukunimi",
@@ -83,49 +85,48 @@
"password": "Salasana",
"syncingVault": "Synkronoidaan holvia",
"savingChangesToVault": "Tallennetaan muutoksia holviin",
"uploadingVaultToServer": "Lähetetään holvi palvelimelle",
"checkingVaultUpdates": "Tarkistetaan holvin päivityksiä",
"uploadingVaultToServer": "Ladataan holvi palvelimelle",
"checkingVaultUpdates": "Tarkistetaan holvin päivitysten varalta",
"syncingUpdatedVault": "Synkronoidaan päivitettyä holvia",
"executingOperation": "Suoritetaan toimintoa...",
"loadMore": "Lataa lisää",
"errors": {
"VaultOutdated": "Holvisi on vanhentunut. Kirjaudu AliasVaultin kotisivulle ja noudata ohjeita.",
"serverNotAvailable": "AliasVault-palvelin ei ole käytettävissä. Yritä myöhemmin uudelleen tai ota yhteyttä tukeen, jos ongelma jatkuu.",
"clientVersionNotSupported": "Palvelin ei enää tue tätä AliasVault-selainlaajennuksen versiota. Ole hyvä ja päivitä selaimen laajennus uusimpaan versioon.",
"clientVersionNotSupported": "Palvelin ei enää tue tätä AliasVault-selainlaajennuksen versiota. Päivitä selaimen laajennus uusimpaan versioon.",
"browserExtensionOutdated": "Tämä selainlaajennus on vanhentunut, eikä sillä voi saada pääsyä tähän holviin. Päivitä tämä selainlaajennus jatkaaksesi.",
"serverVersionNotSupported": "AliasVault-palvelin on päivitettävä uudempaan versioon, jotta voit käyttää tätä selainlaajennusta. Ota yhteyttä tukeen, jos tarvitset apua.",
"unknownError": "Tapahtui tuntematon virhe",
"failedToStoreVault": "Holvin tallentaminen epäonnistui",
"vaultNotAvailable": "Holvi ei ole käytettävissä",
"vaultNotAvailable": "Holvi ei käytettävissä",
"failedToRetrieveData": "Tietojen nouto epäonnistui",
"vaultIsLocked": "Holvi on lukittu",
"failedToUploadVault": "Holvin lataaminen epäonnistui",
"passwordChanged": "Salasanasi on muuttunut edellisen kirjautumisen jälkeen. Ole hyvä ja kirjaudu uudelleen turvallisuussyistä."
"failedToUploadVault": "Holvin ulospäinlataaminen epäonnistui",
"passwordChanged": "Salasanasi on muuttunut edellisen sisäänkirjautumisen jälkeen. Kirjaudu sisään uudelleen turvallisuussyistä."
},
"apiErrors": {
"UNKNOWN_ERROR": "Tapahtui tuntematon virhe. Yritä uudelleen.",
"ACCOUNT_LOCKED": "Tili on tilapäisesti lukittu liian monen epäonnistuneen yrityksen vuoksi. Yritä myöhemmin uudelleen.",
"ACCOUNT_LOCKED": "Tili tilapäisesti lukittu liian monen epäonnistuneen yrityksen vuoksi. Yritä myöhemmin uudelleen.",
"ACCOUNT_BLOCKED": "Tilisi on poistettu käytöstä. Jos uskot, että tämä on virhe, ota yhteyttä tukeen.",
"USER_NOT_FOUND": "Virheellinen käyttäjänimi tai salasana. Yritä uudelleen.",
"INVALID_AUTHENTICATOR_CODE": "Virheellinen tunnistautumiskoodi. Yritä uudelleen.",
"INVALID_RECOVERY_CODE": "Virheellinen palautuskoodi. Yritä uudelleen.",
"REFRESH_TOKEN_REQUIRED": "Päivitysavain vaaditaan.",
"INVALID_REFRESH_TOKEN": "Virheellinen päivitysavain.",
"REFRESH_TOKEN_REVOKED_SUCCESSFULLY": "Päivitysavain peruutettu onnistuneesti.",
"PUBLIC_REGISTRATION_DISABLED": "Uuden tilin rekisteröinti on poistettu käytöstä tällä palvelimella. Ota yhteyttä järjestelmänvalvojaan.",
"REFRESH_TOKEN_REQUIRED": "Virkistyspoletti vaaditaan.",
"INVALID_REFRESH_TOKEN": "Virheellinen virkistyspoletti.",
"REFRESH_TOKEN_REVOKED_SUCCESSFULLY": "Virkistyspoletti peruutettu.",
"PUBLIC_REGISTRATION_DISABLED": "Uuden tilin rekisteröinti on tällä hetkellä poistettu käytöstä tällä palvelimella. Ota yhteyttä järjestelmänvalvojaan.",
"USERNAME_REQUIRED": "Käyttäjänimi vaaditaan.",
"USERNAME_ALREADY_IN_USE": "Käyttäjätunnus on jo käytössä",
"USERNAME_ALREADY_IN_USE": "Käyttäjänimi on jo käytössä.",
"USERNAME_AVAILABLE": "Käyttäjänimi on saatavilla.",
"USERNAME_MISMATCH": "Käyttäjänimi ei vastaa nykyistä käyttäjää.",
"PASSWORD_MISMATCH": "Annettu salasana ei vastaa nykyistä salasanaasi.",
"ACCOUNT_SUCCESSFULLY_DELETED": "Käyttäjätili onnistuneesti poistettu,.",
"USERNAME_EMPTY_OR_WHITESPACE": "Käyttäjätunnus ei voi olla tyhjä.",
"USERNAME_TOO_SHORT": "Käyttäjätunnus on liian lyhyt: sen on oltava vähintään 3 merkkiä pitkä.",
"USERNAME_TOO_LONG": "Käyttäjätunnus on liian pitkä: se voi olla enintään 40 merkkiä.",
"ACCOUNT_SUCCESSFULLY_DELETED": "Tili poistettu.",
"USERNAME_EMPTY_OR_WHITESPACE": "Käyttäjänimi ei voi olla tyhjä eikä siinä voi olla välilyöntejä.",
"USERNAME_TOO_SHORT": "Käyttäjänimi liian lyhyt: sen on oltava vähintään 3 merkkiä pitkä.",
"USERNAME_TOO_LONG": "Käyttäjänimi liian pitkä: ei saa olla yli 40 merkkiä pidempi.",
"USERNAME_INVALID_EMAIL": "Virheellinen sähköpostiosoite.",
"USERNAME_INVALID_CHARACTERS": "Käyttäjätunnus on virheellinen, voi sisältää vain kirjaimia tai numeroita.",
"USERNAME_INVALID_CHARACTERS": "Käyttäjänimi on virheellinen, voi sisältää vain kirjaimia tai numeroita.",
"VAULT_NOT_UP_TO_DATE": "Holvisi ei ole ajan tasalla. Synkronoi holvisi ja yritä uudelleen.",
"INTERNAL_SERVER_ERROR": "Sisäinen palvelinvirhe.",
"VAULT_ERROR": "Paikallinen holvi ei ole ajan tasalla. Synkronoi holvisi päivittämällä sivu ja yritä uudelleen."
"VAULT_ERROR": "Paikallinen holvi ei ole ajan tasalla. Synkronoi holvisi virkistämällä sivu ja yritä uudelleen."
}
},
"content": {
@@ -135,7 +136,7 @@
"search": "Etsi",
"vaultLocked": "AliasVault on lukittu.",
"creatingNewAlias": "Luodaan uutta aliasta...",
"noMatchesFound": "Hakutuloksia ei löytynyt",
"noMatchesFound": "Osumia ei löytynyt",
"searchVault": "Etsi holvi...",
"serviceName": "Palvelun nimi",
"email": "Sähköposti",
@@ -144,24 +145,24 @@
"enterServiceName": "Syötä palvelun nimi",
"enterEmailAddress": "Syötä sähköpostiosoite",
"enterUsername": "Syötä käyttäjänimi",
"hideFor1Hour": "Piilota 1 tunniksi (nykyinen sivusto)",
"hideFor1Hour": "Piilota tunnin ajan (nykyinen sivusto)",
"hidePermanently": "Piilota pysyvästi (nykyinen sivu)",
"createRandomAlias": "Luo sattumanvarainen alias",
"createUsernamePassword": "Luo käyttäjänimi/salasana",
"randomAlias": "Sattumanvarainen alias",
"usernamePassword": "Käyttäjänimi/Salasana",
"createAndSaveAlias": "Luo ja tallenna alias",
"createAndSaveCredential": "Luo ja tallenna käyttäjätunnus",
"createAndSaveCredential": "Luo ja tallenna tunnistetieto",
"randomIdentityDescription": "Luo satunnainen identiteetti, jolla on satunnainen sähköpostiosoite, johon on pääsy AliasVaultissa.",
"randomIdentityDescriptionDropdown": "Satunnainen identiteetti satunnaisella sähköpostiosoitteella",
"randomIdentityDescriptionDropdown": "Satunnainen identiteetti satunnaisen sähköpostiosoitteen kanssa",
"manualCredentialDescription": "Määritä oma sähköpostiosoitteesi ja käyttäjänimesi.",
"manualCredentialDescriptionDropdown": "Manuaalinen käyttäjänimi ja salasana",
"failedToCreateIdentity": "Henkilöllisyyden luonti epäonnistui. Yritä uudelleen.",
"failedToCreateIdentity": "Identiteetin luominen epäonnistui. Yritä uudelleen.",
"enterEmailAndOrUsername": "Syötä sähköpostiosoite ja/tai käyttäjänimi",
"autofillWithAliasVault": "Automaattinen täyttö AliasVaultilla",
"generateRandomPassword": "Luo sattumanvarainen salasana (kopioi leikepöydälle)",
"generateNewPassword": "Luo uusi salasana",
"togglePasswordVisibility": "Vaihda salasanan näkyvyyttä",
"togglePasswordVisibility": "Salasanan näkyvyyden päälle/pois päältä kytkeminen",
"passwordCopiedToClipboard": "Salasana kopioitu leikepöydälle",
"enterEmailAndOrUsernameError": "Syötä sähköpostiosoite ja/tai käyttäjänimi",
"openAliasVaultToUpgrade": "Avaa AliasVault päivittääksesi",
@@ -169,13 +170,13 @@
"dismissPopup": "Hylkää ponnahdusikkuna"
},
"credentials": {
"title": "Käyttäjätunnukset",
"addCredential": "Lisää käyttäjätunnus",
"editCredential": "Muokkaa käyttäjätunnusta",
"deleteCredential": "Poista käyttäjätunnus",
"credentialDetails": "Käyttäjätunnuksen tiedot",
"title": "Tunnistetiedot",
"addCredential": "Lisää tunnistetieto",
"editCredential": "Muokkaa tunnistetietoa",
"deleteCredential": "Poista tunnistetieto",
"credentialDetails": "Tunnistetietojen yksityiskohdat",
"serviceName": "Palvelun nimi",
"serviceNamePlaceholder": "esim. Gmail, Facebook, Pankki",
"serviceNamePlaceholder": "esim. Gmail, Facebook, pankki",
"website": "Verkkosivusto",
"websitePlaceholder": "https://esimerkki.fi",
"username": "Käyttäjänimi",
@@ -186,39 +187,49 @@
"copyPassword": "Kopioi salasana",
"showPassword": "Näytä salasana",
"hidePassword": "Piilota salasana",
"notes": "Muistiinpanot",
"notesPlaceholder": "Muut huomautukset...",
"notes": "Huomautukset",
"notesPlaceholder": "Lisähuomautukset...",
"totp": "Kaksivaiheinen tunnistautuminen",
"totpCode": "TOTP koodi",
"totpCode": "TOTP-koodi",
"copyTotp": "Kopioi TOTP-koodi",
"totpSecret": "TOTP Salaus",
"totpSecretPlaceholder": "Syötä TOTP salainen avain",
"noCredentials": "Käyttäjätunnuksia ei löytynyt",
"noCredentialsDescription": "Lisää ensimmäinen käyttäjätunnuksesi aloittaaksesi",
"searchPlaceholder": "Etsi käyttäjätunnuksia...",
"totpSecret": "TOTP-salaisuus",
"totpSecretPlaceholder": "Syötä TOTP-salainen avain",
"noCredentials": "Tunnistetietoja ei löytynyt",
"noCredentialsDescription": "Lisää ensimmäinen tunnistetietosi aloittaaksesi",
"searchPlaceholder": "Etsi tunnistetietoja...",
"welcomeTitle": "Tervetuloa AliasVaultiin!",
"welcomeDescription": "Käyttääksesi AliasVault-selainlaajennusta: Siirry sivustolle ja käytä AliasVaultin automaattisen täytön ponnahdusikkunaa luodaksesi uuden käyttäjätunnuksen.",
"welcomeDescription": "Käyttääksesi AliasVault-selainlaajennusta, siirry jollekin verkkosivustolle ja käytä AliasVaultin automaattisen täytön ponnahdusikkunaa luodaksesi uuden tunnistetiedon.",
"noPasskeysFound": "Todennusavaimia, Passkey ei ole vielä luotu. Todennusavaimet on luotu vierailemalla verkkosivustolla, joka tarjoaa todennusavaimia todennusmenetelmänä.",
"noAttachmentsFound": "Tunnuksia liitteiden kanssa ei löytynyt",
"noMatchingCredentials": "Vastaavia tunnistetietoja ei löytynyt",
"createdAt": "Luotu",
"updatedAt": "Viimeksi päivitetty",
"autofill": "Automaattinen täyttö",
"fillForm": "Täytä lomake",
"deleteConfirm": "Oletko varma, että haluat poistaa tämän käyttäjätunnuksen?",
"saveSuccess": "Käyttäjätunnus tallennettu onnistuneesti.",
"deleteConfirm": "Oletko varma, että haluat poistaa tämän tunnistetiedon?",
"saveSuccess": "Tunnistetieto tallennettu",
"tags": "Tunnisteet",
"addTag": "Lisää tunniste",
"removeTag": "Poista tunniste",
"folder": "Kansio",
"selectFolder": "Valitse kansio",
"createFolder": "Luo kansio",
"saveCredential": "Tallenna käyttäjätunnus",
"deleteCredentialTitle": "Poista käyttäjätunnus",
"deleteCredentialConfirm": "Oletko varma, että haluat poistaa tämän tunnuksen? Tätä toimintoa ei voi perua.",
"randomAlias": "Sattumanvarainen Alias",
"saveCredential": "Tallenna tunnistetieto",
"deleteCredentialTitle": "Poista tunnistetieto",
"deleteCredentialConfirm": "Oletko varma, että haluat poistaa tämän tunnistetiedon? Tätä toimintoa ei voi perua.",
"filters": {
"all": "(All) Käyttäjätunnukset",
"passkeys": "Sala-avaimet",
"aliases": "Aliakset",
"userpass": "Salasanat",
"attachments": "Liitteet"
},
"randomAlias": "Sattumanvarainen alias",
"manual": "Käyttöopas",
"service": "Palvelu",
"serviceUrl": "Palvelun URL-osoite",
"loginCredentials": "Sisäänkirjautumistiedot",
"generateRandomUsername": "Luo sattumanvarainen käyttäjätunnus",
"loginCredentials": "Sisäänkirjautumistunnistetiedot",
"generateRandomUsername": "Luo sattumanvarainen käyttäjänimi",
"generateRandomPassword": "Luo sattumanvarainen salasana",
"changePasswordComplexity": "Muuta salasanan monimutkaisuutta",
"passwordLength": "Salasanan pituus",
@@ -226,10 +237,10 @@
"includeUppercase": "Sisällytä isot kirjaimet",
"includeNumbers": "Sisällytä numerot",
"includeSpecialChars": "Sisällytä erikoismerkit",
"avoidAmbiguousChars": "Vältä epäselviä merkkejä (o, 0 jne.)",
"avoidAmbiguousChars": "Vältä epäselviä merkkejä (o, 0, jne.)",
"generateNewPreview": "Luo uusi esikatselu",
"generateRandomAlias": "Luo sattumanvarainen alias",
"clearAliasFields": "Tyhjennä aliaksen kentät",
"clearAliasFields": "Tyhjennä alias-kentät",
"alias": "Alias",
"firstName": "Etunimi",
"lastName": "Sukunimi",
@@ -246,18 +257,18 @@
},
"privateEmailTitle": "Yksityinen sähköposti",
"privateEmailAliasVaultServer": "AliasVault-palvelin",
"privateEmailDescription": "E2E salattu, täysin yksityinen.",
"publicEmailTitle": "Julkiset väliaikaisen sähköpostiosoitteen tarjoajat",
"publicEmailDescription": "Anonyymi mutta rajoitettu yksityisyys. Käytettävissä kaikille, jotka tuntevat osoitteen.",
"useDomainChooser": "Käytä verkkotunnuksen valintaa",
"enterCustomDomain": "Anna oma verkkotunnus",
"enterFullEmail": "Syötä täysi sähköpostiosoite",
"privateEmailDescription": "Päästä päähän (E2E) salattu, täysin yksityinen.",
"publicEmailTitle": "Julkiset väliaikaisten sähköpostiosoitteiden tarjoajat (PTEP)",
"publicEmailDescription": "Anonyymi, mutta rajoitettu yksityisyys. Sähköpostin sisällön voi lukea kuka tahansa, joka tietää osoitteen.",
"useDomainChooser": "Käytä verkkotunnuksen valitsijaa",
"enterCustomDomain": "Syötä mukautettu verkkotunnus",
"enterFullEmail": "Syötä koko sähköpostiosoite",
"enterEmailPrefix": "Syötä sähköpostin etuliite"
},
"emails": {
"title": "Sähköpostit",
"deleteEmailTitle": "Poista sähköposti",
"deleteEmailConfirm": "Oletko varma, että haluat poistaa tämän kuvan pysyvästi?",
"deleteEmailConfirm": "Oletko varma, että haluat poistaa tämän sähköpostin pysyvästi?",
"from": "Lähettäjä",
"to": "Vastaanottaja",
"date": "Päivämäärä",
@@ -270,16 +281,16 @@
"justNow": "juuri nyt",
"minutesAgo_single": "{{count}} min sitten",
"minutesAgo_plural": "{{count}} minuuttia sitten",
"hoursAgo_single": "{{count}} h sitten",
"hoursAgo_single": "{{count}} tunti sitten",
"hoursAgo_plural": "{{count}} tuntia sitten",
"yesterday": "eilen"
},
"errors": {
"emailLoadError": "Sähköpostien lataamisessa tapahtui virhe. Yritä myöhemmin uudelleen.",
"emailUnexpectedError": "Odottamaton virhe sähköpostien latauksen aikana. Yritä myöhemmin uudelleen."
"emailUnexpectedError": "Odottamaton virhe sähköpostien lataamisen aikana. Yritä myöhemmin uudelleen."
},
"apiErrors": {
"CLAIM_DOES_NOT_MATCH_USER": "Nykyinen valittu sähköpostiosoite on jo käytössä. Ole hyvä ja vaihda sähköpostiosoite muokkaamalla tätä tunnusta.",
"CLAIM_DOES_NOT_MATCH_USER": "Nykyinen valittu sähköpostiosoite on jo käytössä. Vaihda sähköpostiosoite muokkaamalla tätä tunnistetietoa.",
"CLAIM_DOES_NOT_EXIST": "Tapahtui virhe ladattaessa sähköposteja. Yritä muokata ja tallentaa tunnistetiedot synkronoidaksesi tietokannan, ja yritä sitten uudelleen."
}
},
@@ -287,49 +298,49 @@
"title": "Asetukset",
"serverUrl": "Palvelimen URL-osoite",
"language": "Kieli",
"autofillEnabled": "Ota automaattitäyttö käyttöön",
"autofillEnabled": "Ota automaattinen täyttö käyttöön",
"version": "Versio",
"openInNewWindow": "Avaa uudessa ikkunassa",
"openWebApp": "Avaa verkkosovellus",
"loggedIn": "Kirjautuneena",
"logout": "Kirjaudu ulos",
"globalSettings": "Yleiset asetukset",
"loggedIn": "Sisäänkirjautuneena",
"logout": "Uloskirjautuminen",
"globalSettings": "Yleisesti pätevät asetukset",
"autofillPopup": "Automaattisen täytön ponnahdusikkuna",
"activeOnAllSites": "Aktiivinen kaikilla sivustoilla (paitsi jos pois päältä alla)",
"disabledOnAllSites": "Poistettu käytöstä kaikilla sivustoilla",
"enabled": "Käytössä",
"activeOnAllSites": "Aktiivisena kaikilla sivustoilla (ellei sitä ole poistettu käytöstä alla)",
"disabledOnAllSites": "Pois käytöstä kaikilla sivustoilla",
"enabled": "Otettu käyttöön",
"disabled": "Pois käytöstä",
"rightClickContextMenu": "Oikea-klikkauksen kontekstivalikko",
"autofillMatching": "Autofill osuma",
"autofillMatchingMode": "Autofill osumat käytössä",
"autofillMatchingModeDescription": "Määrittää mitkä käyttäjätunnukset katsotaan osumaksi ja näytetään automaattisen täytön ponnahdusikkunan ehdotuksina tietylle sivustolle.",
"autofillMatchingDefault": "URL + alitoimialue + nimi jokerimerkki",
"autofillMatchingUrlSubdomain": "URL + alitoimialue",
"rightClickContextMenu": "Oikea-napsauta kontekstivalikkoa",
"autofillMatching": "Automaattisen täytön täsmäytys",
"autofillMatchingMode": "Automaattisen täytön täsmäytystila",
"autofillMatchingModeDescription": "Määrittää, mitkä tunnistetiedot katsotaan osumiksi ja näytetään ehdotuksina tietyn verkkosivuston automaattisen täytön ponnahdusikkunassa.",
"autofillMatchingDefault": "URL + aliverkkotunnus + nimi jokerimerkki",
"autofillMatchingUrlSubdomain": "URL + aliverkkotunnus",
"autofillMatchingUrlExact": "Tarkka URL-verkkotunnus vain",
"siteSpecificSettings": "Sivukohtaiset asetukset",
"siteSpecificSettings": "Sivustokohtaiset asetukset",
"autofillPopupOn": "Automaattisen täytön ponnahdusikkuna päällä: ",
"enabledForThisSite": "Käytössä tällä sivustolla",
"disabledForThisSite": "Ei käytössä tällä sivustolla",
"temporarilyDisabledUntil": "Tilapäisesti pois päältä ",
"enabledForThisSite": "Otettu käyttöön tällä sivustolla",
"disabledForThisSite": "Poistettu käytöstä tällä sivustolla",
"temporarilyDisabledUntil": "Väliaikaisesti pois käytöstä, kunnes ",
"resetAllSiteSettings": "Nollaa kaikki sivustokohtaiset asetukset",
"appearance": "Ulkoasu",
"theme": "Teema",
"useDefault": "Käytä oletusta",
"light": "Vaalea",
"dark": "Tumma",
"keyboardShortcuts": "Pikanäppäimet",
"configureKeyboardShortcuts": "Määritä pikanäppäimet",
"keyboardShortcuts": "Näppäimistön pikanppäimet",
"configureKeyboardShortcuts": "Määritä näppäimistön pikanäppäimet",
"configure": "Määritä",
"security": "Tietoturva",
"clipboardClearTimeout": "Tyhjennä leikepöytä kopioinnin jälkeen",
"clipboardClearTimeoutDescription": "Tyhjennä leikepöytä automaattisesti arkaluonteisten tietojen kopioinnin jälkeen",
"clipboardClearDisabled": "Älä tyhjennä koskaan",
"clipboardClearTimeout": "Tyhjennä leikepöytä kopioimisen jälkeen",
"clipboardClearTimeoutDescription": "Tyhjennä leikepöytä automaattisesti arkaluonteisten tietojen kopioimisen jälkeen",
"clipboardClearDisabled": "Älä koskaan tyhjennä",
"clipboardClear5Seconds": "Tyhjennä 5 sekunnin jälkeen",
"clipboardClear10Seconds": "Tyhjennä 10 sekunnin jälkeen",
"clipboardClear15Seconds": "Tyhjennä 15 sekunnin jälkeen",
"autoLockTimeout": "Automaattisen lukituksen aikakatkaisu",
"autoLockTimeoutDescription": "Lukitse holvi automaattisesti käyttämättä jäämisen jälkeen",
"autoLockTimeoutHelp": "Holvi lukittuu vain määritellyn käyttöajan jälkeen (ei automaattisen täytön käyttöä tai laajennuksen ponnahdusikkunaa auki). Holvi lukittuu aina, kun selain on suljettu, tästä asetuksesta riippumatta.",
"autoLockTimeoutDescription": "Lukitse holvi automaattisesti käyttämättömyysjakson jälkeen",
"autoLockTimeoutHelp": "Holvi lukittuu vasta määritetyn käyttämättömyysjakson jälkeen (ei automaattista täyttöä tai laajennusten ponnahdusikkunoita ole avattu). Holvi lukittuu aina, kun selain suljetaan, tästä asetuksesta riippumatta.",
"autoLockNever": "Ei koskaan",
"autoLock15Seconds": "15 sekuntia",
"autoLock1Minute": "1 minuutti",
@@ -344,50 +355,101 @@
"preferences": "Määritykset",
"autofillSettings": "Automaatisen täytön asetukset",
"clipboardSettings": "Leikepöydän asetukset",
"contextMenuSettings": "Sisältövalikon asetukset",
"contextMenuSettings": "Kontekstivalikon asetukset",
"passkeySettings": "Todennusavainten asetukset",
"contextMenu": "Sisältövalikko",
"contextMenuEnabled": "Sisältövalikko käytössä",
"contextMenuDisabled": "Sisältövalikko pois käytöstä",
"contextMenuDescription": "Napsauta syöttökenttiä hiiren kakkospainikkeella päästäksesi käsiksi AliasVaultin valintoihin",
"contextMenuEnabled": "Kontekstivalikko on otettu käyttöön",
"contextMenuDisabled": "Kontekstivalikko on poistettu käytöstä",
"contextMenuDescription": "Oikea-napsauta syöttökenttiä päästäksesi AliasVaultin vaihtoehtoihin",
"selectLanguage": "Valitse kieli",
"serverConfiguration": "Palvelimen asetukset",
"serverConfigurationDescription": "Määritä AliasVault-palvelimen URL-osoite itse isännöityille instanssille",
"customApiUrl": "API-URL-osoite",
"customClientUrl": "Asiakas-URL-osoite",
"apiUrlHint": "API päätepisteen URL (yleensä asiakkaan URL + /api)",
"clientUrlHint": "Web-käyttöliittymän URL-osoite itse isännöidyssä instanssissa",
"autofillSettingsDescription": "Ota käyttöön tai poista käytöstä automaattisen täytön ponnahdusikkuna verkkosivuilta",
"autofillEnabledDescription": "Automaattisen täytön ehdotukset näkyvät kirjautumislomakkeissa",
"autofillDisabledDescription": "Automaattitäyttöehdotukset on poistettu käytöstä kaikkialla",
"languageSettings": "Keili",
"languageSettingsDescription": "Valitse ensisijainen kieli",
"validation": {
"apiUrlRequired": "API URL-osoite vaaditaan",
"apiUrlInvalid": "Anna kelvollinen API URL-osoite",
"apiUrlRequired": "API-URL-osoite vaaditaan",
"apiUrlInvalid": "Syötä kelvollinen API-URL-osoite",
"clientUrlRequired": "Asiakkaan URL-osoite vaaditaan",
"clientUrlInvalid": "Anna kelvollinen asiakkaan URL-osoite"
"clientUrlInvalid": "Syötä kelvollinen asiakas-URL-osoite"
}
},
"passkeys": {
"passkey": "Sala-avain",
"site": "Sivusto",
"displayName": "Nimi",
"helpText": "Todennusavaimet, Passkeys, luodaan sivustolle pyydettäessä. Niitä ei voi manuaalisesti muokata. Voit poistaa tämän salasanan, voit poistaa sen tästä käyttäjätunnuksesta. Voit korvata tämän salasanan tai luoda uuden, käy verkkosivuilla ja seuraa sen kehotuksia.",
"passkeyMarkedForDeletion": "Todennusavain merkitty poistettavaksi",
"passkeyWillBeDeleted": "Tämä todennusavain poistetaan, kun tallennat tämän käyttäjätiedon.",
"bypass": {
"title": "Käytä selaimen sala-avainta",
"description": "Kuinka kauan haluat käyttää selaimen todennusavaimen tarjoajaa {{origin}}?",
"thisTimeOnly": "Vain tällä kertaa",
"alwaysForSite": "Aina tällä sivustolla"
},
"authenticate": {
"title": "Kirjaudu sisään todennusavaimella",
"signInFor": "Kirjaudu sisään todennusavaimella saadaksesi",
"selectPasskey": "Valitse todennusavain jolla kirjaudutaan",
"noPasskeysFound": "Tällä sivustolla ei löytynyt todennusavaimia",
"useBrowserPasskey": "Käytä selaimen todennusavainta"
},
"create": {
"title": "Luo Passkey, todennusavain",
"createFor": "Luo uusi Passkey, todennusavain",
"titleLabel": "Otsikko",
"titlePlaceholder": "Anna nimi tälle todennusavaimelle",
"createButton": "Luo todennusavain, Passkey",
"creatingButton": "Luodaan...",
"useBrowserPasskey": "Käytä selaimen sala-avainta",
"selectPasskeyToReplace": "Valitse todennusavain, johon korvataan",
"createNewPasskey": "Luo uusi sala-avain",
"replacingPasskey": "Korvaava todennusavain: {{displayName}}",
"confirmReplace": "Vahvista korvaaminen"
},
"settings": {
"passkeyProvider": "Todennusavaimen toimittaja",
"passkeyProviderOn": "Todennusavaimen toimittaja käytössä ",
"enable": "Ota AliasVault käyttöön todennusavainten tarjoajana",
"description": "Kun AliasVault on käytössä, se käsittelee todennusavaimia verkkosivustoilta. Kun sivusto pyytää todennusavainta, AliasVaultin ponnahdusikkuna näytetään natiivin selaimen tai käyttöjärjestelmän todennusavaimen sijaan."
}
},
"upgrade": {
"title": "Päivitä holvi",
"subtitle": "AliasVault on päivitetty ja holvisi on päivitettävä. Tämän pitäisi kestää vain muutama sekunti.",
"versionInformation": "Versiotiedot",
"yourVault": "Sinun holvisi:",
"newVersion": "Uusi versio:",
"upgrade": "Päivitä Holvi",
"yourVault": "Holvisi versio:",
"newVersion": "Uusi saatavilla oleva versio:",
"upgrade": "Päivitä holvi",
"upgrading": "Päivitetään...",
"logout": "Kirjaudu ulos",
"logout": "Uloskirjautuminen",
"whatsNew": "Mitä uutta?",
"whatsNewDescription": "Päivitys on tarpeen, jotta voidaan tukea seuraavia muutoksia:",
"noDescriptionAvailable": "Kuvausta ei ole saatavilla tälle versiolle.",
"okay": "Ok",
"whatsNewDescription": "Päivitys vaaditaan seuraavien muutosten tukemiseksi:",
"noDescriptionAvailable": "Tälle versiolle ei ole saatavilla kuvausta.",
"okay": "Hyvä on",
"status": {
"preparingUpgrade": "Valmistellaan päivityksiä...",
"preparingUpgrade": "Valmistellaan päivitystä...",
"vaultAlreadyUpToDate": "Holvi on jo ajan tasalla",
"startingDatabaseTransaction": "Aloitetaan tietokannan siirtoa...",
"startingDatabaseTransaction": "Aloitetaan tietokannan transaktiota...",
"applyingDatabaseMigrations": "Toteutetaan tietokannan siirtoja...",
"applyingMigration": "Siirretään tietoja: {{current}} / {{total}}...",
"committingChanges": "Suoritetaan muutoksia..."
"applyingMigration": "Otetaan siirto käyttöön {{current}} / {{total}}...",
"committingChanges": "Otetaan muutokset käyttöön..."
},
"alerts": {
"error": "Virhe",
"unableToGetVersionInfo": "Versiotietoja ei löytynyt. Yritä uudelleen.",
"selfHostedServer": "Itsehallinnoitu palvelin",
"selfHostedWarning": "Jos käytät itsehallintoitua palvelina, varmista myös että päivität itsehallinnoidun palvelimesi, jos muutoin kirjautuminen web-asiakkaan kautta lakkaa toimimasta.",
"unableToGetVersionInfo": "Versiotietojen hakeminen epäonnistui. Yritä uudelleen.",
"selfHostedServer": "Itseisännöity palvelin",
"selfHostedWarning": "Jos käytät itseisännöityä palvelinta, muista päivittää myös itseisännöity instanssisi, koska muuten verkkoasiakassovellukseen kirjautuminen lakkaa toimimasta.",
"cancel": "Peruuta",
"continueUpgrade": "Jatka päivitystä",
"upgradeFailed": "Päivitys epäonnistui",
"failedToApplyMigration": "Tietojen siirto epäonnistui {{current}} / {{total}} ",
"unknownErrorDuringUpgrade": "Päivityksen aikana tapahtui tuntematon virhe. Yritä uudelleen."
"failedToApplyMigration": "Siirron käyttöönotto epäonnistui ({{current}} / {{total}})"
}
}
}

View File

@@ -22,7 +22,7 @@
"logout": "Se déconnecter",
"logoutConfirm": "Êtes-vous sûr de vouloir vous déconnecter ?",
"sessionExpired": "Votre session a expiré. Veuillez vous reconnecter.",
"unlockSuccess": "Parcourir le contenu du coffre",
"unlockSuccess": "Parcourir le contenu du coffre !",
"unlockSuccessTitle": "Votre coffre a été déverrouillé avec succès",
"unlockSuccessDescription": "Vous pouvez maintenant utiliser le remplissage automatique des formulaires de connexion dans votre navigateur.",
"closePopup": "Fermer cette popup",
@@ -38,7 +38,7 @@
"wrongPassword": "Mot de passe incorrect, veuillez réessayer.",
"accountLocked": "Compte temporairement verrouillé en raison d'un trop grand nombre de tentatives échouées.",
"networkError": "Erreur réseau. Vérifiez votre connexion et réessayez.",
"loginDataMissing": "La session a expiré. Veuillez réessayer."
"sessionExpired": "Votre session a expiré. Veuillez vous reconnecter."
}
},
"menu": {
@@ -52,8 +52,10 @@
"error": "Erreur",
"success": "Succès",
"cancel": "Annuler",
"back": "Back",
"use": "Utiliser",
"delete": "Supprimer",
"or": "Or",
"close": "Fermer",
"copied": "Copié !",
"openInNewWindow": "Ouvrir dans une nouvelle fenêtre",
@@ -89,12 +91,11 @@
"executingOperation": "Exécution de l'opération...",
"loadMore": "Voir plus",
"errors": {
"VaultOutdated": "Votre coffre est obsolète. Veuillez vous connecter sur le site AliasVault et suivre les étapes.",
"serverNotAvailable": "Le serveur d'AliasVault n'est pas disponible. Veuillez réessayer plus tard ou contacter le support si le problème persiste.",
"clientVersionNotSupported": "Cette version de l'extension de navigateur AliasVault n'est plus prise en charge par le serveur. Veuillez mettre à jour votre extension de navigateur à la dernière version.",
"browserExtensionOutdated": "Cette extension de navigateur est obsolète et ne peut pas être utilisée pour accéder à ce coffre-fort. Veuillez la mettre à jour pour continuer.",
"serverVersionNotSupported": "Le serveur d'AliasVault doit être mis à jour vers une version plus récente afin d'utiliser cette extension de navigateur. Veuillez contacter le support si vous avez besoin d'aide.",
"unknownError": "Une erreur inconnue s'est produite",
"failedToStoreVault": "Échec du stockage du coffre",
"vaultNotAvailable": "Coffre non disponible",
"failedToRetrieveData": "Échec de la récupération des données",
"vaultIsLocked": "Le coffre est verrouillé",
@@ -198,6 +199,9 @@
"searchPlaceholder": "Rechercher des identifiants...",
"welcomeTitle": "Bienvenue dans AliasVault !",
"welcomeDescription": "Pour utiliser l'extension de navigateur AliasVault : accédez à un site web et utilisez la fenêtre de saisie automatique AliasVault pour créer un nouvel identifiant.",
"noPasskeysFound": "No passkeys have been created yet. Passkeys are created by visiting a website that offers passkeys as an authentication method.",
"noAttachmentsFound": "No credentials with attachments found",
"noMatchingCredentials": "No matching credentials found",
"createdAt": "Créé",
"updatedAt": "Dernière mise à jour",
"autofill": "Remplissage automatique",
@@ -213,6 +217,13 @@
"saveCredential": "Enregistrer les identifiants",
"deleteCredentialTitle": "Supprimer les identifiants",
"deleteCredentialConfirm": "Êtes-vous sûr de vouloir supprimer ces identifiants ? Cette action est irréversible.",
"filters": {
"all": "(All) Credentials",
"passkeys": "Passkeys",
"aliases": "Aliases",
"userpass": "Passwords",
"attachments": "Attachments"
},
"randomAlias": "Alias aléatoire",
"manual": "Manuel",
"service": "Service",
@@ -229,7 +240,7 @@
"avoidAmbiguousChars": "Éviter les caractères ambigus (o, 0, etc.)",
"generateNewPreview": "Générer un nouvel aperçu",
"generateRandomAlias": "Créer un alias aléatoire",
"clearAliasFields": "Clear Alias Fields",
"clearAliasFields": "Effacer les champs d'alias",
"alias": "Alias",
"firstName": "Prénom",
"lastName": "Nom",
@@ -345,11 +356,23 @@
"autofillSettings": "Paramètres du remplissage automatique",
"clipboardSettings": "Paramètres du presse-papiers",
"contextMenuSettings": "Paramètres du menu contextuel",
"passkeySettings": "Passkey Settings",
"contextMenu": "Menu contextuel",
"contextMenuEnabled": "Le menu contextuel est activé",
"contextMenuDisabled": "Le menu contextuel est désactivé",
"contextMenuDescription": "Faites un clic droit sur les champs de saisie pour accéder aux options d'AliasVault",
"selectLanguage": "Sélectionner une langue",
"serverConfiguration": "Server Configuration",
"serverConfigurationDescription": "Configure the AliasVault server URL for self-hosted instances",
"customApiUrl": "API URL",
"customClientUrl": "Client URL",
"apiUrlHint": "The API endpoint URL (usually client URL + /api)",
"clientUrlHint": "The web interface URL of your self-hosted instance",
"autofillSettingsDescription": "Enable or disable the autofill popup on web pages",
"autofillEnabledDescription": "Autofill suggestions will appear on login forms",
"autofillDisabledDescription": "Autofill suggestions are disabled globally",
"languageSettings": "Language",
"languageSettingsDescription": "Choose your preferred language",
"validation": {
"apiUrlRequired": "L'URL de l'API est requise",
"apiUrlInvalid": "Veuillez entrer une URL d'API valide",
@@ -357,12 +380,52 @@
"clientUrlInvalid": "Veuillez entrer une URL de client valide"
}
},
"passkeys": {
"passkey": "Passkey",
"site": "Site",
"displayName": "Name",
"helpText": "Passkeys are created on the website when prompted. They cannot be manually edited. To remove this passkey, you can delete it from this credential. To replace this passkey or create a new one, visit the website and follow its prompts.",
"passkeyMarkedForDeletion": "Passkey marked for deletion",
"passkeyWillBeDeleted": "This passkey will be deleted when you save this credential.",
"bypass": {
"title": "Use Browser Passkey",
"description": "How long would you like to use the browser's passkey provider for {{origin}}?",
"thisTimeOnly": "This time only",
"alwaysForSite": "Always for this site"
},
"authenticate": {
"title": "Sign in with Passkey",
"signInFor": "Sign in with passkey for",
"selectPasskey": "Select a passkey to sign in:",
"noPasskeysFound": "No passkeys found for this site",
"useBrowserPasskey": "Use Browser Passkey"
},
"create": {
"title": "Create Passkey",
"createFor": "Create a new passkey for",
"titleLabel": "Title",
"titlePlaceholder": "Enter a name for this passkey",
"createButton": "Create Passkey",
"creatingButton": "Creating...",
"useBrowserPasskey": "Use Browser Passkey",
"selectPasskeyToReplace": "Select a passkey to replace:",
"createNewPasskey": "Create New Passkey",
"replacingPasskey": "Replacing passkey: {{displayName}}",
"confirmReplace": "Confirm Replace"
},
"settings": {
"passkeyProvider": "Passkey Provider",
"passkeyProviderOn": "Passkey Provider on ",
"enable": "Enable AliasVault as passkey provider",
"description": "When enabled, AliasVault will handle passkey requests from websites. When a website requests a passkey, the AliasVault popup will be shown instead of the native browser or OS passkey interface."
}
},
"upgrade": {
"title": "Mettre à niveau le coffre",
"subtitle": "AliasVault a mis à jour et votre coffre doit être mis à niveau. Cela ne devrait prendre que quelques secondes.",
"versionInformation": "Informations de version",
"yourVault": "Votre coffre :",
"newVersion": "Nouvelle version :",
"yourVault": "Votre version de coffre-fort :",
"newVersion": "Nouvelle version valable :",
"upgrade": "Mettre le coffre à niveau",
"upgrading": "Mise à niveau...",
"logout": "Se déconnecter",
@@ -386,8 +449,7 @@
"cancel": "Annuler",
"continueUpgrade": "Continuer la mise à jour",
"upgradeFailed": "Échec de la mise à niveau",
"failedToApplyMigration": "Impossible d'appliquer la migration ({{current}} sur {{total}})",
"unknownErrorDuringUpgrade": "Une erreur inconnue s'est produite pendant la mise à niveau. Veuillez réessayer."
"failedToApplyMigration": "Impossible d'appliquer la migration ({{current}} sur {{total}})"
}
}
}

View File

@@ -38,7 +38,7 @@
"wrongPassword": "סיסמה שגויה. נא לנסות שוב.",
"accountLocked": "החשבון נעול זמנית עקב ריבוי ניסיונות כושלים.",
"networkError": "שגיאת רשת. נא לבדוק את החיבור ולנסות שוב.",
"loginDataMissing": "תוקף ההפעלה שלך פג. נא לנסות שוב."
"sessionExpired": "תוקף ההפעלה שלך פג. נא להיכנס מחדש."
}
},
"menu": {
@@ -52,8 +52,10 @@
"error": "שגיאה",
"success": "הצליח",
"cancel": "ביטול",
"back": "חזרה",
"use": "להשתמש",
"delete": "מחיקה",
"or": "או",
"close": "סגירה",
"copied": "הועתק!",
"openInNewWindow": "פתיחה בחלון חדש",
@@ -89,12 +91,11 @@
"executingOperation": "הפעולה רצה…",
"loadMore": "לטעון עוד",
"errors": {
"VaultOutdated": "הכספת שלך לא עדכנית. נא להיכנס לאתר AliasVault ולעקוב אחר ההנחיות.",
"serverNotAvailable": "שרת ה־AliasVault לא זמין. נא לנסות שוב מאוחר יותר או ליצור קשר עם התמיכה אם הבעיה נשנית.",
"clientVersionNotSupported": "הגרסה הזאת של הרחבת הדפדפן של AliasVault לא נתמכת עוד על ידי השרת. נא לעדכן את הרחבת הדפדפן שלך לגרסה העדכנית ביותר.",
"browserExtensionOutdated": "הרחבת הדפדפן הזאת לא עדכנית ואי אפשר להשתמש בה כדי לגשת לכספת הזאת. נא לעדכן את הרחבת הדפדפן הזאת כדי להמשיך.",
"serverVersionNotSupported": "יש לעדכן את שרת AliasVault לגרסה חדשה יותר כדי להשתמש בהרחבת הדפדפן הזאת. נא ליצור קשר עם התמיכה לקבלת עזרה.",
"unknownError": "אירעה שגיאה לא ידועה",
"failedToStoreVault": "אחסון הכספת נכשל",
"vaultNotAvailable": "הכספת לא זמינה",
"failedToRetrieveData": "משיכת הנתונים נכשלה",
"vaultIsLocked": "הכספת נעולה",
@@ -198,6 +199,9 @@
"searchPlaceholder": "חיפוש פרטי גישה…",
"welcomeTitle": "ברוך בואך ל־AliasVault!",
"welcomeDescription": "כדי להשתמש בהרחבת הדפדפן של AliasVault: יש לנווט לאתר ולהשתמש בחלונית ההשלמה האוטומטית של AliasVault כדי ליצור פרטי גישה חדשים.",
"noPasskeysFound": "No passkeys have been created yet. Passkeys are created by visiting a website that offers passkeys as an authentication method.",
"noAttachmentsFound": "No credentials with attachments found",
"noMatchingCredentials": "No matching credentials found",
"createdAt": "יצירה",
"updatedAt": "עדכון אחרון",
"autofill": "השלמה אוטומטית",
@@ -213,6 +217,13 @@
"saveCredential": "שמירת פרטי גישה",
"deleteCredentialTitle": "מחיקת פרטי גישה",
"deleteCredentialConfirm": "למחוק את פרטי הגישה? זאת פעולה בלתי הפיכה.",
"filters": {
"all": "(כל) פרטי הגישה",
"passkeys": "Passkeys",
"aliases": "כינויים",
"userpass": "סיסמאות",
"attachments": "Attachments"
},
"randomAlias": "כינוי אקראי",
"manual": "ידני",
"service": "שירות",
@@ -229,7 +240,7 @@
"avoidAmbiguousChars": "עדיף להימנע מאותיות וספרות שדומים זה לזה (o, 0 וכו׳)",
"generateNewPreview": "יצירת תצוגה מקדימה חדשה",
"generateRandomAlias": "יצירת כינוי אקראי",
"clearAliasFields": "Clear Alias Fields",
"clearAliasFields": "לפנות שדות כינויים",
"alias": "כינוי",
"firstName": "שם פרטי",
"lastName": "שם משפחה",
@@ -345,11 +356,23 @@
"autofillSettings": "הגדרות השלמה אוטומטית",
"clipboardSettings": "הגדרות לוח הגזירים",
"contextMenuSettings": "הגדרות תפריט הקשר",
"passkeySettings": "Passkey Settings",
"contextMenu": "תפריט הקשר",
"contextMenuEnabled": "תפריט הקשר פעיל",
"contextMenuDisabled": "תפריט הקשר כבוי",
"contextMenuDescription": "ניתן ללחוץ על שדה עם הלחצן הימני כדי לגשת לאפשרויות AliasVault",
"selectLanguage": "בחירת שפה",
"serverConfiguration": "Server Configuration",
"serverConfigurationDescription": "Configure the AliasVault server URL for self-hosted instances",
"customApiUrl": "כתובת API",
"customClientUrl": "כתובת לקוח",
"apiUrlHint": "The API endpoint URL (usually client URL + /api)",
"clientUrlHint": "The web interface URL of your self-hosted instance",
"autofillSettingsDescription": "Enable or disable the autofill popup on web pages",
"autofillEnabledDescription": "Autofill suggestions will appear on login forms",
"autofillDisabledDescription": "Autofill suggestions are disabled globally",
"languageSettings": "Language",
"languageSettingsDescription": "Choose your preferred language",
"validation": {
"apiUrlRequired": "כתובת API חובה",
"apiUrlInvalid": "נא למלא כתובת API תקפה",
@@ -357,12 +380,52 @@
"clientUrlInvalid": "נא למלא כתובת לקוח תקפה"
}
},
"passkeys": {
"passkey": "Passkey",
"site": "אתר",
"displayName": "שם",
"helpText": "Passkeys are created on the website when prompted. They cannot be manually edited. To remove this passkey, you can delete it from this credential. To replace this passkey or create a new one, visit the website and follow its prompts.",
"passkeyMarkedForDeletion": "Passkey marked for deletion",
"passkeyWillBeDeleted": "This passkey will be deleted when you save this credential.",
"bypass": {
"title": "Use Browser Passkey",
"description": "How long would you like to use the browser's passkey provider for {{origin}}?",
"thisTimeOnly": "בפעם הזאת בלבד",
"alwaysForSite": "תמיד לאתר הזה"
},
"authenticate": {
"title": "Sign in with Passkey",
"signInFor": "Sign in with passkey for",
"selectPasskey": "Select a passkey to sign in:",
"noPasskeysFound": "No passkeys found for this site",
"useBrowserPasskey": "Use Browser Passkey"
},
"create": {
"title": "Create Passkey",
"createFor": "Create a new passkey for",
"titleLabel": "כותרת",
"titlePlaceholder": "Enter a name for this passkey",
"createButton": "Create Passkey",
"creatingButton": "נוצר…",
"useBrowserPasskey": "Use Browser Passkey",
"selectPasskeyToReplace": "Select a passkey to replace:",
"createNewPasskey": "Create New Passkey",
"replacingPasskey": "Replacing passkey: {{displayName}}",
"confirmReplace": "אישור החלפה"
},
"settings": {
"passkeyProvider": "Passkey Provider",
"passkeyProviderOn": "Passkey Provider on ",
"enable": "Enable AliasVault as passkey provider",
"description": "When enabled, AliasVault will handle passkey requests from websites. When a website requests a passkey, the AliasVault popup will be shown instead of the native browser or OS passkey interface."
}
},
"upgrade": {
"title": "שדרוג כספת",
"subtitle": "AliasVault התעדכן וצריך לשדרג את הכספת שלך. הפעולה הזאת אמורה לארוך מספר שניות.",
"versionInformation": "פרטי גרסה",
"yourVault": "הכספת שלך:",
"newVersion": "גרסה חדשה:",
"yourVault": "גרסת הכספת שלך:",
"newVersion": "הגרסה החדשה הזמינה:",
"upgrade": "שדרוג כספת",
"upgrading": "משתדרגת…",
"logout": "יציאה",
@@ -386,8 +449,7 @@
"cancel": "ביטול",
"continueUpgrade": "להמשיך בשדרוג",
"upgradeFailed": "השדרוג נכשל",
"failedToApplyMigration": "החלת ההסבה נכשלה ({{current}} מתוך {{total}})",
"unknownErrorDuringUpgrade": "אירעה שגיאה בלתי ידועה במהלך השדרוג. נא לנסות שוב."
"failedToApplyMigration": "החלת ההסבה נכשלה ({{current}} מתוך {{total}})"
}
}
}

View File

@@ -38,7 +38,7 @@
"wrongPassword": "Password non corretta. Riprova nuovamente.",
"accountLocked": "Account temporaneamente bloccato a causa di troppi tentativi falliti.",
"networkError": "Errore di rete: Controlla la tua connessione e riprova.",
"loginDataMissing": "Sessione di accesso scaduta. Effettua nuovamente l'accesso."
"sessionExpired": "La tua sessione è scaduta. Effettua di nuovo il login."
}
},
"menu": {
@@ -52,8 +52,10 @@
"error": "Errore",
"success": "Riuscito",
"cancel": "Annulla",
"back": "Indietro",
"use": "Usa",
"delete": "Elimina",
"or": "O",
"close": "Chiudi",
"copied": "Copiato!",
"openInNewWindow": "Apri in una nuova finestra",
@@ -89,12 +91,11 @@
"executingOperation": "Esecuzione operazione...",
"loadMore": "Carica altro",
"errors": {
"VaultOutdated": "La tua cassaforte è obsoleta. Per favore accedi al sito di AliasVault e segui le istruzioni.",
"serverNotAvailable": "Il server di AliasVault non è disponibile. Riprova più tardi o contatta il supporto se il problema persiste.",
"clientVersionNotSupported": "Questa versione dell'estensione del browser AliasVault non è più supportata dal server. Aggiorna l'estensione alla versione più recente.",
"browserExtensionOutdated": "Questa estensione del browser è obsoleta e non può essere utilizzata per accedere a questa cassaforte. Si prega di aggiornare questa estensione per continuare.",
"serverVersionNotSupported": "Il server di AliasVault necessita un aggiornamento a una versione più recente per poter usare questa estensione. Contatta il supporto se hai bisogno di assistenza.",
"unknownError": "Si è verificato un errore sconosciuto",
"failedToStoreVault": "Salvataggio cassaforte non riuscito",
"vaultNotAvailable": "Cassaforte non disponibile",
"failedToRetrieveData": "Recupero dati non riuscito",
"vaultIsLocked": "La cassaforte è bloccata",
@@ -198,6 +199,9 @@
"searchPlaceholder": "Cerca credenziali...",
"welcomeTitle": "Benvenuto in AliasVault!",
"welcomeDescription": "Per usare l'estensione browser AliasVault: naviga su un sito e usa la finestra di compilazione automatica per creare una nuova credenziale.",
"noPasskeysFound": "Non sono state ancora create chiavi di accesso. Le passkey vengono create visitando un sito web che offre le chiavi di accesso come metodo di autenticazione.",
"noAttachmentsFound": "No credentials with attachments found",
"noMatchingCredentials": "Nessuna credenziale corrispondente trovata",
"createdAt": "Creato",
"updatedAt": "Ultimo aggiornamento",
"autofill": "Compilazione automatica",
@@ -213,6 +217,13 @@
"saveCredential": "Salva credenziale",
"deleteCredentialTitle": "Elimina credenziale",
"deleteCredentialConfirm": "Sei sicuro di voler eliminare queste credenziali? Questa azione non può essere annullata.",
"filters": {
"all": "(Tutte) Credenziali",
"passkeys": "Passkey",
"aliases": "Alias",
"userpass": "Password",
"attachments": "Attachments"
},
"randomAlias": "Alias casuale",
"manual": "Manuale",
"service": "Servizio",
@@ -345,11 +356,23 @@
"autofillSettings": "Impostazioni di riempimento automatico",
"clipboardSettings": "Impostazioni appunti",
"contextMenuSettings": "Preferenze menu contestuale",
"passkeySettings": "Impostazioni Passkey",
"contextMenu": "Menu contestuale",
"contextMenuEnabled": "Il menu contestuale è attivato",
"contextMenuDisabled": "Il menu contestuale è disabilitato",
"contextMenuDescription": "Click destro sui campi di input per accedere alle opzioni di AliasVault",
"selectLanguage": "Seleziona la lingua",
"serverConfiguration": "Server Configuration",
"serverConfigurationDescription": "Configure the AliasVault server URL for self-hosted instances",
"customApiUrl": "API URL",
"customClientUrl": "Client URL",
"apiUrlHint": "The API endpoint URL (usually client URL + /api)",
"clientUrlHint": "The web interface URL of your self-hosted instance",
"autofillSettingsDescription": "Enable or disable the autofill popup on web pages",
"autofillEnabledDescription": "Autofill suggestions will appear on login forms",
"autofillDisabledDescription": "Autofill suggestions are disabled globally",
"languageSettings": "Language",
"languageSettingsDescription": "Choose your preferred language",
"validation": {
"apiUrlRequired": "L'URL API è obbligatorio",
"apiUrlInvalid": "Inserisci un URL API valido",
@@ -357,12 +380,52 @@
"clientUrlInvalid": "Inserisci un URL del client valido"
}
},
"passkeys": {
"passkey": "Passkey",
"site": "Sito",
"displayName": "Nome",
"helpText": "Le chiavi di accesso vengono create sul sito web quando richiesto. Non possono essere modificate manualmente. Per rimuovere questa chiave di accesso, è possibile eliminarla da questa credenziale. Per sostituire questa passkey o crearne una nuova, visitare il sito web e seguire i relativi suggerimenti.",
"passkeyMarkedForDeletion": "Passkey contrassegnata per l'eliminazione",
"passkeyWillBeDeleted": "Questa passkey verrà eliminata quando si salva questa credenziale.",
"bypass": {
"title": "Usa Browser Passkey",
"description": "Per quanto tempo vorresti usare il provider di chiavi di accesso del browser per {{origin}}?",
"thisTimeOnly": "Solo questa volta",
"alwaysForSite": "Sempre per questo sito"
},
"authenticate": {
"title": "Accedi con Passkey",
"signInFor": "Accedi con passkey per",
"selectPasskey": "Selezionare una passkey per accedere:",
"noPasskeysFound": "Nessuna passkey trovata per questo sito",
"useBrowserPasskey": "Usa Browser Passkey"
},
"create": {
"title": "Crea Passkey",
"createFor": "Crea una nuova passkey per",
"titleLabel": "Titolo",
"titlePlaceholder": "Inserisci un nome per questa passkey",
"createButton": "Crea Passkey",
"creatingButton": "Creazione in corso...",
"useBrowserPasskey": "Usa Browser Passkey",
"selectPasskeyToReplace": "Selezionare una chiave di accesso da sostituire:",
"createNewPasskey": "Crea Nuova Passkey",
"replacingPasskey": "Sostituzione passkey: {{displayName}}",
"confirmReplace": "Conferma la sostituzione"
},
"settings": {
"passkeyProvider": "Provider Passkey",
"passkeyProviderOn": "Passkey Provider attivo",
"enable": "Abilita AliasVault come provider di passkey",
"description": "Quando abilitato, AliasVault gestirà le richieste di passkey dai siti web. Quando un sito web richiede una passkey, verrà mostrato il popup di AliasVault invece dell'interfaccia nativa del browser o dell'interfaccia di password del sistema operativo."
}
},
"upgrade": {
"title": "Aggiorna Cassaforte",
"subtitle": "AliasVault è stato aggiornato e la tua cassaforte deve essere aggiornata. Dovrebbe richiedere solo pochi secondi.",
"versionInformation": "Informazioni sulla versione",
"yourVault": "La tua cassaforte:",
"newVersion": "Nuova versione:",
"yourVault": "Versione della tua cassaforte:",
"newVersion": "Nuova versione disponibile:",
"upgrade": "Aggiorna cassaforte",
"upgrading": "Aggiornamento in corso...",
"logout": "Disconnetti",
@@ -386,8 +449,7 @@
"cancel": "Annulla",
"continueUpgrade": "Continua aggiornamento",
"upgradeFailed": "Aggiornamento non riuscito",
"failedToApplyMigration": "Impossibile eseguire la migrazione ({{current}} di {{total}})",
"unknownErrorDuringUpgrade": "Si è verificato un errore sconosciuto durante l'aggiornamento. Riprova."
"failedToApplyMigration": "Impossibile eseguire la migrazione ({{current}} di {{total}})"
}
}
}

View File

@@ -38,7 +38,7 @@
"wrongPassword": "Onjuist wachtwoord. Probeer het opnieuw.",
"accountLocked": "Account tijdelijk vergrendeld vanwege te veel mislukte pogingen.",
"networkError": "Netwerkfout. Controleer de verbinding en probeer het opnieuw.",
"loginDataMissing": "Sessie verlopen. Probeer het opnieuw."
"sessionExpired": "Je sessie is verlopen. Log opnieuw in."
}
},
"menu": {
@@ -52,8 +52,10 @@
"error": "Fout",
"success": "Succes",
"cancel": "Annuleren",
"back": "Terug",
"use": "Gebruik",
"delete": "Verwijderen",
"or": "Of",
"close": "Sluiten",
"copied": "Gekopieerd!",
"openInNewWindow": "Openen in nieuw venster",
@@ -89,12 +91,11 @@
"executingOperation": "Actie uitvoeren...",
"loadMore": "Laad meer",
"errors": {
"VaultOutdated": "Je vault is verouderd. Log in op de AliasVault website en volg de stappen.",
"serverNotAvailable": "De AliasVault server is niet beschikbaar. Probeer het later opnieuw of neem contact op met de ondersteuning als het probleem aanhoudt.",
"clientVersionNotSupported": "Deze versie van de AliasVault browserextensie wordt niet meer ondersteund door de server. Update je browserextensie naar de nieuwste versie.",
"browserExtensionOutdated": "Deze browserextensie is verouderd en kan niet worden gebruikt om toegang te krijgen tot deze vault. Update deze browserextensie om door te gaan.",
"serverVersionNotSupported": "De AliasVault server moet worden bijgewerkt naar een nieuwere versie om deze browserextensie te kunnen gebruiken. Neem contact op met support als je hulp nodig hebt.",
"unknownError": "Er is een onbekende fout opgetreden",
"failedToStoreVault": "Vault opslaan mislukt",
"vaultNotAvailable": "Vault niet beschikbaar",
"failedToRetrieveData": "Gegevens ophalen mislukt",
"vaultIsLocked": "Vault is vergrendeld",
@@ -198,6 +199,9 @@
"searchPlaceholder": "Credentials zoeken...",
"welcomeTitle": "Welkom bij AliasVault!",
"welcomeDescription": "Om de AliasVault browser extensie te gebruiken: navigeer naar een website en gebruik de AliasVault autofill popup om nieuwe credentials aan te maken.",
"noPasskeysFound": "Er zijn nog geen passkeys aangemaakt. Passkeys worden gemaakt door een website te bezoeken die passkeys als een authenticatiemethode biedt.",
"noAttachmentsFound": "Geen credentials gevonden met bijlagen",
"noMatchingCredentials": "Geen credentials gevonden",
"createdAt": "Aangemaakt",
"updatedAt": "Laatst bijgewerkt",
"autofill": "Autofill",
@@ -213,6 +217,13 @@
"saveCredential": "Credential opslaan",
"deleteCredentialTitle": "Credential verwijderen",
"deleteCredentialConfirm": "Weet je zeker dat je deze credential wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.",
"filters": {
"all": "(Alle) Credentials",
"passkeys": "Passkeys",
"aliases": "Aliassen",
"userpass": "Wachtwoorden",
"attachments": "Bijlagen"
},
"randomAlias": "Alias",
"manual": "Handmatig",
"service": "Naam",
@@ -345,11 +356,23 @@
"autofillSettings": "Autofill instellingen",
"clipboardSettings": "Klembord instellingen",
"contextMenuSettings": "Context menu instellingen",
"passkeySettings": "Passkey instellingen",
"contextMenu": "Context menu",
"contextMenuEnabled": "Context menu is ingeschakeld",
"contextMenuDisabled": "Context menu is uitgeschakeld",
"contextMenuDescription": "Klik met de rechtermuisknop op invoervelden om AliasVault opties te zien",
"selectLanguage": "Selecteer taal",
"serverConfiguration": "Serverconfiguratie",
"serverConfigurationDescription": "Configureer de AliasVault server URL voor self-hosted omgevingen",
"customApiUrl": "API URL",
"customClientUrl": "Client URL",
"apiUrlHint": "De API endpoint URL (meestal client URL + /api)",
"clientUrlHint": "De webinterface URL van je self-hosted omgeving",
"autofillSettingsDescription": "Schakel de autofill pop-up in of uit op webpagina's",
"autofillEnabledDescription": "Autofill suggesties verschijnen op login formulieren",
"autofillDisabledDescription": "Autofill suggesties zijn uitgeschakeld",
"languageSettings": "Taal",
"languageSettingsDescription": "Kies je voorkeurstaal",
"validation": {
"apiUrlRequired": "API URL is vereist",
"apiUrlInvalid": "Voer een geldige API URL in",
@@ -357,12 +380,52 @@
"clientUrlInvalid": "Voer een geldige client URL in"
}
},
"passkeys": {
"passkey": "Passkey",
"site": "Website",
"displayName": "Naam",
"helpText": "Passkeys worden aangemaakt op de website wanneer er om wordt gevraagd. Ze kunnen niet handmatig worden bewerkt. Om deze toegangssleutel te verwijderen, kun je deze verwijderen uit deze credential. Om deze passkey te vervangen of een nieuwe te maken, bezoek de website in kwestie en volg de instructies.",
"passkeyMarkedForDeletion": "Passkey gemarkeerd om te verwijderen",
"passkeyWillBeDeleted": "Deze passkey zal worden verwijderd wanneer je deze credential opslaat.",
"bypass": {
"title": "Gebruik browser passkey",
"description": "Hoe lang wilt je de browser passkey voor {{origin}} gebruiken?",
"thisTimeOnly": "Alleen deze keer",
"alwaysForSite": "Altijd voor deze site"
},
"authenticate": {
"title": "Inloggen met passkey",
"signInFor": "Inloggen met passkey voor",
"selectPasskey": "Selecteer een passkey om in te loggen:",
"noPasskeysFound": "Geen passkeys gevonden voor deze site",
"useBrowserPasskey": "Gebruik browser passkey"
},
"create": {
"title": "Passkey aanmaken",
"createFor": "Maak een nieuwe passkey voor",
"titleLabel": "Titel",
"titlePlaceholder": "Voer een naam in voor deze passkey",
"createButton": "Passkey aanmaken",
"creatingButton": "Aanmaken...",
"useBrowserPasskey": "Gebruik browser passkey",
"selectPasskeyToReplace": "Selecteer een passkey om te vervangen:",
"createNewPasskey": "Passkey aanmaken",
"replacingPasskey": "Passkey vervangen: {{displayName}}",
"confirmReplace": "Bevestig vervanging"
},
"settings": {
"passkeyProvider": "Passkey provider",
"passkeyProviderOn": "Passkey provider ingeschakeld",
"enable": "AliasVault als passkey provider inschakelen",
"description": "Wanneer ingeschakeld, behandelt AliasVault passkey verzoeken van websites. Wanneer een website een passkey aanvraagt, wordt de AliasVault pop-up getoond in plaats van de browser of OS pop-up."
}
},
"upgrade": {
"title": "Vault upgraden",
"subtitle": "AliasVault is vernieuwd en je vault moet worden bijgewerkt. Dit kan enkele seconden duren.",
"versionInformation": "Versie-informatie",
"yourVault": "Jouw vault:",
"newVersion": "Nieuwe versie:",
"yourVault": "Jouw vault versie:",
"newVersion": "Nieuwe beschikbare versie:",
"upgrade": "Vault upgraden",
"upgrading": "Aan het upgraden...",
"logout": "Uitloggen",
@@ -386,8 +449,7 @@
"cancel": "Annuleren",
"continueUpgrade": "Verdergaan",
"upgradeFailed": "Upgrade mislukt",
"failedToApplyMigration": "Kon migratie niet toepassen ({{current}} van {{total}})",
"unknownErrorDuringUpgrade": "Er is een onbekende fout opgetreden tijdens de upgrade. Probeer het opnieuw."
"failedToApplyMigration": "Kon migratie niet toepassen ({{current}} van {{total}})"
}
}
}

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