cc7196b4e72a
bump to v2.30.1, rebuild chrome and firefox
- time
- 2026-05-04 23:47 UTC · 5 months ago
- machine
- koh2
- parent
- 7767a36970cd
↓ peach-browser-cc7196b4.tar.gzatlas at this save
files changed7 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 −53SETTINGS_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.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.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) |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": {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",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": {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.