2c99399f84d6

fix: isUvpaa returns true unconditionally (was gated on unlock — caused 'passkey can't be created on this device'); handleCreateRequest now waits for unlock instead of immediately rejecting (v3.6.106)

time
2026-05-28 17:02 UTC · 5 months ago
machine
koh2
parent
9b1e359cf8d6
4 files manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/background/passkey.ts+38 −4
manifest.firefox.json+1 −1
@@ -2,7 +2,7 @@22   "_comment_audio_capture": "Firefox does not honor the Chrome 'audioCapture' optional_permission — getUserMedia in WebExtensions is gated solely by the runtime permission prompt. Microphone access for chirp pairing works from the popup under the existing '<all_urls>' host permission; no manifest entry is required or accepted here.",33   "manifest_version": 3,44   "name": "Peach Password Manager",5-  "version": "3.6.105",5+  "version": "3.6.106",66   "description": "Zero-knowledge password manager",77   "browser_specific_settings": {88     "gecko": {

view file

manifest.json+1 −1
@@ -1,7 +1,7 @@11 {22   "manifest_version": 3,33   "name": "Peach Password Manager",4-  "version": "3.6.105",4+  "version": "3.6.106",55   "description": "Zero-knowledge password manager",66   "action": {77     "default_popup": "src/popup/index.html",

view file

package.json+1 −1
@@ -1,6 +1,6 @@11 {22   "name": "peach",3-  "version": "3.6.105",3+  "version": "3.6.106",44   "description": "Zero-knowledge password manager",55   "type": "module",66   "scripts": {

view file

src/background/passkey.ts+38 −4
@@ -370,9 +370,37 @@370370   }371371 372372   if (!vault.isUnlocked()) {373-    await openApprovalPopup();374-    await rejectCreate(request.requestId, "NotAllowedError", "Unlock Peach first.");375-    return;373+    // Same wait-for-unlock pattern handleGetRequest uses: summon the inline374+    // unlock prompt on the active tab so the user can unlock without leaving375+    // the page, then hold the create request open until the vault unlocks or376+    // the timeout fires. Immediately rejecting here means Google shows377+    // "Something went wrong" the instant the user picks Peach.378+    const tabs = await new Promise<chrome.tabs.Tab[]>((resolve) =>379+      chrome.tabs.query({ active: true, lastFocusedWindow: true }, resolve)380+    );381+    const activeTab = tabs[0];382+    if (activeTab?.id) {383+      chrome.tabs.sendMessage(activeTab.id, {384+        type: 'SUMMON_AUTOFILL',385+        source: 'passkey-unlock',386+      }).catch(() => { void openApprovalPopup(); });387+    } else {388+      await openApprovalPopup();389+    }390+391+    const unlocked = await new Promise<boolean>((resolve) => {392+      const timeoutId = setTimeout(() => {393+        unlockWaiters.delete(request.requestId);394+        resolve(false);395+      }, APPROVAL_TIMEOUT_MS);396+      unlockWaiters.set(request.requestId, { resolve, timeoutId });397+    });398+399+    if (!unlocked || !vault.isUnlocked()) {400+      await rejectCreate(request.requestId, "NotAllowedError", "Not unlocked in time.");401+      return;402+    }403+    // Vault is now unlocked — fall through to approval.376404   }377405 378406   const approved = await promptUserApproval({@@ -1123,9 +1151,15 @@11231151 11241152   if (!listenersRegistered) {11251153     chrome.webAuthenticationProxy.onIsUvpaaRequest.addListener((requestInfo) => {1154+      // Peach is a user-verifying platform authenticator whenever the proxy1155+      // is attached. The lock state is irrelevant here — the create/get1156+      // handlers prompt for unlock when needed. Returning false while locked1157+      // makes Google (and any RP that checks isUvpaa as a precondition) show1158+      // "A passkey can't be created on this device" before the user even1159+      // gets a chance to start the ceremony.11261160       void chrome.webAuthenticationProxy.completeIsUvpaaRequest({11271161         requestId: requestInfo.requestId,1128-        isUvpaa: vault.isUnlocked(),1162+        isUvpaa: true,11291163       });11301164     });11311165     chrome.webAuthenticationProxy.onCreateRequest.addListener((requestInfo) => {

view file