f400fcc5c4c3

feat: Reset sync identity from Settings (v2.13.28)

time
2026-04-30 15:10 UTC · 6 months ago
machine
koh2
parent
15f0e8760acb
8 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 −0
manifest.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": {

view file

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",

view file

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": {

view file

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");

view file

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();

view file

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;

view file

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>

view file

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"

view file