062893bfec58
passkey locked-vault auto-resume: wait for inline unlock instead of immediate reject (v2.39.9)
- time
- 2026-05-13 16:26 UTC · 5 months ago
- machine
- koh2
- parent
- 41a04198eca1
↓ peach-browser-062893bf.tar.gzatlas at this save
files changed6 files
manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/background/messages.ts+7 −1src/background/passkey.ts+42 −3src/content/index.ts+18 −1manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach",4- "version": "2.39.8",4+ "version": "2.39.9",55 "description": "Zero-knowledge password manager",66 "browser_specific_settings": {77 "gecko": {manifest.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach",4- "version": "2.39.8",4+ "version": "2.39.9",55 "description": "Zero-knowledge password manager",66 "action": {77 "default_popup": "src/popup/index.html",package.json+1 −1
@@ -1,6 +1,6 @@11 {22 "name": "peach",3- "version": "2.39.8",3+ "version": "2.39.9",44 "description": "Zero-knowledge password manager",55 "type": "module",66 "scripts": {src/background/messages.ts+7 −1
@@ -31,6 +31,7 @@3131 handlePasskeyHasCredentials,3232 initiateFirefoxPasskeyCreate,3333 isPasskeyOfferDeclined,34+ onVaultUnlocked,3435 recordPasskeyOfferDecline,3536 resolvePendingPasskeyRequest,3637 setPasskeyProviderEnabled,@@ -360,6 +361,7 @@360361 await vault.createVault(password);361362 await setupAutoLock();362363 broadcastToContentScripts({ type: "VAULT_UNLOCKED" });364+ onVaultUnlocked();363365 return { ok: true };364366 }365367 case "UNLOCK": {@@ -368,6 +370,7 @@368370 if (ok) {369371 await setupAutoLock();370372 broadcastToContentScripts({ type: "VAULT_UNLOCKED" });373+ onVaultUnlocked();371374 if (isNativePlatform()) await publishInMemoryVault(vault.getEntries());372375 }373376 return { ok };@@ -1195,7 +1198,10 @@11951198 }11961199 try {11971200 const result = await vault.unlockWithKey(masterKeyHex);1198- if (result.ok) await publishInMemoryVault(result.entries);1201+ if (result.ok) {1202+ onVaultUnlocked();1203+ await publishInMemoryVault(result.entries);1204+ }11991205 return { ok: result.ok, data: result.ok ? { entries: result.entries } : undefined, error: result.ok ? undefined : result.error };12001206 } catch {12011207 return { ok: false, error: "invalid_key" };src/background/passkey.ts+42 −3
@@ -35,6 +35,18 @@3535 { resolve: (approved: boolean) => void; timeoutId: ReturnType<typeof setTimeout> }3636 >();3737 38+// Resolvers for GET requests that arrived while the vault was locked and are39+// waiting for the user to unlock before the passkey flow can continue.40+const unlockWaiters = new Map<number, { resolve: (ok: boolean) => void; timeoutId: ReturnType<typeof setTimeout> }>();41+42+export function onVaultUnlocked(): void {43+ for (const { resolve, timeoutId } of unlockWaiters.values()) {44+ clearTimeout(timeoutId);45+ resolve(true);46+ }47+ unlockWaiters.clear();48+}49+3850 function isPasskeySupported(): boolean {3951 if (typeof chrome.webAuthenticationProxy?.attach === "function") return true;4052 // Firefox 150+ exposes navigator.credentials.create/get from the popup@@ -446,9 +458,36 @@446458 ): Promise<void> {447459 const options = JSON.parse(request.requestDetailsJson) as PublicKeyCredentialRequestOptionsJSON;448460 if (!vault.isUnlocked()) {449- await openApprovalPopup();450- await rejectGet(request.requestId, "NotAllowedError", "Unlock Peach first.");451- return;461+ // Summon the inline unlock prompt on the active tab so the user can unlock462+ // without leaving the page. Fall back to the popup if the content script463+ // is unavailable (e.g. restricted URL, script not yet injected).464+ const tabs = await new Promise<chrome.tabs.Tab[]>((resolve) =>465+ chrome.tabs.query({ active: true, currentWindow: true }, resolve)466+ );467+ const activeTab = tabs[0];468+ if (activeTab?.id) {469+ chrome.tabs.sendMessage(activeTab.id, {470+ type: 'SUMMON_AUTOFILL',471+ source: 'passkey-unlock',472+ }).catch(() => { void openApprovalPopup(); });473+ } else {474+ await openApprovalPopup();475+ }476+477+ // Hold the request open until the vault unlocks or the timeout fires.478+ const unlocked = await new Promise<boolean>((resolve) => {479+ const timeoutId = setTimeout(() => {480+ unlockWaiters.delete(request.requestId);481+ resolve(false);482+ }, APPROVAL_TIMEOUT_MS);483+ unlockWaiters.set(request.requestId, { resolve, timeoutId });484+ });485+486+ if (!unlocked || !vault.isUnlocked()) {487+ await rejectGet(request.requestId, "NotAllowedError", "Not unlocked in time.");488+ return;489+ }490+ // Vault is now unlocked — fall through to authenticate.452491 }453492 454493 if (!(await isProUser())) {src/content/index.ts+18 −1
@@ -837,6 +837,7 @@837837 if (msg?.type === "SUMMON_AUTOFILL") {838838 const source = (msg as { type: string; source?: string }).source;839839 const isShortcut = source === "shortcut";840+ const isPasskeyUnlock = source === "passkey-unlock";840841 841842 const activeInput = getDeepActiveInput();842843 const fields = detectLoginFields();@@ -859,6 +860,22 @@859860 lastDismissedField = null;860861 void handleFieldInteraction(target);861862 }863+ } else if (isPasskeyUnlock) {864+ // Vault is locked during a passkey GET request. Show the inline unlock865+ // prompt anchored to whatever element is focused on the page (typically866+ // the passkey/sign-in button the user just tapped).867+ const anchor = (document.activeElement instanceof HTMLElement && document.activeElement !== document.body)868+ ? document.activeElement as unknown as HTMLInputElement869+ : null;870+ if (anchor) {871+ currentDismiss?.();872+ currentAnchorField = null;873+ currentDismiss = showAutofillSelector(anchor, [], () => {}, {874+ domain: getPageDomain(),875+ locked: true,876+ onDismiss: () => {},877+ });878+ }862879 } else if (isShortcut) {863880 // Shortcut on a page with no login form — show clipboard-copy search mode.864881 showSearchPalette();@@ -872,7 +889,7 @@872889 }873890 }874891 875- sendResponse({ ok: hasForm || isShortcut });892+ sendResponse({ ok: hasForm || isShortcut || isPasskeyUnlock });876893 return true;877894 }878895 if (msg?.type === "PASSKEY_PRO_GATE") {