cc7196b4e72a

bump to v2.30.1, rebuild chrome and firefox

time
2026-05-04 23:47 UTC · 5 months ago
machine
koh2
parent
7767a36970cd
7 files SETTINGS_REDESIGN_SPEC.md+0 −743SYNC_STATUS.md+0 −309audit2.29.3.md+0 −275manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1syn001.md+0 −53
SETTINGS_REDESIGN_SPEC.md+0 −743
@@ -1,743 +0,0 @@1-# Settings Redesign + Feedback System — Implementation Spec2-3-> Read `AGENTS.md`, `.agent/Projects.md`, and `.agent/CONTINUITY.md` before4-> touching any file. Follow every invariant in Projects.md without exception.5-> All Koh rules in AGENTS.md apply. Use the `dev` lane. Start with a6-> checkpoint.7-8----9-10-## 0. Pre-flight11-12-```sh13-koh status --json         # confirm dev lane, clean working tree14-koh checkpoint "before settings redesign and feedback feature"15-```16-17-Note: Projects.md §8 says "no new features until P0/P1 findings are closed."18-This task has explicit user authorisation to proceed despite that rule.19-Record the override in `.agent/CONTINUITY.md` before starting.20-21----22-23-## 1. Version bump24-25-This is a new feature (MINOR). Bump all three files from `2.0.3` → `2.1.0`.26-All three must agree — this is a hard invariant (Projects.md §2.1 / §4.8).27-28-- `manifest.json` → `"version": "2.1.0"`29-- `manifest.firefox.json` → `"version": "2.1.0"`30-- `package.json` → `"version": "2.1.0"`31-32----33-34-## 2. `src/shared/types.ts` — add `SUBMIT_FEEDBACK` to the MessageType union35-36-Add `"SUBMIT_FEEDBACK"` to the background-bound section of `MessageType`37-(before the content-script-bound comment block, after `"IS_CARD_DECLINED"`):38-39-```ts40-| "IS_CARD_DECLINED"41-| "SUBMIT_FEEDBACK"42-// Content-script-bound message types ...43-```44-45-No other changes to this file.46-47----48-49-## 3. `src/background/messages.ts` — SUBMIT_FEEDBACK handler50-51-### 3a. Add the feedback endpoint constant52-53-Directly below the existing `PRIVATE_IPV4_RANGES` constant at the top of the54-file, add:55-56-```ts57-// Feedback endpoint — replace with your ingestion URL before shipping.58-// Leaving this empty disables network submission while keeping the UI functional.59-// When set, this URL is validated by isAllowedCanaryWebhookUrl before use.60-const FEEDBACK_ENDPOINT = "";61-```62-63-### 3b. Add the case to the switch64-65-Place this case immediately before the `default:` case at the bottom of the66-`switch (msg.type)` block:67-68-```ts69-case "SUBMIT_FEEDBACK": {70-  const { category, message, version, browser } = msg.payload as {71-    category: string;72-    message: string;73-    version: string;74-    browser: string;75-  };76-  if (!message.trim()) return { ok: false, error: "message is empty" };77-  if (!FEEDBACK_ENDPOINT) return { ok: true };78-  if (!isAllowedCanaryWebhookUrl(FEEDBACK_ENDPOINT)) {79-    return { ok: false, error: "invalid feedback endpoint" };80-  }81-  try {82-    const res = await fetch(FEEDBACK_ENDPOINT, {83-      method: "POST",84-      headers: { "Content-Type": "application/json" },85-      body: JSON.stringify({ category, message, version, browser, source: "peach-extension" }),86-    });87-    return { ok: res.ok, error: res.ok ? undefined : `HTTP ${res.status}` };88-  } catch (err) {89-    return { ok: false, error: err instanceof Error ? err.message : "network error" };90-  }91-}92-```93-94-### 3c. Update `shouldRefreshAutoLock`95-96-`SUBMIT_FEEDBACK` must not refresh the auto-lock timer. Add it to the97-exclusion list in `shouldRefreshAutoLock`:98-99-```ts100-function shouldRefreshAutoLock(type: MessageType): boolean {101-  return (102-    type !== "HAS_VAULT" &&103-    type !== "IS_UNLOCKED" &&104-    type !== "GET_VAULT_STATUS" &&105-    type !== "LOCK" &&106-    type !== "UPDATE_SETTINGS" &&107-    type !== "GET_PENDING_PASSKEY" &&108-    type !== "GET_PASSKEY_PROVIDER_STATE" &&109-    type !== "DECLINE_CARD_SAVE" &&110-    type !== "IS_CARD_DECLINED" &&111-    type !== "SUBMIT_FEEDBACK"    // ← add this line112-  );113-}114-```115-116-### 3d. Network surface — update Projects.md §3.7117-118-Add a third permitted fetch entry to `§3.7 Network surface` in119-`.agent/Projects.md`:120-121-```122-3. `FEEDBACK_ENDPOINT` constant in `src/background/messages.ts` — only when123-   non-empty, only on `SUBMIT_FEEDBACK` messages initiated by the user. URL124-   validated by `isAllowedCanaryWebhookUrl`. Defaults to empty string125-   (disabled). Set by the developer before shipping.126-```127-128-Also verify the network grep still passes:129-130-```sh131-grep -r "fetch(" src/ | grep -v "pwnedpasswords" | grep -v "webhookUrl" | grep -v "FEEDBACK_ENDPOINT" | grep -v "\.test\."132-```133-134-Must return zero results.135-136-### 3e. Tests — `src/background/messages.test.ts` (or equivalent test file)137-138-Per Projects.md §2.2, every new `messages.ts` handler requires tests. Add139-these three cases to the existing test file:140-141-```ts142-describe("SUBMIT_FEEDBACK", () => {143-  it("returns ok:true when FEEDBACK_ENDPOINT is empty (no-op path)", async () => {144-    // FEEDBACK_ENDPOINT defaults to "" in test environment145-    const res = await handleMessageDirect({146-      type: "SUBMIT_FEEDBACK",147-      payload: { category: "Bug report", message: "test", version: "2.1.0", browser: "Chrome" },148-    });149-    expect(res.ok).toBe(true);150-  });151-152-  it("returns ok:false when message is empty", async () => {153-    const res = await handleMessageDirect({154-      type: "SUBMIT_FEEDBACK",155-      payload: { category: "Bug report", message: "  ", version: "2.1.0", browser: "Chrome" },156-    });157-    expect(res.ok).toBe(false);158-    expect(res.error).toBe("message is empty");159-  });160-161-  it("does not refresh auto-lock timer", () => {162-    expect(shouldRefreshAutoLockPublic("SUBMIT_FEEDBACK")).toBe(false);163-  });164-});165-```166-167-Adapt to the existing test harness conventions in the project. If168-`handleMessageDirect` or `shouldRefreshAutoLockPublic` are not already169-exported for testing, export them or use whatever test helper pattern exists.170-171----172-173-## 4. `src/popup/views/Settings.tsx` — full rewrite174-175-This is the largest change. The file goes from a single 987-line flat scroll176-to a two-level navigation system: a home screen of category rows, each177-navigating to a focused sub-view.178-179-### 4a. Overall structure180-181-The outer component keeps the same `SettingsProps` interface (no changes to182-`App.tsx`). It gains a new `activeSection` signal that drives navigation.183-184-```ts185-type SectionName =186-  | "security"187-  | "vault"188-  | "sync"189-  | "appearance"190-  | "shortcuts"191-  | "license"192-  | "feedback";193-```194-195-The component returns a single `peach-overlay-view` div. The back button196-navigates: sub-view → home → `props.onBack()`. The title reflects the current197-section or reads `"settings"` on home.198-199-Use SolidJS `<Switch>/<Match>` (not early returns — see Projects.md §4.1):200-201-```tsx202-<Switch fallback={renderHome()}>203-  <Match when={activeSection() === "security"}>{renderSecurity()}</Match>204-  <Match when={activeSection() === "vault"}>{renderVault()}</Match>205-  <Match when={activeSection() === "sync"}>{renderSync()}</Match>206-  <Match when={activeSection() === "appearance"}>{renderAppearance()}</Match>207-  <Match when={activeSection() === "shortcuts"}>{renderShortcuts()}</Match>208-  <Match when={activeSection() === "license"}>{renderLicense()}</Match>209-  <Match when={activeSection() === "feedback"}>{renderFeedback()}</Match>210-</Switch>211-```212-213-All state signals from the current implementation are preserved at the top of214-`SettingsView` exactly as they are today. Nothing is split into child215-components — this is one file, render-function style, closing over shared216-signals. Follow "co-locate related logic" from AGENTS.md.217-218-Add the feedback signals alongside the existing ones:219-220-```ts221-const [fbCategory, setFbCategory] = createSignal("Bug report");222-const [fbMessage, setFbMessage] = createSignal("");223-const [fbSubmitting, setFbSubmitting] = createSignal(false);224-const [fbResult, setFbResult] = createSignal<"success" | "error" | null>(null);225-```226-227-Add the feedback submit handler:228-229-```ts230-async function handleSubmitFeedback() {231-  if (!fbMessage().trim() || fbSubmitting()) return;232-  setFbSubmitting(true);233-  setFbResult(null);234-  const res = await sendMessage({235-    type: "SUBMIT_FEEDBACK",236-    payload: {237-      category: fbCategory(),238-      message: fbMessage().trim(),239-      version,240-      browser: navigator.userAgent,241-    },242-  }) as { ok: boolean; error?: string };243-  setFbSubmitting(false);244-  if (res.ok) {245-    setFbResult("success");246-    setFbMessage("");247-    setTimeout(() => setFbResult(null), 4000);248-  } else {249-    setFbResult("error");250-  }251-}252-```253-254-### 4b. `renderHome()` — the category list255-256-Returns a `<div class="peach-settings-body">` containing two section groups257-and a footer. Each row uses the `.peach-settings-cat-row` class (new, defined258-in §5).259-260-```261-Group 1 label: "PROTECTION"262-  Row: Security        "Locking, breach monitoring, passkeys"   → "security"263-  Row: Account         "Plan & license key"                     → "license"264-265-Group 2 label: "VAULT"266-  Row: Vault           "Entries, health, import & export"       → "vault"267-  Row: Sync    [Pro]   "Paired devices & trusted networks"      → "sync"268-269-Group 3 label: "PREFERENCES"270-  Row: Appearance      "Theme & accent color"                   → "appearance"271-  Row: Shortcuts       "Keyboard reference"                     → "shortcuts"272-273-Group 4 label: "SUPPORT"274-  Row: Feedback        "Report issues, suggest features"        → "feedback"275-276-Footer: "Peach {version}  ·  Browser extension"  (same as current)277-```278-279-Each row renders as:280-281-```tsx282-<button283-  class="peach-settings-cat-row"284-  onClick={() => setActiveSection("security")}285->286-  <div class="peach-settings-cat-icon peach-settings-cat-icon--security">287-    {/* inline SVG shield icon, 14×14, stroke="var(--peach-accent)" */}288-  </div>289-  <div class="peach-settings-cat-content">290-    <span class="peach-settings-cat-name">Security</span>291-    <span class="peach-settings-cat-desc">Locking, breach monitoring, passkeys</span>292-  </div>293-  <svg class="peach-settings-cat-chevron" ...>›</svg>294-</button>295-```296-297-The Sync row also renders a Pro badge when `!isPro()`:298-299-```tsx300-<Show when={!isPro()}>301-  <span class="peach-sync-pro-badge">Pro</span>302-</Show>303-```304-305-The `peach-sync-pro-badge` class already exists in popup.css.306-307-**Icons** — all inline SVG, `width="14" height="14"`, `fill="none"`,308-`stroke="currentColor"` (inherits from icon container's color), `stroke-width="1.2"`,309-`stroke-linecap="round"`, `stroke-linejoin="round"`:310-311-| Section    | Icon path description                                         |312-|------------|---------------------------------------------------------------|313-| Security   | Shield: `<path d="M8 2L3 4.5V8c0 3 2.5 5 5 6 2.5-1 5-3 5-6V4.5L8 2z"/>` |314-| Account    | Key: `<path d="M4 10a3 3 0 100-6 3 3 0 000 6zm3-3h6m-2-2v4"/>` |315-| Vault      | Lock body+shackle: `<rect x="3" y="7" width="10" height="7" rx="1.5"/><path d="M6 7V5a3 3 0 016 0v2"/>` |316-| Sync       | Two arrows in circle: `<path d="M4 8a4 4 0 014-4m4 4a4 4 0 01-4 4"/><path d="M14 6l-2 2-2-2"/><path d="M2 10l2-2 2 2"/>` |317-| Appearance | Sun: `<circle cx="8" cy="8" r="3"/><path d="M8 2v1m0 10v1M2 8h1m10 0h1m-2.1-4.1-.7.7M4.8 11.3l-.7.7m0-7.7.7.7m6.5 6.3-.7.7"/>` |318-| Shortcuts  | Two keys: `<rect x="2" y="5" width="5" height="4" rx="1"/><rect x="9" y="7" width="5" height="4" rx="1"/><path d="M7 7h2m-2 2h2"/>` |319-| Feedback   | Comment bubble: `<path d="M3 4h10a1 1 0 011 1v6a1 1 0 01-1 1H5l-3 2V5a1 1 0 011-1z"/>` |320-321-**Section labels** use a new class `.peach-settings-nav-label` (defined in §5).322-323-### 4c. `renderSecurity()` — Security sub-view324-325-Content is exactly the existing "security" + "duress password" + "passkeys"326-groups from the current Settings.tsx, reorganised into three labelled327-sub-sections within a single `peach-settings-body`. No logic changes — copy328-the JSX and signals verbatim, just remove the outer `peach-settings-group`329-wrapper divs and replace with `peach-settings-sub-label` divs (new class,330-defined in §5).331-332-Sub-section labels:333-- `"LOCKING"` — Lock on close, Auto-lock after334-- `"BREACH PROTECTION"` — Breach monitoring toggle, Check all passwords button, last check result335-- `"MASTER PASSWORD"` — Change master password (inline expand form, same as current)336-- `"DURESS PASSWORD"` — The full duress setup/disable flow (same as current)337-- `"PASSKEYS"` — Intercept toggle, status, browser, conflict notice338-339-The `peach-info-notice` div for the passkey conflict note is kept verbatim.340-341-### 4d. `renderVault()` — Vault sub-view342-343-Takes the existing "vault" group and splits it into labelled sub-sections:344-345-- `"ENTRIES"` — Entries count (read-only), Duplicates (scan/remove flow, same as current)346-- `"MAINTENANCE"` — Trash auto-delete dropdown, Vault Health button347-- `"DATA"` — Import passwords, Export backup, Replace vault from backup (CJK + file buttons)348-349-No logic changes.350-351-### 4e. `renderSync()` — Sync sub-view352-353-This is the existing `renderSyncSection()` body verbatim. Remove the outer354-`peach-settings-group` wrapper; the body is now the sub-view itself.355-The Pro gate (blurred rows + overlay) works exactly as before.356-357-### 4f. `renderAppearance()` — Appearance sub-view358-359-Contains the two rows currently buried in the "vault" group:360-361-- Accent color picker (`.peach-accent-picker` grid — same as current)362-- Theme toggle button363-364-Add a sub-section label `"THEME"` above the theme row and `"ACCENT"` above365-the accent picker.366-367-### 4g. `renderShortcuts()` — Shortcuts sub-view368-369-Merges the two existing keyboard shortcut groups (currently duplicated):370-371-- One row: "Inline popup shortcut" with the `Ctrl+Shift+Space — customize ↗` link372-- The full `.peach-shortcuts-grid` (C, U, /, ↑↓, Esc, ⌘⇧P)373-374-No duplication. The customize link opens `chrome://extensions/shortcuts`375-exactly as before.376-377-### 4h. `renderLicense()` — License/Account sub-view378-379-The existing "license" group body verbatim. Label at top: `"PLAN"`.380-381-### 4i. `renderFeedback()` — Feedback sub-view382-383-```tsx384-function renderFeedback() {385-  const categories = ["Bug report", "Feature request", "Question", "Other"];386-  return (387-    <div class="peach-settings-body">388-      <div class="peach-settings-group">389-        <p class="peach-feedback-intro">390-          Help us improve Peach. Every report goes directly to our inbox.391-        </p>392-393-        <p class="peach-settings-sub-label">TYPE</p>394-        <div class="peach-feedback-chips">395-          <For each={categories}>396-            {(cat) => (397-              <button398-                class={`peach-feedback-chip ${fbCategory() === cat ? "is-active" : ""}`}399-                onClick={() => setFbCategory(cat)}400-              >401-                {cat}402-              </button>403-            )}404-          </For>405-        </div>406-407-        <p class="peach-settings-sub-label">MESSAGE</p>408-        <textarea409-          class="peach-feedback-textarea"410-          placeholder="Describe the issue or idea..."411-          value={fbMessage()}412-          onInput={(e) => setFbMessage(e.currentTarget.value)}413-          rows={5}414-        />415-416-        <p class="peach-feedback-meta">417-          Version {version} · will be included automatically418-        </p>419-420-        <Show when={fbResult() === "success"}>421-          <p class="peach-feedback-result peach-feedback-result--ok">422-            Sent — thank you423-          </p>424-        </Show>425-        <Show when={fbResult() === "error"}>426-          <p class="peach-feedback-result peach-feedback-result--err">427-            Failed to send — try again428-          </p>429-        </Show>430-431-        <button432-          class="peach-btn peach-btn--accent peach-btn--sm peach-feedback-submit"433-          onClick={() => void handleSubmitFeedback()}434-          disabled={fbSubmitting() || !fbMessage().trim()}435-        >436-          {fbSubmitting() ? "Sending..." : "Send feedback"}437-        </button>438-439-        <p class="peach-feedback-anon">440-          Sent anonymously · no account required441-        </p>442-      </div>443-    </div>444-  );445-}446-```447-448-### 4j. Imports449-450-Add `Switch`, `Match`, and `For` to the SolidJS import at the top:451-452-```ts453-import { createSignal, For, Match, onMount, Show, Switch } from "solid-js";454-```455-456----457-458-## 5. `src/popup/styles/popup.css` — new CSS rules459-460-Append the following block after the existing `/* ── Settings view ── */`461-section (after line ~1714, before the `.peach-passkey-badge` rule). Do not462-modify any existing rules.463-464-```css465-/* ── Settings home navigation ──────────────────────────────────────────── */466-.peach-settings-nav-label {467-  margin: 0 0 2px;468-  padding: 12px 16px 4px;469-  font-family: "IBM Plex Mono", monospace;470-  font-size: 8px;471-  text-transform: uppercase;472-  letter-spacing: 0.18em;473-  color: var(--peach-t2);474-}475-476-.peach-settings-cat-row {477-  display: flex;478-  align-items: center;479-  gap: 10px;480-  width: 100%;481-  padding: 9px 16px;482-  background: none;483-  border: none;484-  border-bottom: 1px solid var(--peach-t3);485-  cursor: pointer;486-  text-align: left;487-  color: inherit;488-  transition: background var(--peach-dur-80) ease;489-}490-.peach-settings-cat-row:hover {491-  background: var(--peach-t3);492-}493-.peach-settings-cat-row:last-child {494-  border-bottom: none;495-}496-497-.peach-settings-cat-icon {498-  width: 28px;499-  height: 28px;500-  border-radius: 7px;501-  background: color-mix(in srgb, var(--peach-accent) 10%, transparent);502-  color: var(--peach-accent);503-  display: flex;504-  align-items: center;505-  justify-content: center;506-  flex-shrink: 0;507-}508-509-.peach-settings-cat-content {510-  flex: 1;511-  display: flex;512-  flex-direction: column;513-  gap: 2px;514-  min-width: 0;515-}516-517-.peach-settings-cat-name {518-  font-size: 13px;519-  font-weight: 450;520-  color: var(--peach-t0);521-  line-height: 1.2;522-}523-524-.peach-settings-cat-desc {525-  font-size: 10px;526-  color: var(--peach-t2);527-  line-height: 1.3;528-  white-space: nowrap;529-  overflow: hidden;530-  text-overflow: ellipsis;531-}532-533-.peach-settings-cat-chevron {534-  color: var(--peach-t2);535-  flex-shrink: 0;536-  opacity: 0.5;537-}538-539-/* ── Settings sub-view section labels ──────────────────────────────────── */540-.peach-settings-sub-label {541-  margin: 12px 0 4px;542-  font-family: "IBM Plex Mono", monospace;543-  font-size: 8px;544-  text-transform: uppercase;545-  letter-spacing: 0.18em;546-  color: var(--peach-t2);547-}548-.peach-settings-sub-label:first-child {549-  margin-top: 4px;550-}551-552-/* ── Feedback form ──────────────────────────────────────────────────────── */553-.peach-feedback-intro {554-  margin: 4px 0 14px;555-  font-size: 12px;556-  line-height: 1.55;557-  color: var(--peach-t1);558-}559-560-.peach-feedback-chips {561-  display: flex;562-  flex-wrap: wrap;563-  gap: 6px;564-  margin-bottom: 14px;565-}566-567-.peach-feedback-chip {568-  padding: 4px 10px;569-  border-radius: 999px;570-  border: 1px solid var(--peach-t3);571-  background: none;572-  color: var(--peach-t2);573-  font-size: 11px;574-  font-family: var(--font-ui);575-  cursor: pointer;576-  transition: border-color var(--peach-dur-80) ease, color var(--peach-dur-80) ease,577-    background var(--peach-dur-80) ease;578-}579-.peach-feedback-chip:hover {580-  border-color: var(--peach-t2);581-  color: var(--peach-t1);582-}583-.peach-feedback-chip.is-active {584-  border-color: color-mix(in srgb, var(--peach-accent) 50%, transparent);585-  background: color-mix(in srgb, var(--peach-accent) 8%, transparent);586-  color: var(--peach-accent);587-}588-589-.peach-feedback-textarea {590-  width: 100%;591-  box-sizing: border-box;592-  background: var(--peach-t3);593-  border: 1px solid var(--peach-t3);594-  border-radius: 6px;595-  color: var(--peach-t0);596-  font-size: 12px;597-  font-family: var(--font-ui);598-  line-height: 1.5;599-  padding: 8px 10px;600-  resize: none;601-  outline: none;602-  transition: border-color var(--peach-dur-150) ease;603-}604-.peach-feedback-textarea:focus {605-  border-color: color-mix(in srgb, var(--peach-accent) 40%, transparent);606-}607-.peach-feedback-textarea::placeholder {608-  color: var(--peach-t2);609-}610-611-.peach-feedback-meta {612-  margin: 6px 0 0;613-  font-size: 10px;614-  color: var(--peach-t2);615-  font-family: "IBM Plex Mono", monospace;616-}617-618-.peach-feedback-result {619-  margin: 8px 0 0;620-  font-size: 11px;621-  font-family: "IBM Plex Mono", monospace;622-}623-.peach-feedback-result--ok { color: var(--peach-sync-ok); }624-.peach-feedback-result--err { color: var(--peach-breach); }625-626-.peach-feedback-submit {627-  width: 100%;628-  margin-top: 12px;629-  justify-content: center;630-}631-632-.peach-feedback-anon {633-  margin: 8px 0 0;634-  font-size: 10px;635-  color: var(--peach-t2);636-  text-align: center;637-}638-```639-640----641-642-## 6. `App.tsx` — no changes required643-644-The `SettingsProps` interface is unchanged. The `<SettingsView>` call site in645-`App.tsx` does not need modification.646-647----648-649-## 7. `.agent/CONTINUITY.md` — create/update650-651-Create `.agent/CONTINUITY.md` if it does not exist. Add an entry:652-653-```markdown654-## [PLANS]655-- 2026-04-26T00:00Z [USER] Settings screen redesign: two-level navigation656-  (home → category sub-views) + in-settings feedback form with configurable657-  backend endpoint. Explicit user authorisation to proceed despite Projects.md658-  §8 "no new features" freeze.659-660-## [DECISIONS]661-- 2026-04-26T00:00Z [CODE] Kept all state signals in top-level SettingsView662-  rather than splitting into child components — avoids prop drilling, keeps663-  co-location, and the file was already a single component. Render functions664-  close over shared signals.665-- 2026-04-26T00:00Z [CODE] FEEDBACK_ENDPOINT defaults to empty string so666-  SUBMIT_FEEDBACK is a no-op until the developer sets the URL. Reuses the667-  existing isAllowedCanaryWebhookUrl validator — no new validation surface.668-- 2026-04-26T00:00Z [ASSUMPTION] No changes to App.tsx required; SettingsProps669-  interface is unchanged.670-671-## [PROGRESS]672-  (fill in as work proceeds)673-674-## [DISCOVERIES]675-- Keyboard shortcuts section was duplicated in the old Settings.tsx (lines 608676-  and 965). Fixed by merging into a single Shortcuts sub-view.677-- The "vault" group was a catch-all for 9 unrelated concerns. Split into:678-  Vault (data ops), Appearance (theme/accent), Sync (devices/networks).679-```680-681----682-683-## 8. Verification gate684-685-Run the full sequence from Projects.md §2 before the final save:686-687-```sh688-# Network surface check — must return zero results689-grep -r "fetch(" src/ | grep -v "pwnedpasswords" | grep -v "webhookUrl" | grep -v "FEEDBACK_ENDPOINT" | grep -v "\.test\."690-691-# TypeScript — zero errors692-npx tsc --noEmit693-694-# Tests — zero failures695-npm test696-697-# Build — no new errors698-npm run build699-700-# Version consistency701-grep '"version"' package.json manifest.json manifest.firefox.json702-# All three must show 2.1.0703-```704-705----706-707-## 9. Koh save sequence708-709-Use session name `"settings redesign and feedback"` on every save.710-711-```sh712-# Pass 1 — scaffolding713-koh save --agent "<model>" --session "settings redesign and feedback" \714-  "add SUBMIT_FEEDBACK message type, handler, feedback endpoint constant, version bump to 2.1.0"715-716-# Pass 2 — Settings.tsx717-koh save --agent "<model>" --session "settings redesign and feedback" \718-  "rewrite Settings.tsx: two-level navigation, seven sub-views, feedback form"719-720-# Pass 3 — CSS721-koh save --agent "<model>" --session "settings redesign and feedback" \722-  "add category nav and feedback form CSS rules to popup.css"723-724-# Pass 4 — tests + CONTINUITY725-koh save --agent "<model>" --session "settings redesign and feedback" \726-  "add SUBMIT_FEEDBACK handler tests, update CONTINUITY.md and Projects.md §3.7"727-```728-729----730-731-## 10. What is intentionally out of scope732-733-- **No changes to `App.tsx`** — the `SettingsProps` interface and call site734-  are unchanged.735-- **No actual feedback backend** — `FEEDBACK_ENDPOINT` is left empty. The736-  implementing agent must not invent or hardcode a URL.737-- **No animation changes** — the overlay enter/exit animations (`ov-enter-right`)738-  are not modified. Sub-view transitions within settings are not animated.739-- **No P0/P1 findings** — do not fix any items from Projects.md §6 in this740-  task. If you notice something while reading, add it to CONTINUITY.md as a741-  discovery and leave the code alone.742-- **No `App.tsx` warm-up list changes** — Settings is not a lazy-loaded view743-  so §4.3 does not apply here.

