9c471c1968d6
chore: Remove residual diagnostics and compatibility debris (v4.10.41)
- time
- 2026-08-29 19:06 UTC · 1 month ago
- machine
- koh2
- parent
- c327200c1db0
↓ peach-browser-9c471c19.tar.gzatlas at this save
files changed17 files
.gitignore+0 −1.kohignore+0 −1manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/background/entry.lock-port.test.ts+4 −4src/background/entry.ts+1 −4src/background/messages.ts+0 −8src/background/passkey.ts+2 −35src/content/content.dismissal.test.ts+1 −2src/content/content.fill.test.ts+1 −1src/content/content.interaction.test.ts+1 −2src/content/content.message-routing.test.ts+2 −8src/content/content.policy.test.ts+2 −2src/content/content.selector-ui.test.ts+4 −6src/content/content.totp-toast.addendum.test.ts+0 −60src/popup/views/PairDevice.tsx+2 −3.gitignore+0 −1
@@ -2,7 +2,6 @@22 dist/33 dist-chrome/44 dist-firefox/5-dist-mobile/65 .DS_Store76 .playwright-mcp/87 .gstack/.kohignore+0 −1
@@ -2,7 +2,6 @@22 dist/33 dist-chrome/44 dist-firefox/5-dist-mobile/65 .git/76 .koh/87 .DS_Storemanifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach Password Manager",4- "version": "4.10.40",4+ "version": "4.10.41",55 "description": "Zero-knowledge password manager",66 "browser_specific_settings": {77 "gecko": {manifest.json+1 −1
@@ -2,7 +2,7 @@22 "manifest_version": 3,33 "minimum_chrome_version": "116",44 "name": "Peach Password Manager",5- "version": "4.10.40",5+ "version": "4.10.41",66 "description": "Zero-knowledge password manager",77 "action": {88 "default_popup": "src/popup/index.html",package.json+1 −1
@@ -1,6 +1,6 @@11 {22 "name": "peach",3- "version": "4.10.40",3+ "version": "4.10.41",44 "description": "Zero-knowledge password manager",55 "license": "GPL-3.0-only",66 "type": "module",src/background/entry.lock-port.test.ts+4 −4
@@ -1,5 +1,5 @@1-// LV-5: the lock-on-close onConnect listener accepts only our popup port2-// (legacy bare "popup" or nonce'd "popup-<uuid>") and ignores anything else.1+// LV-5: the lock-on-close onConnect listener accepts only our nonce'd2+// "popup-<uuid>" port and ignores anything else.33 import { beforeEach, describe, expect, it, vi } from "vitest";44 55 type StorageShape = Record<string, unknown>;@@ -121,11 +121,11 @@121121 expect(vault.isUnlocked()).toBe(false);122122 });123123 124- it("ignores a port with an unrelated name — no lock", async () => {124+ it.each(["popup", "evil"])("ignores the non-nonce port name %s", async (name) => {125125 await import("./entry");126126 const vault = await setupUnlockedVaultWithLockOnClose();127127 128- const port = makePort("evil");128+ const port = makePort(name);129129 connectListeners[0](port);130130 // The listener returned early, so no onDisconnect handler was registered.131131 await port.fireDisconnect();src/background/entry.ts+1 −4
@@ -46,10 +46,7 @@4646 const POPUP_PORT_PREFIX = "popup-";4747 4848 chrome.runtime.onConnect.addListener((port) => {49- // Accept legacy bare "popup" for one release so an in-flight popup from the50- // previous version straddling the update doesn't lose lock-on-close.51- // TODO(2026-Q3): drop the bare "popup" acceptance once v3.8.0 has rolled out.52- if (port.name !== "popup" && !port.name.startsWith(POPUP_PORT_PREFIX)) return;49+ if (!port.name.startsWith(POPUP_PORT_PREFIX)) return;5350 port.onDisconnect.addListener(async () => {5451 await denyAllPendingPasskeyRequests();5552 const s = await ensureSettingsLoaded();src/background/messages.ts+0 −8
@@ -295,29 +295,21 @@295295 return { ok: true };296296 }297297 case "CREATE_VAULT_FROM_ROOT_KEY": {298- console.log("[Peach diag] CREATE_VAULT_FROM_ROOT_KEY handler entered");299298 const { password, rootKeyHex } = (msg.payload ?? {}) as {300299 password?: unknown;301300 rootKeyHex?: unknown;302301 };303- console.log("[Peach diag] payload types:", typeof password, typeof rootKeyHex, "lengths:", (password as string)?.length, (rootKeyHex as string)?.length);304302 if (typeof password !== "string" || password.length === 0 || typeof rootKeyHex !== "string") {305- console.log("[Peach diag] payload invalid, returning invalid_payload");306303 return { ok: false, error: "invalid_payload" };307304 }308305 309306 try {310- console.log("[Peach diag] calling vault.createVaultFromRootKey");311307 const { vaultId } = await vault.createVaultFromRootKey(password, rootKeyHex);312- console.log("[Peach diag] createVaultFromRootKey returned vaultId", vaultId);313308 await setupAutoLock();314- console.log("[Peach diag] setupAutoLock done");315309 broadcastToContentScripts({ type: "VAULT_UNLOCKED" });316310 onVaultUnlocked();317- console.log("[Peach diag] handler completing successfully");318311 return { ok: true, vaultId };319312 } catch (err) {320- console.error("[Peach diag] handler caught error:", err);321313 return { ok: false, error: err instanceof Error ? err.message : String(err) };322314 }323315 }src/background/passkey.ts+2 −35
@@ -70,10 +70,6 @@7070 */7171 export async function denyAllPendingPasskeyRequests(): Promise<void> {7272 if (approvals.size === 0 && unlockWaiters.size === 0) return;73- console.log("[Peach passkey] denyAllPendingPasskeyRequests (popup disconnected)", {74- approvals: approvals.size,75- unlockWaiters: unlockWaiters.size,76- });7773 for (const [rid, { resolve, timeoutId }] of approvals.entries()) {7874 clearTimeout(timeoutId);7975 resolve(false);@@ -226,7 +222,6 @@226222 });227223 } catch (err) {228224 if (isInvalidRequestIdError(err)) {229- console.log("[Peach passkey] completeCreateRequest skipped (already cancelled)", { requestId });230225 return;231226 }232227 throw err;@@ -245,7 +240,6 @@245240 });246241 } catch (err) {247242 if (isInvalidRequestIdError(err)) {248- console.log("[Peach passkey] completeGetRequest skipped (already cancelled)", { requestId });249243 return;250244 }251245 throw err;@@ -347,7 +341,6 @@347341 await chrome.webAuthenticationProxy.attach();348342 attached = true;349343 conflict = undefined;350- console.log("[Peach passkey] proxy attached successfully");351344 } catch (error) {352345 attached = false;353346 conflict = error instanceof Error ? error.message : "Another provider is active.";@@ -420,17 +413,11 @@420413 async function handleCreateRequest(421414 request: chrome.webAuthenticationProxy.CreateRequest422415 ): Promise<void> {423- console.log("[Peach passkey] create event received", {424- requestId: request.requestId,425- detailsJson: request.requestDetailsJson,426- });427416 const options = JSON.parse(request.requestDetailsJson) as PublicKeyCredentialCreationOptionsJSON;428417 const rpId = options.rp.id;429418 const rpName = options.rp.name ?? options.rp.id;430419 const userName = options.user.name;431420 const userDisplayName = options.user.displayName ?? options.user.name;432- console.log("[Peach passkey] create parsed", { rpId, rpName, userName, unlocked: vault.isUnlocked() });433-434421 if (!rpId) {435422 console.warn("[Peach passkey] reject: missing rp.id");436423 await rejectCreate(request.requestId, "NotSupportedError", "Missing RP ID");@@ -458,7 +445,6 @@458445 }459446 460447 if (!vault.isUnlocked()) {461- console.log("[Peach passkey] vault locked, summoning inline unlock");462448 // Same wait-for-unlock pattern handleGetRequest uses: summon the inline463449 // unlock prompt on the active tab so the user can unlock without leaving464450 // the page, then hold the create request open until the vault unlocks or@@ -468,7 +454,6 @@468454 chrome.tabs.query({ active: true, lastFocusedWindow: true }, resolve)469455 );470456 const activeTab = tabs[0];471- console.log("[Peach passkey] active tab for unlock summon", { id: activeTab?.id, url: activeTab?.url });472457 if (activeTab?.id) {473458 chrome.tabs.sendMessage(activeTab.id, {474459 type: 'SUMMON_AUTOFILL',@@ -489,7 +474,6 @@489474 unlockWaiters.set(request.requestId, { resolve, timeoutId });490475 });491476 492- console.log("[Peach passkey] unlock wait resolved", { unlocked, vaultUnlocked: vault.isUnlocked() });493477 if (!unlocked || !vault.isUnlocked()) {494478 console.warn("[Peach passkey] reject: unlock timeout");495479 await rejectCreate(request.requestId, "NotAllowedError", "Not unlocked in time.");@@ -498,7 +482,6 @@498482 // Vault is now unlocked — fall through to approval.499483 }500484 501- console.log("[Peach passkey] requesting approval via popup");502485 const approved = await promptUserApproval({503486 requestId: request.requestId,504487 type: "create",@@ -507,7 +490,6 @@507490 userName,508491 userDisplayName,509492 });510- console.log("[Peach passkey] approval resolved", { approved });511493 if (!approved) {512494 console.warn("[Peach passkey] reject: user denied");513495 await rejectCreate(request.requestId, "NotAllowedError", "User denied request.");@@ -520,8 +502,6 @@520502 await rejectCreate(request.requestId, "NotSupportedError", "No supported algorithm offered.");521503 return;522504 }523- console.log("[Peach passkey] algorithm selected", algorithm);524-525505 const credentialIdBytes = crypto.getRandomValues(new Uint8Array(32));526506 const { publicKeyCose, publicKeySpki, privateKeyJwk } = await generatePasskeyKeypair(527507 algorithm@@ -597,10 +577,8 @@597577 clientExtensionResults: {},598578 }),599579 });600- console.log("[Peach passkey] completeCreateRequest success", { requestId: request.requestId });601580 } catch (err) {602581 if (isInvalidRequestIdError(err)) {603- console.log("[Peach passkey] completeCreateRequest skipped (already cancelled)", { requestId: request.requestId });604582 return;605583 }606584 throw err;@@ -715,7 +693,6 @@715693 // authenticator in the chain (the built-in browser passkey manager etc.).716694 // Rejecting with NotAllowedError would propagate as an error to the page and717695 // break RPs (e.g. Google) that issue a GET as part of their create-passkey flow.718- console.log("[Peach passkey] no matching credential — passing through to platform", { requestId: request.requestId });719696 try {720697 await chrome.webAuthenticationProxy.completeGetRequest({ requestId: request.requestId });721698 } catch (err) {@@ -731,7 +708,6 @@731708 // Passkeys created on Firefox have no privateKeyJwk — pass through so Chrome732709 // can route to a capable authenticator instead of surfacing an error to the page.733710 if (!credential.privateKeyJwk) {734- console.log("[Peach passkey] no private key (Firefox passkey) — passing through to platform", { requestId: request.requestId });735711 try {736712 await chrome.webAuthenticationProxy.completeGetRequest({ requestId: request.requestId });737713 } catch (err) {@@ -804,10 +780,8 @@804780 requestId: request.requestId,805781 responseJson,806782 });807- console.log("[Peach passkey] completeGetRequest success", { requestId: request.requestId });808783 } catch (err) {809784 if (isInvalidRequestIdError(err)) {810- console.log("[Peach passkey] completeGetRequest skipped (already cancelled)", { requestId: request.requestId });811785 return;812786 }813787 throw err;@@ -1364,7 +1338,6 @@13641338 13651339 export async function setupPasskeyProxy(): Promise<void> {13661340 if (!isPasskeySupported()) {1367- console.log("[Peach passkey] setupPasskeyProxy: not supported on this browser");13681341 return;13691342 }13701343 @@ -1376,7 +1349,6 @@13761349 }13771350 13781351 if (!listenersRegistered) {1379- console.log("[Peach passkey] registering webAuthenticationProxy listeners");13801352 chrome.webAuthenticationProxy.onIsUvpaaRequest.addListener((requestInfo) => {13811353 // Peach is a user-verifying platform authenticator whenever the proxy13821354 // is attached. The lock state is irrelevant here — the create/get@@ -1390,13 +1362,11 @@13901362 });13911363 });13921364 chrome.webAuthenticationProxy.onCreateRequest.addListener((requestInfo) => {1393- console.log("[Peach passkey] onCreateRequest fired", { requestId: requestInfo.requestId });13941365 void handleCreateRequest(requestInfo).catch(async (error) => {13951366 // "Invalid requestId" means Chrome already cancelled — handlers will1396- // log it themselves and short-circuit, but if it propagates out1397- // (e.g., from a still-in-flight branch) don't double-complete.1367+ // short-circuit, but if it propagates out from a still-in-flight branch,1368+ // don't double-complete.13981369 if (isInvalidRequestIdError(error)) {1399- console.log("[Peach passkey] create handler resolved after cancel", { requestId: requestInfo.requestId });14001370 return;14011371 }14021372 console.error("Peach passkey create failed", error);@@ -1408,10 +1378,8 @@14081378 });14091379 });14101380 chrome.webAuthenticationProxy.onGetRequest.addListener((requestInfo) => {1411- console.log("[Peach passkey] onGetRequest fired", { requestId: requestInfo.requestId });14121381 void handleGetRequest(requestInfo).catch(async (error) => {14131382 if (isInvalidRequestIdError(error)) {1414- console.log("[Peach passkey] get handler resolved after cancel", { requestId: requestInfo.requestId });14151383 return;14161384 }14171385 console.error("Peach passkey get failed", error);@@ -1423,7 +1391,6 @@14231391 });14241392 });14251393 chrome.webAuthenticationProxy.onRequestCanceled.addListener((requestId) => {1426- console.log("[Peach passkey] onRequestCanceled", { requestId });14271394 markCancelled(requestId);14281395 // Release any unlock waiter parked for this request.14291396 const waiter = unlockWaiters.get(requestId);src/content/content.dismissal.test.ts+1 −2
@@ -223,8 +223,7 @@223223 // form A's grace). The implementation's page-level grace is NOT form-scoped:224224 // for PAGE_FILL_GRACE_MS, EVERY field is suppressed. After the window, the225225 // filled form A's fields stay locked (filledForms is permanent) while a field226- // in an unrelated form B opens. We pin the ACTUAL behavior. See227- // INTEGRATION-CONTRACT.md.226+ // in an unrelated form B opens. This test pins the actual behavior.228227 it("T-D10: during the page grace every field is suppressed; after it, only the filled form stays locked", async () => {229228 const { password: aPassword } = await openAndFill();230229 // Build a second, independent form B.src/content/content.fill.test.ts+1 −1
@@ -185,7 +185,7 @@185185 // returns void and the caller (index.ts recordFillSuccess) tracks success by186186 // the elements it passed. We pin the actual contract: fillFields returns187187 // undefined, and an invisible password is intentionally skipped — which is how188- // a caller can tell a fill was partial. (See INTEGRATION-CONTRACT.md.)188+ // a caller can tell a fill was partial.189189 it("T-FL12: fillFields returns void and skips an invisible password field", () => {190190 const u = makeInput({ type: "text" });191191 const p = makeInput({ type: "password" });src/content/content.interaction.test.ts+1 −2
@@ -7,8 +7,7 @@77 // open?". It is deliberately narrow: it does NOT scope by form (the caller hands88 // it a pre-scoped field list) and it does NOT inspect the live DOM. The tests99 // below pin its real contract; where the brief assumed broader behavior10-// (form-scoping, email-first shape), the deviation is noted inline and in11-// INTEGRATION-CONTRACT.md.10+// (form-scoping, email-first shape), the deviation is noted inline.1211 1312 import { beforeEach, describe, expect, it } from "vitest";1413 src/content/content.message-routing.test.ts+2 −8
@@ -9,9 +9,8 @@99 // a spy sendResponse and a same-extension sender, then assert the routed effect.1010 //1111 // Where the brief assumed messages that this listener does not own12-// (DISMISS_AUTOFILL, FILL_AND_SUBMIT), and the Addendum-2 showTotpCopyToast that13-// does not exist yet, the cases are documented rather than faked. See14-// INTEGRATION-CONTRACT.md.12+// (DISMISS_AUTOFILL, FILL_AND_SUBMIT), the cases are documented rather than13+// faked.1514 1615 import { vi } from "vitest";1716 @@ -177,8 +176,3 @@177176 expect(send).not.toHaveBeenCalled();178177 });179178 });180-181-// Addendum 2 (T-M11–T-M13, proposed showTotpCopyToast) lives in182-// content.totp-toast.addendum.test.ts, which imports the REAL inject.ts so it183-// can detect whether the proposed export exists. It cannot be probed here184-// because this file mocks ./inject (and the mock throws on unknown exports).src/content/content.policy.test.ts+2 −2
@@ -8,8 +8,8 @@88 // text (aria-describedby, hints, labels). It does NOT read a minlength attribute99 // and it inspects only the first password field — where the brief assumed1010 // otherwise (T-PO01 minlength, T-PO05 generator wiring, T-PO06 multi-field11-// "most restrictive"), the deviation is pinned and documented in12-// INTEGRATION-CONTRACT.md so the gap is explicit, not silently "passing".11+// "most restrictive"), the deviation is pinned inline so the gap remains12+// explicit rather than silently "passing".1313 1414 import { beforeEach, describe, expect, it } from "vitest";1515 src/content/content.selector-ui.test.ts+4 −6
@@ -10,12 +10,10 @@1010 // behind that closed boundary and is, by design, unreachable from any external1111 // script (including this test and even a page-context Playwright script).1212 //13-// So this file asserts the ONLY externally-observable contract: the selector14-// mounts a host into the document and the returned dismiss function tears it15-// down. The interior behaviors are marked it.todo and routed to the Playwright16-// e2e, which verifies them through their effects (fields getting filled, the17-// host disappearing) rather than by reading the closed tree. The recommendation18-// to add a test-only "open shadow" seam is recorded in INTEGRATION-CONTRACT.md.13+// So this file asserts the externally-observable contract: the selector mounts14+// a host into the document and the returned dismiss function tears it down.15+// Browser-level behavior is covered through observable effects rather than by16+// adding a test-only seam into the closed shadow root.1917 2018 import "./_setup"; // chrome mock + ResizeObserver polyfill2119 import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";src/content/content.totp-toast.addendum.test.ts+0 −60
@@ -1,60 +0,0 @@1-// @vitest-environment jsdom2-//3-// Addendum 2 — TOTP auto-copy toast (T-M11–T-M13), proposed feature.4-//5-// Today, when Peach auto-copies a TOTP code it shows the generic "2FA code6-// copied" toast (showToast in index.ts). The addendum asks for a dedicated7-// showTotpCopyToast(secondsUntilClear) that renders a passkey-family toast8-// ("2FA code copied / clears in 30s") and auto-dismisses after 3s, resetting its9-// timer if a second copy lands while it is visible.10-//11-// showTotpCopyToast does not exist yet. This file imports the REAL inject.ts and12-// activates the spec the moment the export appears — so it is an executable13-// contract, not a stub that always passes. See INTEGRATION-CONTRACT.md.14-15-import "./_setup"; // installs the chrome mock inject.ts touches at import time16-import { describe, expect, it, vi } from "vitest";17-import * as inject from "./inject";18-19-const showTotpCopyToast = (inject as unknown as {20- showTotpCopyToast?: (secondsUntilClear: number) => void;21-}).showTotpCopyToast;22-23-describe.skipIf(typeof showTotpCopyToast !== "function")(24- "Addendum 2 — proposed showTotpCopyToast (requires inject.ts change)",25- () => {26- const toast = showTotpCopyToast!;27-28- it("T-M11: renders a toast showing the configured clear time", () => {29- document.body.innerHTML = "";30- expect(() => toast(30)).not.toThrow();31- expect(document.body.textContent ?? "").toMatch(/clears in 30/i);32- });33-34- it("T-M12: a second copy while showing resets, leaving a single toast", () => {35- document.body.innerHTML = "";36- toast(30);37- toast(30);38- const toasts = document.querySelectorAll('[data-peach-toast], .peach-totp-toast');39- expect(toasts.length).toBeLessThanOrEqual(1);40- });41-42- it("T-M13: auto-dismisses after ~3s", () => {43- document.body.innerHTML = "";44- vi.useFakeTimers();45- toast(30);46- vi.advanceTimersByTime(3500);47- vi.useRealTimers();48- expect(document.body.textContent ?? "").not.toMatch(/2FA code copied/i);49- });50- }51-);52-53-// Addendum 2 is now implemented: the export exists and the T-M11–T-M13 spec54-// above runs. This pins that the feature shipped (and that the skipIf block is55-// no longer being silently skipped).56-describe("Addendum 2 status", () => {57- it("exposes showTotpCopyToast now that the addendum is implemented", () => {58- expect(typeof showTotpCopyToast).toBe("function");59- });60-});src/popup/views/PairDevice.tsx+2 −3
@@ -6,9 +6,8 @@66 //77 // A populated extension is the source, so this screen displays one QR for the88 // empty device to scan. A fresh extension takes the complementary receiver path9-// in AdoptVaultView, where camera/manual invitation fallback belongs. Keeping10-// the raw invitation off this source screen prevents it from being mistaken for11-// the later 8-digit safety code.9+// in AdoptVaultView. Keeping the raw invitation out of the visible copy prevents10+// it from being mistaken for the later 8-digit safety code.1211 //1312 // The pairing itself runs in the background (relay-adoption-bg.ts) and1413 // reaches this view via the ADOPTION_SOURCE_* messages.