f400fcc5c4c3
feat: Reset sync identity from Settings (v2.13.28)
- time
- 2026-04-30 15:10 UTC · 6 months ago
- machine
- koh2
- parent
- 15f0e8760acb
↓ peach-browser-f400fcc5.tar.gzatlas at this save
files changed8 files
manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/background/messages.test.ts+19 −0src/background/messages.ts+22 −0src/popup/styles/popup.css+57 −0src/popup/views/Settings.tsx+59 −0src/shared/types.ts+1 −0manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach",4- "version": "2.13.27",4+ "version": "2.13.28",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.13.27",4+ "version": "2.13.28",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.13.27",3+ "version": "2.13.28",44 "description": "Zero-knowledge password manager",55 "type": "module",66 "scripts": {src/background/messages.test.ts+19 −0
@@ -671,6 +671,25 @@671671 expect(hits[0]!.useCount).toBe(3);672672 });673673 674+ it("RESET_SYNC_IDENTITY is Pro-gated and rejects free users", async () => {675+ const res = await dispatch({ type: "RESET_SYNC_IDENTITY" });676+ expect(res.ok).toBe(false);677+ expect((res as { error?: string }).error).toBe("pro_required");678+ });679+680+ it("RESET_SYNC_IDENTITY clears local paired devices for Pro users", async () => {681+ storage["peach_paired_devices"] = [682+ { deviceId: "a", displayName: "iPhone", x25519PublicKey: "pk-a", pairedAt: 1 },683+ { deviceId: "b", displayName: "MacBook", x25519PublicKey: "pk-b", pairedAt: 2 },684+ ];685+ const license = await import("./license");686+ vi.spyOn(license, "isProUser").mockResolvedValue(true);687+ const res = await dispatch({ type: "RESET_SYNC_IDENTITY" });688+ expect(res.ok).toBe(true);689+ expect((res as { data?: { removedDevices: number } }).data?.removedDevices).toBe(2);690+ expect(storage["peach_paired_devices"]).toEqual([]);691+ });692+674693 it("SUBMIT_FEEDBACK does not refresh the auto-lock timer", async () => {675694 const vault = await import("./vault");676695 await vault.createVault("correct-pass-12345");src/background/messages.ts+22 −0
@@ -618,6 +618,28 @@618618 });619619 return { ok: true };620620 }621+ case "RESET_SYNC_IDENTITY": {622+ const gate = await requirePro("sync");623+ if (!gate.allowed) return { ok: false, error: "pro_required" };624+ const { getPairedDevices, removePairedDevice } = await import('./sync-storage');625+ const devices = await getPairedDevices();626+ for (const device of devices) {627+ await removePairedDevice(device.deviceId);628+ }629+ // Native rotation is best-effort: when the native messaging host is630+ // not yet installed (Phase 9), connectNative throws and we still631+ // honour the local clear so the UI reflects the reset. The native632+ // identity rotates the next time the host starts cleanly anyway.633+ let nativePublicKey: string | null = null;634+ try {635+ const { rotateNativeIdentity, disconnectSyncHost } = await import('./sync');636+ nativePublicKey = await rotateNativeIdentity();637+ disconnectSyncHost();638+ } catch {639+ nativePublicKey = null;640+ }641+ return { ok: true, data: { removedDevices: devices.length, nativePublicKey } };642+ }621643 case "RECORD_MANUAL_SUMMON": {622644 const { domain } = msg.payload as { domain: string };623645 const settings = vault.getSettings();src/popup/styles/popup.css+57 −0
@@ -4013,6 +4013,63 @@40134013 color: var(--peach-breach);40144014 }40154015 4016+.peach-sync-advanced-row {4017+ display: flex;4018+ justify-content: space-between;4019+ align-items: center;4020+ padding: 6px 0;4021+ gap: 12px;4022+}4023+4024+.peach-sync-advanced-info {4025+ display: flex;4026+ flex-direction: column;4027+ gap: 2px;4028+ flex: 1;4029+}4030+4031+.peach-sync-advanced-name {4032+ font-size: 13px;4033+ color: var(--peach-t0);4034+}4035+4036+.peach-sync-reset-confirm {4037+ display: flex;4038+ flex-direction: column;4039+ gap: 8px;4040+ padding: 10px;4041+ border: 1px solid var(--peach-breach);4042+ border-radius: 10px;4043+ background: color-mix(in srgb, var(--peach-breach) 6%, transparent);4044+}4045+4046+.peach-sync-reset-confirm-headline {4047+ font-size: 13px;4048+ font-weight: 600;4049+ color: var(--peach-t0);4050+ margin: 0;4051+}4052+4053+.peach-sync-reset-confirm-body {4054+ font-size: 12px;4055+ color: var(--peach-t1);4056+ margin: 0;4057+ line-height: 1.4;4058+}4059+4060+.peach-btn--danger {4061+ background: var(--peach-breach);4062+ color: #fff;4063+ border-color: var(--peach-breach);4064+}4065+.peach-btn--danger:hover:not(:disabled) {4066+ background: color-mix(in srgb, var(--peach-breach) 88%, #000);4067+}4068+.peach-btn--danger:disabled {4069+ opacity: 0.55;4070+ cursor: not-allowed;4071+}4072+40164073 .peach-sync-hero-icon {40174074 color: var(--peach-t1);40184075 opacity: 0.85;src/popup/views/Settings.tsx+59 −0
@@ -355,6 +355,19 @@355355 await sendMessage({ type: "MARK_NETWORK_TRUSTED" });356356 }357357 358+ const [resetIdentityConfirm, setResetIdentityConfirm] = createSignal(false);359+ const [resetIdentityRunning, setResetIdentityRunning] = createSignal(false);360+361+ async function handleResetSyncIdentity() {362+ setResetIdentityRunning(true);363+ const res = await sendMessage({ type: "RESET_SYNC_IDENTITY" });364+ setResetIdentityRunning(false);365+ if ((res as { ok: boolean }).ok) {366+ setPairedDevices([]);367+ setResetIdentityConfirm(false);368+ }369+ }370+358371 function formatRelativeTime(timestamp?: number): string {359372 if (!timestamp) return "Never";360373 const diff = Date.now() - timestamp;@@ -1192,6 +1205,52 @@11921205 <span class="peach-sync-device-meta">11931206 Tailscale is always trusted11941207 </span>1208+ </div>1209+1210+ <div class="peach-settings-row peach-settings-row--meta peach-settings-row--stack">1211+ <span class="peach-sync-section-label">Advanced</span>1212+ <Show1213+ when={resetIdentityConfirm()}1214+ fallback={1215+ <div class="peach-sync-advanced-row">1216+ <div class="peach-sync-advanced-info">1217+ <span class="peach-sync-advanced-name">Reset sync identity</span>1218+ <span class="peach-sync-device-meta">1219+ Generates a new keypair and unpairs every device.1220+ </span>1221+ </div>1222+ <button1223+ class="peach-btn peach-btn--ghost peach-btn--sm peach-sync-remove-btn"1224+ onClick={() => setResetIdentityConfirm(true)}1225+ >1226+ Reset1227+ </button>1228+ </div>1229+ }1230+ >1231+ <div class="peach-sync-reset-confirm">1232+ <p class="peach-sync-reset-confirm-headline">Reset sync identity?</p>1233+ <p class="peach-sync-reset-confirm-body">1234+ This will unpair all devices. You will need to pair them again from scratch.1235+ </p>1236+ <div class="peach-settings-actions--row">1237+ <button1238+ class="peach-btn peach-btn--ghost peach-btn--sm"1239+ onClick={() => setResetIdentityConfirm(false)}1240+ disabled={resetIdentityRunning()}1241+ >1242+ Cancel1243+ </button>1244+ <button1245+ class="peach-btn peach-btn--danger peach-btn--sm"1246+ onClick={() => void handleResetSyncIdentity()}1247+ disabled={resetIdentityRunning()}1248+ >1249+ {resetIdentityRunning() ? "Resetting..." : "Reset"}1250+ </button>1251+ </div>1252+ </div>1253+ </Show>11951254 </div>11961255 </div>11971256 </div>src/shared/types.ts+1 −0
@@ -246,6 +246,7 @@246246 | "REMOVE_PAIRED_DEVICE"247247 | "REMOVE_TRUSTED_NETWORK"248248 | "MARK_NETWORK_TRUSTED"249+ | "RESET_SYNC_IDENTITY"249250 | "GET_PENDING_PASSKEY"250251 | "APPROVE_PASSKEY"251252 | "DENY_PASSKEY"