view file at the parent

SYNC_STATUS.md+0 −309
@@ -1,309 +0,0 @@1-# Peach Sync — Honest Status2-3-Snapshot as of 2026-04-30, HEAD `7d738aa11c96`, package v2.14.9, native v0.1.14.4-5-This document distinguishes what is **implemented and verified**, what is6-**implemented but not yet exercised on real hardware** (compiles, tests in7-isolation, walking-skeleton ready but not yet pair-tested between two real8-devices), and what is **deliberately deferred**.9-10-The previous version of this document (under v2.13.33) marked the system as11-"two devices cannot sync today." That is no longer accurate. The blockers it12-listed (B1–B5, M1–M8) have all been resolved in the v2.14.x rewrite per13-`peach_sync_and_history_spec.md`. The only remaining gate is M9 — the manual14-two-device E2E — which the user (June) is running personally on their daily15-driver.16-17----18-19-## TL;DR — can two devices sync today?20-21-**Yes, by the walking skeleton:**22-23-1. ✓ `runBlobSyncSession` is wired into both `TRIGGER_SYNC` (manual) and24-   the discovered-peer auto-sync (60s debounced).25-2. ✓ First-time pairing succeeds — native host gates an `pairing_mode`26-   flag that admits otherwise-untrusted peers; `INITIATE_PAIR_WITH_PEER`27-   actually sends `PAIR_REQUEST` over the encrypted Noise channel and28-   awaits `PAIR_ACCEPT` / `PAIR_REJECT` (30s timeout).29-3. ✓ Android runs Noise NK in JS via `@stablelib`. Cross-check test pins30-   the auth-proof hex against the Rust side byte-for-byte.31-4. ✓ Auto-advertise lifecycle (`sync-lifecycle.ts`) calls `advertise()`32-   when Pro + unlocked + paired devices > 0, stops on lock.33-34-What's still unverified is **manual two-device E2E** (Phase 10 in the35-spec checklist) — that requires a release host build + manifest install36-+ two devices on the same network. June will run this manually since37-this is their daily driver.38-39----40-41-## What is solid (implemented + verified)42-43-### Native Rust host (`native/peach-sync-host/`, v0.1.14)44-45-- **Per-install X25519 identity** (`identity.rs`) — generates on first run,46-  persisted at `<config_dir>/peach-sync-identity.key` mode 0o600 on Unix,47-  bails on corrupt keys. Tested.48-- **Noise NK transport** (`noise.rs`) — snow-based, X25519+ChaChaPoly+BLAKE2s.49-  HKDF-SHA256 over the captured handshake hash for derived session keys.50-  Tested.51-- **mDNS** (`mdns.rs`) — advertises and browses `_peach-sync._tcp.local.`,52-  TXT records `pk` / `did` / `name` / `pair` / `wsport`. Tested.53-- **TCP transport** (`tcp.rs`) — listener at OS-assigned port, length-prefix54-  framing, full Noise handshake, HMAC-SHA256 challenge/proof auth round,55-  transport-mode encrypt/decrypt, per-IP rate limiting (1 conn/sec/IP, 60s56-  pruning) **shared with WebSocket via `SharedConnectionState`** (M7 fix in57-  v2.14.6), explicit `FrameWriter::shutdown()` on disconnect (M8 fix in58-  v2.14.6 — TCP `write_half.shutdown()` / WS `Message::Close(None)`).59-  Loopback initiator↔responder integration test passes.60-- **WebSocket transport** (`websocket.rs`) — same Noise/auth/transport stack61-  over `tokio_tungstenite` with one Binary message per logical frame, shares62-  the connection map / channels / **rate limiter** with TCP. Loopback test63-  against a raw `tokio_tungstenite` client passes.64-- **Pairing-mode flag** (`HostCore::pairing_mode: Arc<AtomicBool>`,65-  v2.14.2) — toggled by `EnterPairingMode` / `ExitPairingMode` host66-  messages. When true, `authenticate_responder_side` returns67-  `AuthOutcome { is_pairing_only: true }` for otherwise-untrusted peers68-  whose auth proof verifies. The `IncomingConn` event carries69-  `is_pairing_only` + `peer_public_key_hex` so the extension can route70-  accordingly.71-- **Trusted devices store** (`trusted_devices.rs`) — JSON file in config72-  dir. Tested.73-- **Native messaging framing** — u32-length-prefixed JSON over stdin/stdout,74-  1 MiB cap, parse errors do not kill the host.75-- **`cargo test`** — 30 passing, including the new76-  `print_known_proof_for_js_cross_check` that pins the JS↔Rust auth-proof77-  hex to `39efee52ccc9d7eaa24fcfec9a4c75eaad75a6bf3319d9794d9227f2b1466752`.78-79-### Extension TypeScript80-81-- **`src/background/sync.ts`** — full client mirror of the host protocol.82-  In v2.14.2 added `enterPairingMode` / `exitPairingMode` /83-  `getIdentityPublicKey` and reshaped `onIncomingConnection` to deliver84-  `IncomingConnectionInfo { connectionId, peerAddr, isPairingOnly,85-  peerPublicKeyHex }`.86-- **`src/shared/sync-types.ts`** — `SyncMessage` union rewritten to the87-  blob protocol: `SYNC_VAULT_SEND` / `SYNC_VAULT_ACK` / `SYNC_ERROR` /88-  `PAIR_REQUEST` / `PAIR_ACCEPT` / `PAIR_REJECT`. The old per-entry89-  variants are gone. `IncomingPairingRequest` carries the live90-  `connectionId` so accept/reject can ride the still-open pairing91-  connection.92-- **`src/shared/sync-wire.ts`** — base64url JSON-over-Noise codec with93-  per-variant validators for the new union. Tested.94-- **`src/background/sync-coordinator.ts`** —95-    - `applyIncomingVault(vault_a, vault_b, sent_at)` — last-write-wins96-      against `peach_last_sync_at`; snapshots the about-to-be-overwritten97-      blob into vault history with reason `sync_received` BEFORE writing.98-    - `runBlobSyncSession(role, transport, deps)` — bidirectional99-      exchange. Initiator sends first; responder applies-if-newer +100-      ACK + sends back; initiator applies-if-newer + ACK + done. 30s101-      timeout. Tested.102-    - `waitForPairingResponse(connectionId, timeoutMs, bindings?)` — used103-      by `INITIATE_PAIR_WITH_PEER` to await `PAIR_ACCEPT` / `PAIR_REJECT`104-      after sending a `PAIR_REQUEST` over the encrypted channel.105-    - `createNativeSyncTransport(connectionId, bindings?)` — adapter to106-      `SyncSessionTransport`, scoped to a connection id. Tested.107-- **`src/background/sync-lifecycle.ts`** (new in v2.14.3) — owns:108-    - `syncLifecycleTick()` — advertise iff Pro + unlocked + paired109-      devices > 0; stop otherwise.110-    - `setupIncomingPairingSubscriber()` — listens on111-      `sync.onIncomingConnection`, reads the first frame on112-      pairing-only connections, decodes `PAIR_REQUEST`, populates113-      `IncomingPairingRequest` with the live `connectionId`, and pings114-      the popup with `INCOMING_PAIRING_REQUEST`.115-    - `getDiscoveredPeerCache()` — module-local mDNS peer cache fed by116-      `onPeerDiscovered`/`onPeerLost` so `TRIGGER_SYNC` can quickly117-      enumerate reachable paired devices.118-    - `maybeAutoSync(peer)` — fires on `onPeerDiscovered` for known119-      paired peers, with a 60s debounce against `peach_last_sync_at`.120-    - `onNativeHostConnected` / `onNativeHostDisconnected` /121-      `onVaultUnlocked` / `onVaultLocked` / `onProActivated` lifecycle122-      hooks called from `entry.ts` and `alarms.ts`.123-- **`src/shared/noise-js.ts`** (new in v2.14.5) — JS Noise NK initiator124-  using `@stablelib/x25519` / `chacha20poly1305` / `blake2s` / `hkdf` /125-  `sha256` / `hmac`. `computeAuthProof` matches `auth.rs::proof_for`126-  byte-for-byte. The `noise-js.test.ts` cross-check pins the JS hex to127-  `39efee52…` — divergence from the Rust side fails the test128-  immediately.129-- **`src/shared/capacitor-sync-transport.ts`** — opens130-  `ws://host:wsport/`, runs Noise NK + auth, then exposes a131-  `SyncSessionTransport`. WS Binary message ≡ one logical frame; no132-  length prefix.133-- **`src/shared/android-sync-identity.ts`** — persistent X25519 keypair134-  via the Capacitor `SyncKey` plugin. JS handles key generation; the135-  Kotlin plugin is a typed encrypted-blob store (still TBD on the136-  Kotlin side; sync-bridge.ts will throw cleanly if invoked before that137-  ships).138-- **`src/shared/sync-bridge.ts`** — `getSyncBridge()` returns139-  `CapacitorSyncBridge` (NsdManager via Sync plugin) on Capacitor,140-  `NativeMessagingSyncBridge` on extension. Now exposes141-  `syncWithPeer(peer)` on the Capacitor side that wires142-  `getAndroidSyncIdentity` → `createCapacitorSyncTransport` →143-  `runBlobSyncSession`.144-- **Settings UX** (Settings.tsx) — Sync section: real `Sync now` button145-  with `Syncing… / ✓ Synced N / No devices found / Vault is locked /146-  Sync failed / Sync now` feedback states; last-sync time;147-  simultaneous-edit hint pointing at Vault History.148-- **Vault History UI** (Settings.tsx renderVault) — new VAULT HISTORY149-  group at the bottom of Settings → Vault. 10 rows max, each showing150-  time / entry count / reason label / Restore button. Restoring locks151-  the vault; user re-unlocks with the password that encrypted that152-  snapshot.153-- **`vitest`** — 281 passing, including 14 vault-history cases and 8154-  blob-sync session cases.155-156-### Vault History (`peach_vault_history`, new in v2.14.0)157-158-Replaces the old `peach_vault_snapshots` system. Snapshots the159-about-to-be-overwritten encrypted blob BEFORE every real-mode vault160-write. Cap 10. Reasons: `local_save` / `sync_received` / `import` /161-`restore`. Dedup on byte-identical blobs. Restoring locks the vault.162-DESTROY_VAULT clears history.163-164-The Lock-screen self-recovery UI that used the old system (which let a165-user attempt a forgotten password against an old snapshot) was removed166-— that was a different feature from "undo a recent sync overwrite."167-Forgotten-password recovery may be brought back in a future spec.168-169-### Android170-171-- **`SyncPlugin.kt`** — NsdManager-based mDNS browser of172-  `_peach-sync._tcp.`, WiFi MulticastLock acquired during discovery,173-  parses TXT records, emits Capacitor `peerDiscovered` / `peerLost`174-  listeners. Tested via `./gradlew assembleDebug`.175-- **`SyncKey.kt` Capacitor plugin** — TBD. The TS side176-  (`android-sync-identity.ts`) is ready and will throw cleanly if177-  invoked before the Kotlin plugin lands. Plugin spec: persistent178-  X25519 keypair via encrypted SharedPreferences,179-  `loadKeypair`/`saveKeypair` returning hex.180-- `INTERNET`, `ACCESS_WIFI_STATE`, `CHANGE_WIFI_MULTICAST_STATE`,181-  `CHANGE_NETWORK_STATE` permissions present in `AndroidManifest.xml`.182-183-### Verification matrix (all green as of v2.14.9)184-185-- `cargo test` — 30/30186-- `npx tsc --noEmit` — clean187-- `npm test` — 281/281188-- `npm run build:chrome` / `:firefox` — clean (only the pre-existing189-  dynamic-import warning on `Import.tsx`)190-191----192-193-## Spec deviations worth knowing194-195-### Auth proof scheme196-197-The spec text (Part 8.3) showed an auth proof computed as198-`SHA256(DH || "peach-sync-auth/v1" || iPub || rPub || nonce)`. The199-existing native `auth.rs::proof_for` already used a different scheme:200-`HMAC-SHA256(key=DH, "peach-sync-v1-initiator-auth" || iPub || rPub ||201-nonce)`. The JS implementation in `noise-js.ts::computeAuthProof`202-matches the native side (source of truth) — both are pinned by the203-`print_known_proof_for_js_cross_check` test in `auth.rs` and the204-matching test in `noise-js.test.ts`. Change one, the other fails.205-206-### Vault history replaces lock-screen recovery207-208-The spec replaced `peach_vault_snapshots` with `peach_vault_history`. A209-side effect was removing the lock-screen UI that let a user attempt a210-forgotten password against one of the last 6 snapshots. The new system211-only helps when you can already unlock — it's "undo a recent sync212-overwrite," not "recover from a forgotten password." This is by design213-per the spec, but if forgotten-password recovery is wanted back it214-needs a separate spec.215-216-### `unlimitedStorage` not requested217-218-10 × ~200KB blobs + slot A/B + settings fits comfortably under219-`chrome.storage.local`'s 5MB quota. If vaults grow past ~400 entries,220-history alone could approach 4MB; consider adding `unlimitedStorage`221-then. Not requested in v2.14.x.222-223----224-225-## What still needs hands-on verification226-227-### M9. Manual two-device E2E228-229-The only honest verification that the system works end-to-end. Steps:230-231-1. `cargo build --release --manifest-path232-   native/peach-sync-host/Cargo.toml`233-2. `npm run native:install -- --browser chrome --extension-id234-   <extension-id>` on each device.235-3. Pair via Settings → Sync → "Pair a new device" on Device A; accept on236-   Device B's pairing banner.237-4. Verify `peach_paired_devices` populated on both sides; verify both238-   sides' trust stores carry each other's public key.239-5. Make a vault change on Device A; tap Sync now → Device B should240-   show the change.241-6. Make a vault change on Device B; tap Sync now on Device A → should242-   pull the change.243-7. Auto-sync: change something on A while B is also unlocked and244-   advertising → within 60s, B should pick it up via the245-   `onPeerDiscovered` auto-sync path.246-8. Vault History: open Settings → Vault → VAULT HISTORY; verify247-   snapshots from the writes above appear with correct reasons. Tap248-   Restore on a recent one; vault locks; unlock with current password249-   succeeds.250-9. Three devices: same flow with phone (once Kotlin `SyncKey` plugin251-   lands) → desktop → laptop. `TRIGGER_SYNC` should walk the reachable252-   paired devices sequentially.253-10. After E2E passes, run `koh checkpoint "SYN-001 + vault history E2E254-    verified"`.255-256-This is being run by June manually — daily-driver verification.257-258-### Kotlin `SyncKey` Capacitor plugin259-260-Required for Android sync. Not included in the v2.14.x TS-side rewrite.261-Until it ships, the Capacitor sync bridge will throw cleanly when262-`getAndroidSyncIdentity()` is invoked. Desktop↔desktop sync is263-unaffected.264-265----266-267-## What changed from the v2.13.33 status268-269-For posterity / reviewers comparing the old document to this one:270-271-- **B1 (responder hard-rejects untrusted)** — RESOLVED v2.14.2.272-  Pairing-mode flag admits the connection; `AuthOutcome` distinguishes273-  trusted from pairing-only.274-- **B2 (PAIR_* dispatcher stubs)** — RESOLVED v2.14.2.275-  `INITIATE_PAIR_WITH_PEER` sends real `PAIR_REQUEST` over the276-  authenticated Noise channel; `RESPOND_INCOMING_PAIRING` sends real277-  `PAIR_ACCEPT` / `PAIR_REJECT` over the saved connection.278-- **B3 (banner never fires from real traffic)** — RESOLVED v2.14.3.279-  `setupIncomingPairingSubscriber` in `sync-lifecycle.ts` reads the280-  first frame off pairing-only connections and populates the281-  IncomingPairingRequest from real traffic.282-- **B4 (extension never advertises)** — RESOLVED v2.14.3. Auto-advertise283-  lifecycle ticks on vault unlock / lock / native-host connect /284-  Pro change.285-- **B5 (TRIGGER_SYNC stub)** — RESOLVED v2.14.4. `TRIGGER_SYNC` now286-  enumerates reachable paired devices, runs `runBlobSyncSession`287-  sequentially against each, returns `{ syncedCount, totalReachable,288-  results[] }`.289-- **M1 (JS Noise NK)** — RESOLVED v2.14.5. `noise-js.ts` +290-  `capacitor-sync-transport.ts` + `android-sync-identity.ts`. JS↔Rust291-  cross-check test pins the proof hex.292-- **M2 (pairing protocol)** — RESOLVED v2.14.2 (native +293-  extension wiring).294-- **M3 (live trigger)** — RESOLVED v2.14.4 (TRIGGER_SYNC + on-discover295-  auto-sync).296-- **M4 (PEER_CONNECTED → banner)** — RESOLVED v2.14.3.297-- **M5 (re-implement INITIATE_PAIR_WITH_PEER)** — RESOLVED v2.14.2.298-- **M6 (auto-advertise on Pro+unlocked)** — RESOLVED v2.14.3.299-- **M7 (shared rate limiter)** — RESOLVED v2.14.6.300-- **M8 (clean shutdown)** — RESOLVED v2.14.6 (`FrameWriter::shutdown`).301-- **M9 (manual E2E)** — DEFERRED to user, daily-driver verification302-  pending.303-304-Plus, since the v2.13.33 doc was written, the entire per-entry sync305-protocol was retired and replaced with a blob-transfer protocol. At306-~200KB for an 800-entry vault local-WiFi transfer takes under 200ms,307-which made the whole manifest/diff/conflict-resolver layer308-unnecessary. Vault history is the safety net for last-write-wins. See309-Projects.md §3.9 + §3.12 for the full architecture.

