3aafd13289fb
pass through GET requests with no matching Peach credential instead of rejecting NotAllowedError
- time
- 2026-05-30 21:09 UTC · 4 months ago
- machine
- koh2
- agent
- claude-sonnet-4-6
- session
- passkey google create fix
- parent
- ea3d3ceb520a
↓ peach-browser-3aafd132.tar.gzatlas at this save
files changed5 files
manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/background/passkey-chrome-ceremony.test.ts+12 −8src/background/passkey.ts+23 −10manifest.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.140",5+ "version": "3.6.141",66 "description": "Zero-knowledge password manager",77 "browser_specific_settings": {88 "gecko": {manifest.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach Password Manager",4- "version": "3.6.140",4+ "version": "3.6.141",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": "3.6.140",3+ "version": "3.6.141",44 "description": "Zero-knowledge password manager",55 "type": "module",66 "scripts": {src/background/passkey-chrome-ceremony.test.ts+12 −8
@@ -512,13 +512,15 @@512512 return { entry, credentialId };513513 }514514 515- it("get returns NotAllowedError when no stored credential matches the rpId", async () => {515+ it("get passes through to platform when no stored credential matches the rpId", async () => {516516 const vault = await import("./vault");517517 await vault.createVault("correct-pass-12345");518518 519519 fireGet({ rpId: "github.com", challenge: "Y2g", allowCredentials: [] }, 200);520520 const done = await waitForGetComplete(200);521- expect(done.error).toMatchObject({ name: "NotAllowedError" });521+ // Pass-through: no error and no responseJson — Chrome routes to next authenticator.522+ expect(done.error).toBeUndefined();523+ expect(done.responseJson).toBeUndefined();522524 });523525 524526 it("get encodes the active tab page origin into clientDataJSON (not https://<rpId>)", async () => {@@ -552,7 +554,7 @@552554 expect(after!.passkey!.lastUsedAt).toBeDefined();553555 });554556 555- it("get rejects with NotAllowedError when the stored passkey has no private key (Firefox-created)", async () => {557+ it("get passes through to platform when the stored passkey has no private key (Firefox-created)", async () => {556558 const vault = await import("./vault");557559 await vault.createVault("correct-pass-12345");558560 const { base64urlEncode, generatePasskeyKeypair } = await import("./passkey-crypto");@@ -583,7 +585,9 @@583585 584586 fireGet({ rpId: "webauthn.io", challenge: "Y2g", allowCredentials: [] }, 203);585587 const done = await waitForGetComplete(203);586- expect(done.error).toMatchObject({ name: "NotAllowedError" });588+ // Pass-through: no error and no responseJson — Chrome routes to next authenticator.589+ expect(done.error).toBeUndefined();590+ expect(done.responseJson).toBeUndefined();587591 });588592 589593 it("get on a locked vault parks the request and proceeds after unlock", async () => {@@ -618,19 +622,19 @@618622 // "Invalid requestId" survival — conditional-UI gets cancel fast.619623 // ─────────────────────────────────────────────────────────────────────620624 621- it("rejectGet swallows 'Invalid requestId' from a cancelled conditional-UI get without throwing", async () => {625+ it("pass-through swallows 'Invalid requestId' from a cancelled conditional-UI get without throwing", async () => {622626 const vault = await import("./vault");623627 await vault.createVault("correct-pass-12345");624628 625- // Simulate Chrome cancelling the request between handleGetRequest626- // running and rejectGet calling completeGetRequest.629+ // Simulate Chrome cancelling the request between handleGetRequest running630+ // and the pass-through completeGetRequest call landing.627631 const ce = (globalThis as { chrome: typeof chrome }).chrome.webAuthenticationProxy628632 .completeGetRequest as unknown as ReturnType<typeof vi.fn>;629633 ce.mockImplementationOnce(async () => {630634 throw new Error("Invalid requestId");631635 });632636 633- // No matching passkey for this rpId → goes straight to rejectGet,637+ // No matching passkey for this rpId → pass-through completeGetRequest call,634638 // which must NOT propagate the Invalid requestId error.635639 let resolved = false;636640 proxyListeners.get!({src/background/passkey.ts+23 −10
@@ -675,21 +675,34 @@675675 });676676 677677 if (!match) {678- await rejectGet(request.requestId, "NotAllowedError", "No matching passkey found.");678+ // No Peach passkey for this RP — pass through to the platform authenticator.679+ // completeGetRequest with no error and no responseJson is the webAuthenticationProxy680+ // signal for "I'm not handling this"; Chrome routes the request to the next681+ // authenticator in the chain (the built-in browser passkey manager etc.).682+ // Rejecting with NotAllowedError would propagate as an error to the page and683+ // break RPs (e.g. Google) that issue a GET as part of their create-passkey flow.684+ console.log("[Peach passkey] no matching credential — passing through to platform", { requestId: request.requestId });685+ try {686+ await chrome.webAuthenticationProxy.completeGetRequest({ requestId: request.requestId });687+ } catch (err) {688+ if (isInvalidRequestIdError(err)) return;689+ throw err;690+ }679691 return;680692 }681693 682694 const credential = match.passkey;683- // Chrome's webAuthenticationProxy path requires Peach-held private key684- // material to sign the assertion. Firefox-created passkeys delegate to the685- // platform authenticator — the browser will route to the platform UI686- // automatically when we reject, which is the right outcome.695+ // Chrome's webAuthenticationProxy path requires Peach-held private key material.696+ // Passkeys created on Firefox have no privateKeyJwk — pass through so Chrome697+ // can route to a capable authenticator instead of surfacing an error to the page.687698 if (!credential.privateKeyJwk) {688- await rejectGet(689- request.requestId,690- "NotAllowedError",691- "This passkey was created on Firefox; sign in via the platform authenticator."692- );699+ console.log("[Peach passkey] no private key (Firefox passkey) — passing through to platform", { requestId: request.requestId });700+ try {701+ await chrome.webAuthenticationProxy.completeGetRequest({ requestId: request.requestId });702+ } catch (err) {703+ if (isInvalidRequestIdError(err)) return;704+ throw err;705+ }693706 return;694707 }695708