371dbc476bcb
close PB-26-925 and PB-26-927 through PB-26-929 at Browser 5.2.51; record PB-26-926 Android gate blocker
- time
- 2026-09-12 01:39 UTC · 1 month ago
- machine
- koh2
- agent
- camel-stream/auto
- session
- close audit findings
- parent
- bea130facd46
↓ peach-browser-371dbc47.tar.gzatlas at this save
files changed13 files
Dockerfile.firefox-gate+2 −6README.md+2 −3manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/background/message-policy.test.ts+1 −1src/background/message-policy.ts+1 −1src/background/messages.ts+12 −8src/background/sync-alerts.test.ts+11 −0src/background/sync-alerts.ts+15 −4src/popup/App.tsx+8 −8src/shared/release-notes.ts+9 −1src/shared/types.ts+2 −1Dockerfile.firefox-gate+2 −6
@@ -1,10 +1,6 @@1-FROM oven/bun:1.3.14-debian1+FROM mcr.microsoft.com/playwright:v1.59.1-noble@sha256:b0ab6f3cb99aa7803adbc14d9027ec1785fc6e433b97e134e0f8fe61683b6b5322 3-RUN apt-get update \4- && apt-get install -y --no-install-recommends firefox-esr ca-certificates nodejs npm5-63 WORKDIR /app7-ENV FIREFOX_BIN=/usr/bin/firefox-esr4+ENV FIREFOX_BIN=/usr/bin/firefox85 ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright96 10-RUN bunx playwright install --with-deps firefoxREADME.md+2 −3
@@ -48,9 +48,8 @@4848 4949 Implementation details live alongside the code:5050 51-- [Cryptographic test vectors](./docs/CRYPTOGRAPHIC-TEST-VECTORS.md)52-- [WebAssembly provenance](./docs/WASM-PROVENANCE.md)53-51+- [Security finding resolutions](./docs/PEACH_BROWSER_SECURITY_FINDING_RESOLUTIONS_2026-09-05.md)52+- [Cross-client Sync protocol](./docs/sync-protocol.md)5453 ---5554 5655 ## Buildingmanifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach Password Manager",4- "version": "5.2.50",4+ "version": "5.2.51",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": "5.2.50",5+ "version": "5.2.51",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": "5.2.50",3+ "version": "5.2.51",44 "description": "Zero-knowledge password manager",55 "license": "GPL-3.0-only",66 "type": "module",src/background/message-policy.test.ts+1 −1
@@ -11,7 +11,7 @@1111 it("classifies every declared message type", () => {1212 const policy = MESSAGE_CONTEXT_POLICY as Record<string, string>;1313 const declaredTypes: MessageType[] = [14- "GET_SYNC_ALERTS", "CLEAR_CLIPBOARD", "GET_PENDING_VOUCHED_DEVICES", "CONFIRM_VOUCHED_DEVICE",14+ "GET_SYNC_ALERTS", "DISMISS_SYNC_ALERTS", "CLEAR_CLIPBOARD", "GET_PENDING_VOUCHED_DEVICES", "CONFIRM_VOUCHED_DEVICE",1515 "REJECT_VOUCHED_DEVICE", "VOUCHED_DEVICE_PENDING", "ROTATE_SYNC_IDENTITY", "DEVICE_KEY_ROTATED",1616 "UNLOCK", "UNLOCK_WITH_PIN", "GET_PIN_STATUS", "LOCK", "IS_UNLOCKED", "HAS_VAULT", "GET_ENTRIES", "EXPORT_ENTRIES", "ADD_ENTRY",1717 "BATCH_ADD_ENTRIES", "RESTORE_ENTRIES", "CLEAR_VAULT_ENTRIES", "UPDATE_ENTRY", "ADD_ADDITIONAL_URL",src/background/message-policy.ts+1 −1
@@ -17,8 +17,8 @@1717 * fail type-checking instead of silently becoming trusted.1818 */1919 export const MESSAGE_CONTEXT_POLICY = {20- // Popup/options operations and sensitive reads/writes.2120 GET_SYNC_ALERTS: "extension-page-only",21+ DISMISS_SYNC_ALERTS: "extension-page-only",2222 GET_PENDING_VOUCHED_DEVICES: "extension-page-only",2323 CONFIRM_VOUCHED_DEVICE: "extension-page-only",2424 REJECT_VOUCHED_DEVICE: "extension-page-only",src/background/messages.ts+12 −8
@@ -10,7 +10,7 @@1010 */1111 1212 import { copyToClipboard, getEffectiveClearSeconds } from "./clipboard";13-import { getPendingSyncAlerts, clearPendingSyncAlerts } from "./sync-alerts";13+import { dismissPendingSyncAlerts, getPendingSyncAlerts } from "./sync-alerts";1414 import { extractDomain } from "../shared/domain";1515 import {1616 getPendingVouchedDevice,@@ -1876,13 +1876,17 @@18761876 18771877 case "GET_SYNC_ALERTS": {18781878 if (vault.isDecoyMode()) return { ok: true, data: [] };1879- const { consume } = (msg.payload ?? {}) as { consume?: boolean };1880- const alerts = await getPendingSyncAlerts();1881- // The popup reads on open and acknowledges with consume:true after1882- // rendering, so a popup crash between read and render doesn't lose1883- // the alerts.1884- if (consume) await clearPendingSyncAlerts();1885- return { ok: true, data: alerts };1879+ return { ok: true, data: await getPendingSyncAlerts() };1880+ }1881+1882+ case "DISMISS_SYNC_ALERTS": {1883+ if (vault.isDecoyMode()) return { ok: true };1884+ const type = (msg.payload as { type?: unknown } | undefined)?.type;1885+ if (type !== "DEVICE_KEY_ROTATED" && type !== "DEVICE_REVOKED") {1886+ return { ok: false, error: "invalid_sync_alert_type" };1887+ }1888+ await dismissPendingSyncAlerts(type);1889+ return { ok: true };18861890 }18871891 18881892 case "GET_PENDING_VOUCHED_DEVICES": {src/background/sync-alerts.test.ts+11 −0
@@ -1,6 +1,7 @@11 import { beforeEach, describe, expect, it } from "vitest";22 import {33 clearPendingSyncAlerts,4+ dismissPendingSyncAlerts,45 getPendingSyncAlerts,56 recordSyncAlerts,67 } from "./sync-alerts";@@ -56,5 +57,15 @@5657 ]);5758 await clearPendingSyncAlerts();5859 expect(await getPendingSyncAlerts()).toEqual([]);60+ });61+ it("dismisses one alert group without losing another", async () => {62+ await recordSyncAlerts([63+ { type: "DEVICE_KEY_ROTATED", entryId: "", entryTitle: "", deviceName: "k", timestamp: 1 },64+ { type: "DEVICE_REVOKED", entryId: "", entryTitle: "", deviceName: "r", timestamp: 2 },65+ ]);66+ await dismissPendingSyncAlerts("DEVICE_KEY_ROTATED");67+ expect(await getPendingSyncAlerts()).toEqual([68+ { type: "DEVICE_REVOKED", entryId: "", entryTitle: "", deviceName: "r", timestamp: 2 },69+ ]);5970 });6071 });src/background/sync-alerts.ts+15 −4
@@ -4,10 +4,9 @@44 // events enter this queue so a reliable sync does not turn the vault into an55 // activity feed.66 //7-// Persistence: chrome.storage.local under a dedicated key so alerts survive87 // MV3 worker eviction between the sync round and the next popup open. The9-// queue is cleared after the popup acknowledges display (GET_SYNC_ALERTS10-// with `consume: true`).8+// queue remains until the popup explicitly dismisses the corresponding alert9+// group.1110 1211 import { capDeviceName } from "../shared/sync-limits";1312 @@ -48,10 +47,22 @@4847 export async function getPendingSyncAlerts(): Promise<SyncAlert[]> {4948 return readAlerts();5049 }51-5250 export async function clearPendingSyncAlerts(): Promise<void> {5351 try {5452 await chrome.storage.local.remove(SYNC_ALERTS_KEY);53+ } catch {54+ /* non-fatal */55+ }56+}57+58+export async function dismissPendingSyncAlerts(type: SyncAlert["type"]): Promise<void> {59+ try {60+ const remaining = (await readAlerts()).filter((alert) => alert.type !== type);61+ if (remaining.length === 0) {62+ await chrome.storage.local.remove(SYNC_ALERTS_KEY);63+ } else {64+ await chrome.storage.local.set({ [SYNC_ALERTS_KEY]: remaining });65+ }5566 } catch {5667 /* non-fatal */5768 }src/popup/App.tsx+8 −8
@@ -161,14 +161,14 @@161161 ok: boolean;162162 data?: SyncAlert[];163163 };164- if (r.ok && (r.data?.length ?? 0) > 0) {165- setSyncAlerts(r.data ?? []);166- // Acknowledge so a crash between read and render doesn't lose them —167- // they remain on screen until the user dismisses each.168- void sendMessage({ type: "GET_SYNC_ALERTS", payload: { consume: true } });169- }164+ if (r.ok) setSyncAlerts(r.data ?? []);170165 }171- function dismissAlertGroup(type: string) {166+ async function dismissAlertGroup(type: SyncAlert["type"]) {167+ const result = (await sendMessage({168+ type: "DISMISS_SYNC_ALERTS",169+ payload: { type },170+ })) as { ok: boolean };171+ if (!result.ok) return;172172 setSyncAlerts((arr) => arr.filter((a) => a.type !== type));173173 setAlertFlipIdx((m) => { const n = { ...m }; delete n[type]; return n; });174174 }@@ -1692,7 +1692,7 @@16921692 class="peach-sync-alert-dismiss"16931693 type="button"16941694 aria-label={group.length > 1 ? "Dismiss all" : "Dismiss"}1695- onClick={() => dismissAlertGroup(type)}1695+ onClick={() => void dismissAlertGroup(type as SyncAlert["type"])}16961696 >×</button>16971697 </div>16981698 );src/shared/release-notes.ts+9 −1
@@ -18,8 +18,16 @@1818 notes: ReleaseNote[];1919 importance?: ReleaseImportance;2020 }21-2221 export const RELEASES: PeachRelease[] = [22+ {23+ version: "5.2.51",24+ releasedAt: "2026-09-11",25+ notes: [{26+ heading: "Keep Sync security alerts until dismissal",27+ body: "Device-key and revocation alerts remain queued until you dismiss them, so closing the popup cannot hide a trust event.",28+ }],29+ },30+2331 {2432 version: "5.2.49",2533 releasedAt: "2026-09-11",src/shared/types.ts+2 −1
@@ -197,8 +197,9 @@197197 // handled in src/background/messages.ts. Everything in this group must198198 // have a matching case in that file's switch statement.199199 export type MessageType =200- | "GET_SYNC_ALERTS"201200 | "CLEAR_CLIPBOARD"201+ | "GET_SYNC_ALERTS"202+ | "DISMISS_SYNC_ALERTS"202203 | "GET_PENDING_VOUCHED_DEVICES"203204 | "CONFIRM_VOUCHED_DEVICE"204205 | "REJECT_VOUCHED_DEVICE"