view file at the parent

audit2.29.3.md+0 −275
@@ -1,275 +0,0 @@1-# Peach v2.29.3 — Audit Report2-3----4-5-## Section 1 — Sync: Dead Code Inventory6-7-### What exists8-9-**Native host binary (`native/peach-sync-host/`):** EXISTS — full Rust crate with Cargo.lock, Cargo.toml, and `src/` containing `lib.rs`, `main.rs`, `mdns.rs`, `message.rs`, `noise.rs`, `tcp.rs`.10-11-**`nativeMessaging` permission:** Present in **both** `manifest.json` (line 55) and `manifest.firefox.json` (line 80). The comment in `sync.ts` explicitly says to keep it out of manifests until SYN-001 passes. **The permission is in the manifests prematurely.**12-13-**`src/background/sync.ts` (553 lines):** Marked `GATED` at the top — explicitly states it is not imported by any live background path. Contains `connectNative`, the full native host protocol, all message types.14-15-**Other sync files:**16-17-| File | Lines | Purpose |18-|---|---|---|19-| `src/background/sync-coordinator.ts` | 356 | Blob sync session logic — keep |20-| `src/background/sync-lifecycle.ts` | 385 | Drives native host advertisement + mDNS lifecycle — imports sync.ts |21-| `src/shared/sync-bridge.ts` | 238 | Android NSD bridge — keep |22-| `src/shared/noise-js.ts` | 397 | Noise NK JS implementation — keep |23-| `src/shared/capacitor-sync-transport.ts` | 226 | WebSocket transport for Android — keep |24-| `src/shared/android-sync-identity.ts` | 90 | X25519 keypair management — keep |25-| `src/popup/views/SyncPairing.tsx` | 323 | Pairing UI (show + enter modes) — keep |26-27-**Installer scripts:** `scripts/generate-native-sync-manifest.mjs`, `scripts/install-native-sync-host.mjs`, `scripts/sync-chrome-dist.mjs` (last one is unrelated to native host; keep it).28-29-**Sync message types in `types.ts`:** 14+ sync/pairing message types — all relevant to the WebSocket path; keep them.30-31-### Verdict32-33-**Delete cleanly:**34-- `native/peach-sync-host/` — entire Rust directory35-- `src/background/sync.ts` — native host client only, nothing here belongs to the WebSocket path36-- `scripts/generate-native-sync-manifest.mjs`37-- `scripts/install-native-sync-host.mjs`38-- Remove `nativeMessaging` from both manifests39-40-**Delete with surgery:**41-- `src/background/sync-lifecycle.ts` — currently imports and drives `sync.ts`; the mDNS advertisement it manages belongs to the native host. Either delete entirely and replace with a lightweight desktop-side sync coordinator, or strip the native host parts and keep only the blob-sync scheduling logic.42-43-**Keep intact:**44-- `noise-js.ts`, `sync-bridge.ts`, `capacitor-sync-transport.ts`, `android-sync-identity.ts`, `sync-coordinator.ts`, `SyncPairing.tsx`45-46-**Build from scratch:**47-- Mayan code (QR) pairing UI — not present at all48-- `SCAN_PAIRING_QR` message handler49-- Subnet scan for peer discovery50-- Desktop-side sync-lifecycle replacement (decoupled from native host)51-52----53-54-## Section 2 — Kotlin Plugins: What Exists and What's Wired55-56-All six plugins are registered in `MainActivity`: `AutofillPlugin`, `BiometricPlugin`, `InMemoryVaultPlugin`, `SyncPlugin`, `SyncKeyPlugin`, `FoldPlugin`.57-58-### AssistStructureParser.kt59-Helper class (no plugin methods). Traverses autofill `AssistStructure` to extract `usernameId`, `passwordId`, `webDomain`, and `packageName`. Used only by `PeachAutofillService`.60-61-### AutofillPlugin.kt — 5 plugin methods62-63-| Method | What it does |64-|---|---|65-| `checkPendingSave()` | Reads `peach_pending_save` from SharedPreferences; consumed on read |66-| `openAutofillSettings()` | Opens system autofill settings screen |67-| `isEnabledAsAutofillService()` | Returns whether Peach is the active autofill service |68-| `consumePendingAutofillRequest()` | Reads `autofill_domain`/`autofill_package` from intent; consumed on read |69-| `completeAutofillAuthentication()` | After unlock, constructs `FillResponse` and returns via `EXTRA_AUTHENTICATION_RESULT` |70-71-Fully wired to JS. The `completeAutofillAuthentication()` path is complete — it reconstructs field IDs from intent extras and builds datasets from `VaultBridge.findEntries()`.72-73-### BiometricPlugin.kt — 2 plugin methods74-75-| Method | What it does |76-|---|---|77-| `authenticate()` | Shows `BiometricPrompt.PromptInfo`, resolves `{ verified: true }` or rejects |78-| `isAvailable()` | Checks `BIOMETRIC_STRONG` via `BiometricManager` |79-80-Correctly implemented on the Kotlin side. Wired to `src/shared/biometric.ts`. Gap is in JS layer (see Section 5).81-82-### FoldPlugin.kt — 2 plugin methods83-84-| Method | What it does |85-|---|---|86-| `startMonitoring()` | Starts `WindowInfoTracker` collector; emits `postureChanged` events |87-| `stopMonitoring()` | Cancels coroutine |88-89-Used in `App.tsx` via `startFoldMonitoring()`. Fully functional. Sets `data-posture` and `--peach-hinge-height` CSS variable.90-91-### InMemoryVaultPlugin.kt — 2 plugin methods92-93-| Method | What it does |94-|---|---|95-| `setEntries()` | Deserializes JSON entries into `InMemoryVault` singleton |96-| `clearEntries()` | Wipes `InMemoryVault` |97-98-Wired correctly. `InMemoryVault.kt` is the in-memory singleton — not a plugin itself.99-100-### SyncPlugin.kt — 8 plugin methods101-102-| Method | What it does |103-|---|---|104-| `startDiscovery()` | NSD `_peach-sync._tcp` discovery with MulticastLock |105-| `stopDiscovery()` | Stops NSD discovery |106-| `getDiscoveredPeers()` | Returns resolved peer map |107-| `resolvePeerByCode()` | UDP broadcast on port 44101 to locate peer by 6-digit token |108-| `startPairingTcpServer()` | Opens `ServerSocket(0)`, pumps bytes as base64 `pairingChunk` events |109-| `sendPairingChunk()` | Writes raw bytes to connected pairing socket |110-| `startPairingBeacon()` | UDP listener on 44101; responds to `PEACH_PAIR_QUERY` with `PEACH_PAIR_RESPONSE` |111-| `stopPairingServer()` | Closes all sockets, clears state |112-113-Both sides of pairing implemented: enter-mode (UDP broadcast → `resolvePeerByCode`) and show-mode (TCP server + UDP beacon). Wired to `sync-bridge.ts`.114-115-### SyncKeyPlugin.kt — 3 plugin methods116-Stores X25519 keypair in `EncryptedSharedPreferences` (Keystore-backed AES256-GCM). Validates key length (64 hex = 32 bytes) on save. Wired to `android-sync-identity.ts`.117-118-### PeachAutofillService.kt (118 lines)119-System service, not a Capacitor plugin. Uses `VaultBridge` directly. See Section 6.120-121-### VaultBridge.kt122-Kotlin singleton. `isUnlocked()` delegates to `InMemoryVault.isAvailable()`. `findEntries()` has tiered domain matching: exact host (100) > base-domain (80) > subdomain (70/60) > package hint (50) > substring (40/25). Used by both `PeachAutofillService` and `AutofillPlugin`.123-124----125-126-## Section 3 — Mobile CSS: Leakage and Dead Code127-128-**42 occurrences** of `peach-mobile` in `popup.css`. All major layout overrides are properly scoped to `body.peach-mobile`.129-130-**Potential leakage (not `body.peach-mobile`-scoped):**131-- `.peach-mobile-root` (line 12) — applied to `<html>`, harmless; only set on native platform132-- `.peach-mobile-row` (line 7093) — standalone utility class; would render in extension if applied in non-mobile context, but only used in mobile-specific components133-- `.peach-mobile-status` (line 7100) — same; standalone class134-- `.peach-onboarding-mobile-card-buttons` (line 6529) — standalone; only used in onboarding flow gated behind native platform checks in JS135-136-None produce broken extension behavior because the classes are only applied from JS when `Capacitor.isNativePlatform()` is true.137-138-**Panel/swipe navigation remnants:** NONE. No `.peach-panel`, `.peach-panels`, `300vw`, `VaultPanels`, or `activePanel` anywhere. The old 3-panel swipe layout has been completely removed.139-140-**Bottom sheet CSS:** Does NOT exist. Line 7284 has a comment about "long-press context menu + swipe-copy flash" — unrelated. No `.bottom-sheet`, `.peach-sheet`, drag, or swipe CSS for sheets. **The bottom sheet must be built from scratch.**141-142-**Entry row gesture CSS:** `.peach-entry-row` is standard list styling. The `transform: translateX/Y` values in the file are all fade-in/slide-in animations, not gesture responses.143-144----145-146-## Section 4 — App.tsx: Mobile Wiring147-148-**`body.peach-mobile` on mount:** YES — lines 378–380, guarded by `Capacitor.isNativePlatform()`, applied in `onMount`. Both `document.body` and `document.documentElement` are classed correctly.149-150-**Autolock implementation:** The `appStateChange` listener (line 418) does NOT implement autolock. It only handles: (a) checking for a pending autofill save when the app foregrounds, and (b) re-checking autofill request extras. The `autoLockMinutes` signal (line 150) is read from settings (line 898) and passed as a prop to the Lock component (line 1502). There is no `backgroundedAt` timestamp and no setTimeout-based lock timer in `App.tsx`. Autolock either lives inside the Lock component (which receives the prop) or is not implemented.151-152-**Vault list on mobile:** `VaultView` at line 1481 handles the main entry view. A separate `view() === "list"` path at line 1480 renders a standalone list. No bottom sheet. On single-pane mobile, the vault opens directly to the entry view (no visible list). Two-pane layout (tablet) shows list + detail side by side via CSS overrides on `body.peach-mobile.layout-medium/layout-expanded`.153-154----155-156-## Section 5 — Biometrics: Current Wiring157-158-**JS module (`src/shared/biometric.ts`):**159-- `biometricUnlock()` → `Biometric.authenticate()` → returns `result.verified`160-- `biometricAvailable()` → `Biometric.isAvailable()`161-162-**Kotlin side:** Correct. `authenticate()` shows a strong biometric prompt, resolves `{ verified: true }` on success.163-164-**Lock.tsx flow:**165-1. `BiometricUnlockButton` calls `handleBiometricUnlock(props)` on click166-2. `handleBiometricUnlock` calls `biometricUnlock()` — gets `true` on success167-3. On success, calls `props.onUnlock()` — which is App.tsx's `handleUnlock`168-169-**The gap:** `props.onUnlock` is the *post*-unlock callback — the same one called after a successful password `UNLOCK` message. Biometric success calls this directly, bypassing the UNLOCK message entirely. **The vault is never decrypted.** No master password is retrieved; no derived key is stored or loaded from a biometric-protected keystore. Biometric auth returns `verified: true` and the app proceeds as if unlocked, but `vault.isUnlocked()` in the background is still false.170-171-**Result:** Biometric button appears, prompt shows, authentication succeeds, but the vault remains encrypted. The app's subsequent behavior (trying to read entries) will fail silently or show an empty vault.172-173-**Fix required:** Store the master password (or a vault-derived key) in an AndroidKeystore-backed `EncryptedSharedPreferences` entry, keyed to biometric auth. On successful `authenticate()`, retrieve that secret and send a real `UNLOCK` message with it.174-175----176-177-## Section 6 — Autofill: Current State178-179-**`PeachAutofillService.kt` (118 lines):**180-181-*When locked:* Builds a single auth dataset — "Unlock Peach to autofill" — with a `PendingIntent` that launches `MainActivity`. The `autofill_domain`, `autofill_package`, and field IDs are passed as extras.182-183-*When unlocked:* Calls `VaultBridge.findEntries(domain ?? packageName)`. Returns up to 3 match datasets with title + username in the presentation view. Password is filled but not shown in presentation.184-185-*`onSaveRequest`:* Stub — calls `callback.onSuccess(null)` immediately. Save is handled via `VaultBridge.offerSave()` called from the autofill service's save path, which writes to SharedPreferences. App.tsx reads this on foreground via `checkPendingSave()`.186-187-**Inline suggestions (IME bar):** NOT implemented. Zero references to `InlineSuggestionsRequest`, `InlinePresentationSpec`, or `InlineSuggestion` anywhere in Kotlin.188-189-**"Create a new login" dataset:** NOT present. Only existing-match datasets are returned.190-191-**TOTP in autofill:** NOT handled. No OTP field detection.192-193-**`InMemoryVault` population:** Correctly wired. On vault open, `publishInMemoryVault(loadedEntries)` is called (App.tsx line 940) before `openVault()` completes. On lock, `clearInMemoryVault()` is called (lines 1101, 1608). `VaultBridge.isUnlocked()` checks `InMemoryVault.isAvailable()` — so the autofill service correctly knows vault state.194-195-**Multiple datasets:** YES, up to 3 per fill request.196-197----198-199-## Section 7 — Extension Health Check200-201-| Target | Result |202-|---|---|203-| Tests | **311 passed**, 31 files, 0 failures |204-| TypeScript (`--noEmit`) | **Clean** — 0 errors |205-| `build:chrome` | **Success** (chunk size warning only, not an error) |206-| `build:firefox` | **Success** (same warning) |207-| `build:mobile` | **Success** (same warning) |208-| Android `assembleDebug` | **BUILD SUCCESSFUL** in 4s, 34 tasks executed |209-210-The chunk size warning (`>500 kB after minification`) is consistent across all three web builds and is a bundle optimization note, not a correctness issue.211-212----213-214-## Section 8 — Dead Code and Imports215-216-**Files importing `sync.ts` (native client):**217-- `src/background/sync-lifecycle.ts` — the only live importer (`import * as sync from "./sync"`)218-- `src/background/sync.test.ts` — test file mocking `connectNative`219-220-**Native host references in TypeScript:**221-- `connectNative` — `sync.ts` lines 269, 273; test mock in `sync.test.ts`222-- `peach-sync-host` — `sync.ts` header comment, `capacitor-sync-transport.ts` (protocol reference), `noise-js.ts` (auth proof reference), `native-sync-manifest-script.test.ts`223-- `mDNS`/`mdns` — `sync-lifecycle.ts` (lines 4, 147, 361), `sync-bridge.ts` (comment)224-- `NsdManager` — `SyncPlugin.kt` (actively used, not dead)225-226-**Kotlin dead imports:** None — `NsdManager` and `WifiManager` in `SyncPlugin.kt` are in active use.227-228-**`SYN-001`:** Referenced only in `sync.ts` line 6 (the original gate comment). Not used elsewhere.229-230-**Safe to delete:**231-232-| File/item | Reason |233-|---|---|234-| `native/peach-sync-host/` | Entire Rust binary; replaced by WebSocket path |235-| `src/background/sync.ts` | Native host client; nothing reusable |236-| `src/background/sync-lifecycle.ts` | Tightly coupled to sync.ts; needs replacement |237-| `scripts/generate-native-sync-manifest.mjs` | Native host installer |238-| `scripts/install-native-sync-host.mjs` | Native host installer |239-| `nativeMessaging` in both manifests | Premature; no installer in place |240-241-**References to clean up after deletions:**242-- `messages.ts` line 1227 — `connectNative` comment reference243-- `sync-bridge.ts` — `NsdManager` comment reference (the Kotlin plugin it describes stays)244-- `noise-js.ts` — `native/peach-sync-host/src/auth.rs` comment reference (keep the code, update the comment)245-246----247-248-## Section 9 — Master Spec vs. What Exists249-250-### Vault List Bottom Sheet251-- No bottom sheet CSS exists — not even a skeleton252-- No bottom sheet JS component exists253-- Mobile vault uses extension-style layout with `body.peach-mobile` overrides254-- Single-pane mobile goes straight to the entry view; no swipeable list255-- **Must be built entirely from scratch:** component, CSS, gesture handling, open/close animation256-257-### Biometrics258-- `authenticate()` completes successfully — Kotlin is correct259-- After authentication: calls the post-unlock callback without sending `UNLOCK` → vault not decrypted260-- No derived key stored/retrieved; no biometric-protected keystore entry261-- **Broken:** biometric succeeds but vault decryption never happens. Fix requires storing the master password (or a vault key) in an AndroidKeystore-backed secret, retrievable only after biometric auth, and using that key to perform the actual UNLOCK.262-263-### Autofill Inline (IME bar)264-- `InlineSuggestionsRequest` — NOT present in any Kotlin file265-- Multiple datasets: YES (up to 3 existing matches)266-- "Create new login" dataset: NO267-- TOTP: NOT handled268-269-### Sync — Delete / Keep / Build270-271-| Category | Items |272-|---|---|273-| **Delete** | `native/peach-sync-host/`, `sync.ts`, `sync-lifecycle.ts`, installer scripts, `nativeMessaging` from manifests |274-| **Keep** | `noise-js.ts`, `sync-bridge.ts`, `capacitor-sync-transport.ts`, `android-sync-identity.ts`, `sync-coordinator.ts`, `SyncPairing.tsx`, all Kotlin sync plugins |275-| **Build from scratch** | Mayan code scanning UI, `SCAN_PAIRING_QR` message handler, subnet scan for peer discovery (no JS or Kotlin implementation exists), new desktop sync-lifecycle (decoupled from native host) |

view file at the parent

manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22   "manifest_version": 3,33   "name": "Peach",4-  "version": "2.30.0",4+  "version": "2.30.1",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.30.0",4+  "version": "2.30.1",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.30.0",3+  "version": "2.30.1",44   "description": "Zero-knowledge password manager",55   "type": "module",66   "scripts": {

view file

syn001.md+0 −53
@@ -1,53 +0,0 @@1-# SYN-0012-3-## Summary4-5-SYN-001 is the Peach Sync blocker that kept desktop native sync dormant.6-7-The original bug was that the Rust native host used an all-zero Noise NK static8-private key on the responder path. That made every install present the same9-responder identity, which defeats the peer-authentication property sync relies10-on.11-12-That original all-zero-key bug has been fixed in the SYN-001 sprint. The host13-now has a persisted per-install X25519 identity, advertises only the public key,14-uses the real identity in responder and initiator Noise paths, and has Rust tests15-for real-key handshakes and wrong-public-key rejection.16-17-## Completed So Far18-19-- Phase 0 baseline captured before implementation: version `2.13.2`, Koh head20-  `636a946976ec`, and initial `cargo check` failure summary.21-- Compile-only native-host baseline saved as `4b36429b213d`22-  (`chore: Prepare native host compile baseline (v2.13.3)`).23-- First sync security behavior change saved separately as `b49712298cc7`24-  (`fix: Replace sync host static Noise identity (v2.13.4)`).25-- Trusted-device store, identity rotation, and authentication proof layer added.26-- Browser native-messaging framing added to the Rust host.27-- Dormant TypeScript client aligned with the Rust host protocol.28-- mDNS discovery events now flow back through the native-messaging output loop.29-30-## Current Safety Gate31-32-The risky path is still not shipped:33-34-- neither browser manifest includes `nativeMessaging`;35-- no live background handler imports or calls `connectSyncHost()`;36-- `src/background/sync.ts` is gated and documents that sync remains disabled37-  until the audit/gate checklist passes;38-- Android sync remains deferred to the later WebSocket/Capacitor phase.39-40-## Remaining Before Re-Enabling41-42-1. Finish the native-host audit checklist: private-key handling/logging,43-   identity file permissions, mDNS TXT contents, random-port behavior,44-   trusted-device lifecycle, pairing flow, error surfaces, and rate limits.45-2. Build the real sync payload flow: length-prefixed encrypted JSON messages,46-   vault manifest/diff exchange, merge rules, locked-vault cancellation, and47-   Pro/background gates.48-3. Add native-host installer manifest generation and only then add49-   `nativeMessaging` to the browser manifests.50-4. Add manual E2E coverage with two desktop installs before marking SYN-00151-   resolved.52-53-Until those are complete, sync should remain gated and dormant.

view file at the parent