78a43519bc73

delete unreferenced modules, exports, and the dead relay dial path

time
2026-08-29 05:05 UTC · 1 month ago
machine
koh2
agent
Hy4-preview
session
repo cleanup: mobile removal
parent
8de15d6aae62
21 files manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/background/locked-sync-spool.ts+0 −4src/background/passkey.ts+0 −6src/background/recovery-pending.ts+0 −6src/content/_setup.ts+0 −47src/popup/App.tsx+0 −16src/popup/codex/aztec.ts+0 −70src/popup/components/LockRuneCanvas.tsx+0 −47src/popup/components/NoteRunes.tsx+0 −159src/popup/components/settings/StackPill.tsx+0 −20src/popup/components/settings/StackToggle.tsx+0 −21src/popup/pairing-qr-scanner.ts+0 −21src/popup/views/vault-search.ts+0 −6src/shared/debug.ts+0 −8src/shared/pairing-dial.ts+6 −217src/shared/recovery-word-list.ts+0 −3src/shared/sync-types.ts+0 −12src/shared/types.ts+0 −40src/shared/vault-adopt.ts+0 −7
manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22   "manifest_version": 3,33   "name": "Peach Password Manager",4-  "version": "4.10.29",4+  "version": "4.10.30",55   "description": "Zero-knowledge password manager",66   "browser_specific_settings": {77     "gecko": {

view file

manifest.json+1 −1
@@ -2,7 +2,7 @@22   "manifest_version": 3,33   "minimum_chrome_version": "116",44   "name": "Peach Password Manager",5-  "version": "4.10.29",5+  "version": "4.10.30",66   "description": "Zero-knowledge password manager",77   "action": {88     "default_popup": "src/popup/index.html",

view file

package.json+1 −1
@@ -1,6 +1,6 @@11 {22   "name": "peach",3-  "version": "4.10.29",3+  "version": "4.10.30",44   "description": "Zero-knowledge password manager",55   "license": "GPL-3.0-only",66   "type": "module",

view file

src/background/locked-sync-spool.ts+0 −4
@@ -42,10 +42,6 @@4242   });4343 }4444 45-export async function clearLockedSyncSpool(): Promise<void> {46-  await mutateSpool(async () => chrome.storage.local.remove(STORAGE_KEY));47-}48-4945 export async function getLockedSyncIdentity(): Promise<SyncPeerIdentity | null> {5046   const spool = await loadSpool();5147   if (!spool) return null;

view file

src/background/passkey.ts+0 −6
@@ -952,12 +952,6 @@952952   return { ok: true };953953 }954954 955-export async function cancelFirefoxPasskeyCreate(): Promise<void> {956-  if (chrome.storage.session) {957-    await chrome.storage.session.remove(FIREFOX_PENDING_KEY);958-  }959-}960-961955 function declineKey(domain: string): string {962956   return `${FIREFOX_DECLINE_PREFIX}${domain}`;963957 }

view file

src/background/recovery-pending.ts+0 −6
@@ -25,9 +25,3 @@2525     await chrome.storage.local?.set({ [KEY]: "1" });2626   } catch { /* best-effort */ }2727 }28-29-export async function clearRecoveryConfirmPending(): Promise<void> {30-  try {31-    await chrome.storage.local?.remove(KEY);32-  } catch { /* best-effort */ }33-}

view file

src/content/_setup.ts+0 −47
@@ -50,16 +50,6 @@5050 }5151 5252 /** Deliver a message to every registered onMessage listener (e.g. VAULT_UNLOCKED). */53-export function dispatchRuntimeMessage(54-  msg: any,55-  sender: any = { id: "peach-test-extension-id" }56-): Array<unknown> {57-  const responses: unknown[] = [];58-  for (const listener of messageListeners) {59-    responses.push(listener(msg, sender, (r?: any) => responses.push(r)));60-  }61-  return responses;62-}6353 6454 const sendMessage = vi.fn((msg: any, maybeCb?: (resp: any) => void) => {6555   sentMessages.push(msg);@@ -231,42 +221,5 @@231221 }232222 233223 /** Signup form: username + new-password + confirm. */234-export function createSignupForm(): {235-  form: HTMLFormElement;236-  username: HTMLInputElement;237-  password: HTMLInputElement;238-  confirm: HTMLInputElement;239-} {240-  const form = document.createElement("form");241-  document.body.appendChild(form);242-  const username = makeInput({ type: "text", name: "new-username" }, form);243-  const password = makeInput(244-    { type: "password", name: "new-password", autocomplete: "new-password" },245-    form246-  );247-  const confirm = makeInput(248-    { type: "password", name: "confirm-password", autocomplete: "new-password" },249-    form250-  );251-  const button = document.createElement("button");252-  button.type = "submit";253-  button.textContent = "Create account";254-  form.appendChild(button);255-  return { form, username, password, confirm };256-}257224 258225 /** Split-OTP form: `count` adjacent single-char numeric inputs. */259-export function createSplitOtpForm(count = 6): {260-  form: HTMLFormElement;261-  boxes: HTMLInputElement[];262-} {263-  const form = document.createElement("form");264-  document.body.appendChild(form);265-  const boxes: HTMLInputElement[] = [];266-  for (let i = 0; i < count; i++) {267-    boxes.push(268-      makeInput({ type: "text", maxlength: "1", inputmode: "numeric" }, form)269-    );270-  }271-  return { form, boxes };272-}

view file

src/popup/App.tsx+0 −16
@@ -78,21 +78,8 @@7878 const INITIAL_SYNC_POLL_MS = 500;7979 const INITIAL_SYNC_SLOW_MS = 15_000;8080 81-const canarySuppressMap = new Map<string, boolean>();8281 let shellRingAnimation: Animation | null = null;8382 84-export function isCanarySuppressed(entryId: string): boolean {85-  return canarySuppressMap.get(entryId) ?? false;86-}87-88-export function setCanarySuppressed(entryId: string, suppressed: boolean): void {89-  canarySuppressMap.set(entryId, suppressed);90-}91-92-export function clearCanarySuppressions(): void {93-  canarySuppressMap.clear();94-}95-9683 export function triggerRing() {9784   const shell = document.getElementById("hd-shell");9885   if (!shell) return;@@ -125,7 +112,6 @@125112   animation.oncancel = clearGlow;126113 }127114 128-129115 export function triggerPipPulse() {130116   const pip = document.querySelector(".peach-status-pip, .pip");131117   if (pip) {@@ -1142,7 +1128,6 @@11421128   }11431129 11441130   function handleLock() {1145-    clearCanarySuppressions();11461131     setEntries([]);11471132     setSelectedEntry(null);11481133     setEditEntry(undefined);@@ -1766,7 +1751,6 @@17661751                     setView("export");17671752                   }}17681753                   onDestroyed={async () => {1769-                    clearCanarySuppressions();17701754                     // Fresh onboarding start after a destroy — don't let the17711755                     // resume markers land the user mid-carousel.17721756                     setOnboardingCardAtExit(0);

view file

src/popup/codex/aztec.ts+0 −70
@@ -25,11 +25,7 @@2525   AztecCodeReader,2626   AztecEncoder,2727   BinaryBitmap,28-  BarcodeFormat,29-  DecodeHintType,3028   GlobalHistogramBinarizer,31-  HybridBinarizer,32-  MultiFormatReader,3329   RGBLuminanceSource,3430 } from "@zxing/library";3531 import type { BitMatrix } from "@zxing/library";@@ -179,72 +175,6 @@179175   const width = view.getUint32(16, false);180176   const height = view.getUint32(20, false);181177   return { width, height };182-}183-184-// ── Public: Aztec PNG generation ──────────────────────────────────────────────185-186-/**187- * Render an Aztec barcode and return its PNG bytes + dimensions.188- * Browser: ZXing BitMatrix → OffscreenCanvas → PNG.189- * Node/test: bwip-js/node → Buffer → PNG.190- */191-export async function generateAztecPng(payload: string): Promise<AztecImage> {192-  if (typeof OffscreenCanvas !== "undefined") {193-    return matrixToPngBrowser(buildAztecMatrix(payload));194-  }195-  return matrixToPngNode(payload);196-}197-198-// ── Aztec PNG decoding (canvas-based, kept for external callers) ──────────────199-200-/**201- * Decode an Aztec PNG via @zxing. Returns the decoded UTF-8 text, or `null`202- * if decoding fails for any reason (no DOM, malformed PNG, no match found).203- * Never throws.204- *205- * Note: `generateVerifiedAztec` no longer calls this — it uses the in-memory206- * `verifyMatrixRoundTrip` path instead. This export is kept for tooling that207- * needs to decode an arbitrary Aztec PNG image.208- */209-export async function decodeAztecFromPng(pngBytes: Uint8Array): Promise<string | null> {210-  if (typeof OffscreenCanvas === "undefined" || typeof createImageBitmap === "undefined") {211-    return null;212-  }213-214-  try {215-    const owned = new Uint8Array(pngBytes.byteLength);216-    owned.set(pngBytes);217-    const blob = new Blob([owned], { type: "image/png" });218-    const bitmap = await createImageBitmap(blob);219-    const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);220-    const ctx = canvas.getContext("2d");221-    if (!ctx) return null;222-    ctx.fillStyle = "#ffffff";223-    ctx.fillRect(0, 0, bitmap.width, bitmap.height);224-    ctx.drawImage(bitmap, 0, 0);225-    const imageData = ctx.getImageData(0, 0, bitmap.width, bitmap.height);226-227-    const argb = new Int32Array(bitmap.width * bitmap.height);228-    const raw = imageData.data;229-    for (let i = 0, j = 0; i < raw.length; i += 4, j++) {230-      argb[j] = (0xff << 24) | (raw[i]! << 16) | (raw[i + 1]! << 8) | raw[i + 2]!;231-    }232-233-    const source = new RGBLuminanceSource(argb, bitmap.width, bitmap.height);234-    const binarizer = new HybridBinarizer(source);235-    const bitmapBinary = new BinaryBitmap(binarizer);236-237-    const reader = new MultiFormatReader();238-    const hints = new Map();239-    hints.set(DecodeHintType.POSSIBLE_FORMATS, [BarcodeFormat.AZTEC]);240-    hints.set(DecodeHintType.TRY_HARDER, true);241-    reader.setHints(hints);242-243-    const result = reader.decode(bitmapBinary);244-    return result.getText();245-  } catch {246-    return null;247-  }248178 }249179 250180 // ── Verified Aztec generation ─────────────────────────────────────────────────

view file

src/popup/components/LockRuneCanvas.tsx+0 −47
@@ -1,47 +0,0 @@1-import { Index, onCleanup, onMount } from "solid-js";2-import { createSignal } from "solid-js";3-4-const RUNE_COUNT = 9;5-const RUNES = Array.from(6-  { length: RUNE_COUNT },7-  () => String.fromCodePoint(0xe000 + Math.floor(Math.random() * 256)),8-);9-10-export function LockRuneCanvas() {11-  const [glowIdx, setGlowIdx] = createSignal(-1);12-13-  onMount(() => {14-    let timer: ReturnType<typeof setTimeout>;15-16-    function scheduleNext() {17-      timer = setTimeout(18-        () => {19-          setGlowIdx(Math.floor(Math.random() * RUNE_COUNT));20-          timer = setTimeout(21-            () => {22-              setGlowIdx(-1);23-              scheduleNext();24-            },25-            2000 + Math.random() * 1500,26-          );27-        },28-        1500 + Math.random() * 3000,29-      );30-    }31-32-    scheduleNext();33-    onCleanup(() => clearTimeout(timer));34-  });35-36-  return (37-    <div class="peach-lock-rune-string" aria-hidden="true">38-      <Index each={RUNES}>39-        {(char, idx) => (40-          <span classList={{ "is-glowing": glowIdx() === idx }}>41-            {char()}42-          </span>43-        )}44-      </Index>45-    </div>46-  );47-}

view file at the parent

src/popup/components/NoteRunes.tsx+0 −159
@@ -1,159 +0,0 @@1-import { onCleanup, onMount } from "solid-js";2-3-interface NoteRunesProps {4-  mode: "toc" | "editor";5-}6-7-const FONT_SIZE = 13;8-const RUNE_LEN = 120;9-10-function buildRuneString(): string {11-  const out: string[] = [];12-  for (let i = 0; i < RUNE_LEN; i++) {13-    out.push(String.fromCodePoint(0xe000 + ((i * 41 + 7) & 0xff)));14-  }15-  return out.join("");16-}17-18-const RUNES = buildRuneString();19-20-interface RuneState {21-  x: number;22-  y: number;23-  char: string;24-  vx: number;25-  vy: number;26-  opacity: number;27-  dwellMs: number;28-  elapsed: number;29-  fading: boolean;30-}31-32-function makeRune(w: number, h: number, mode: "toc" | "editor"): RuneState {33-  // Edge-biased placement: runes stay in margins34-  const MARGIN = 40;35-  const side = Math.random() < 0.5 ? "left" : "right";36-  const x = side === "left"37-    ? Math.random() * MARGIN38-    : w - MARGIN + Math.random() * MARGIN;39-  const y = Math.random() * h;40-41-  const [minOp, maxOp] = mode === "toc" ? [0.03, 0.06] : [0.02, 0.04];42-  const [minV, maxV] = mode === "toc" ? [0.04, 0.08] : [0.02, 0.05];43-  const [minD, maxD] = mode === "toc" ? [20000, 40000] : [30000, 60000];44-45-  const speed = minV + Math.random() * (maxV - minV);46-  const angle = Math.random() * Math.PI * 2;47-  const charIdx = Math.floor(Math.random() * RUNES.length);48-49-  return {50-    x,51-    y,52-    char: RUNES[charIdx]!,53-    vx: Math.cos(angle) * speed,54-    vy: Math.sin(angle) * speed,55-    opacity: minOp + Math.random() * (maxOp - minOp),56-    dwellMs: minD + Math.random() * (maxD - minD),57-    elapsed: 0,58-    fading: false,59-  };60-}61-62-export function NoteRunes(props: NoteRunesProps) {63-  let canvas!: HTMLCanvasElement;64-  let raf = 0;65-  let running = false;66-67-  onMount(() => {68-    const ctx = canvas.getContext("2d");69-    if (!ctx) return;70-71-    const W = canvas.width;72-    const H = canvas.height;73-    const count = props.mode === "toc" ? 3 : 2;74-    const runes: RuneState[] = Array.from({ length: count }, () => makeRune(W, H, props.mode));75-76-    let last = performance.now();77-    const FADE_MS = 600;78-79-    const frame = (now: number) => {80-      const dt = Math.min(now - last, 64);81-      last = now;82-83-      ctx.clearRect(0, 0, W, H);84-      ctx.font = `${FONT_SIZE}px PeachScript, monospace`;85-      ctx.textBaseline = "alphabetic";86-87-      for (let i = 0; i < runes.length; i++) {88-        const r = runes[i]!;89-        r.elapsed += dt;90-91-        // Move slowly92-        r.x += r.vx * dt;93-        r.y += r.vy * dt;94-95-        // Clamp to canvas (bounce off edges)96-        if (r.x < 0 || r.x > W) r.vx = -r.vx;97-        if (r.y < 0 || r.y > H) r.vy = -r.vy;98-99-        let alpha = r.opacity;100-        const remaining = r.dwellMs - r.elapsed;101-102-        // Fade in over first FADE_MS103-        if (r.elapsed < FADE_MS) {104-          alpha *= r.elapsed / FADE_MS;105-        }106-        // Fade out over last FADE_MS107-        if (remaining < FADE_MS && remaining > 0) {108-          alpha *= remaining / FADE_MS;109-        }110-111-        // Replace rune when dwell completes112-        if (r.elapsed >= r.dwellMs) {113-          runes[i] = makeRune(W, H, props.mode);114-          continue;115-        }116-117-        ctx.fillStyle = `rgba(245, 240, 234, ${alpha})`;118-        ctx.fillText(r.char, r.x, r.y);119-      }120-121-      raf = requestAnimationFrame(frame);122-    };123-124-    const start = () => {125-      if (running) return;126-      running = true;127-      last = performance.now();128-      raf = requestAnimationFrame(frame);129-    };130-131-    const stop = () => {132-      if (!running) return;133-      running = false;134-      cancelAnimationFrame(raf);135-    };136-137-    const onVisibility = () => {138-      if (document.hidden) stop(); else start();139-    };140-    document.addEventListener("visibilitychange", onVisibility);141-142-    document.fonts.load(`${FONT_SIZE}px PeachScript`).finally(start);143-144-    onCleanup(() => {145-      stop();146-      document.removeEventListener("visibilitychange", onVisibility);147-    });148-  });149-150-  return (151-    <canvas152-      ref={canvas}153-      width={400}154-      height={700}155-      class="peach-note-rune-canvas"156-      aria-hidden="true"157-    />158-  );159-}

view file at the parent

src/popup/components/settings/StackPill.tsx+0 −20
@@ -1,20 +0,0 @@1-import type { JSX } from "solid-js";2-3-interface StackPillProps {4-  accent?: boolean;5-  class?: string;6-  children: JSX.Element;7-  onClick?: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent>;8-}9-10-export function StackPill(props: StackPillProps) {11-  return (12-    <button13-      class={`stk-pill ${props.accent ? "is-accent" : ""} ${props.class ?? ""}`}14-      onClick={props.onClick}15-      type="button"16-    >17-      {props.children}18-    </button>19-  );20-}

view file at the parent

src/popup/components/settings/StackToggle.tsx+0 −21
@@ -1,21 +0,0 @@1-interface StackToggleProps {2-  on: boolean;3-  disabled?: boolean;4-  label: string;5-  onToggle: () => void;6-}7-8-export function StackToggle(props: StackToggleProps) {9-  return (10-    <button11-      class={`peach-toggle ${props.on ? "is-on" : ""}`}12-      onClick={props.onToggle}13-      aria-label={props.label}14-      aria-pressed={props.on}15-      disabled={props.disabled}16-      type="button"17-    >18-      <span class="peach-toggle-knob" />19-    </button>20-  );21-}

view file at the parent

src/popup/pairing-qr-scanner.ts+0 −21
@@ -1,21 +0,0 @@1-import { BrowserQRCodeReader, NotFoundException } from "@zxing/library";2-3-export interface PairingQrScannerHandle {4-  stop(): void;5-}6-7-export async function startPairingQrScanner(8-  video: HTMLVideoElement,9-  onScan: (value: string) => void,10-  onError: (error: Error) => void,11-): Promise<PairingQrScannerHandle> {12-  const reader = new BrowserQRCodeReader();13-  await reader.decodeFromVideoDevice(null, video, (result, error) => {14-    if (result) {15-      onScan(result.getText());16-      return;17-    }18-    if (error && !(error instanceof NotFoundException)) onError(error);19-  });20-  return { stop: () => reader.reset() };21-}

view file at the parent

src/popup/views/vault-search.ts+0 −6
@@ -11,12 +11,6 @@1111   { key: "recent", label: "recent" },1212 ];1313 14-export const VAULT_SEARCH_SCOPE_OPTIONS: { key: SearchScope; label: string }[] = [15-  { key: "both", label: "both" },16-  { key: "emails", label: "emails" },17-  { key: "usernames", label: "usernames" },18-];19-2014 function fuzzyMatch(query: string, target: string): boolean {2115   const q = query.toLowerCase();2216   const t = target.toLowerCase();

view file

src/shared/debug.ts+0 −8
@@ -1,8 +0,0 @@1-// Compile-time debug flag. Vite's `define` folds __PEACH_DEBUG__ to false in2-// production builds, so the debug() export becomes a no-op and is tree-shaken3-// away. Existing console.* calls will be migrated to this helper over time.4-declare const __PEACH_DEBUG__: boolean;5-6-export const debug = __PEACH_DEBUG__7-  ? (...args: unknown[]) => console.info("[Peach]", ...args)8-  : () => {};

view file at the parent

src/shared/pairing-dial.ts+6 −217
@@ -1,47 +1,12 @@1-// The dialling half of code pairing — shared by device pairing and vault2-// adoption.1+// The WebSocket seam every relay-facing Peach flow shares.32 //4-// Both flows are identical up to the point where they diverge: decode a5-// pairing code, dial the address inside it, present the token in a plaintext6-// hello, and run Noise NK as the INITIATOR against the ephemeral key the7-// listener returns. What they then say over the encrypted channel differs8-// (PAIR_IDENTITY vs ADOPT_REQUEST); how they get there does not.9-//10-// The browser is always the dialler. It cannot listen, so it is always the NK11-// initiator and the listener's `ephemeralPublicKeyHex` is the responder static.12-//13-// Nothing here touches chrome, the vault, or a real WebSocket — the socket is14-// injected — because vault adoption runs before a vault exists at all.3+// Production wires the real `WebSocket`; tests inject a fake. One binary4+// message per frame. Nothing here touches chrome or the vault, because vault5+// adoption runs before a vault exists at all.156 16-import {17-  noiseNKInitiator,18-  performInitiatorAuth,19-  type NoiseNKSession,20-} from "./noise-js";21-import { derivePairingSAS, type PairingTransport } from "./pairing-handshake";22-import {23-  pairingInvitationUrl,24-  parsePairingInvitation,25-} from "./pairing-invitation";26-import { bytesToHex, deriveRendezvousSecret } from "./rendezvous";27-28-/** Default pairing port used by native listeners. */297 /**30- * The port Android's reconnect listener used to accept on. Retained only31- * because paired records may still carry a `wsPort`; nothing dials it. LAN32- * transport is retired — see docs/sync-lan-retirement.md.8+ * Minimal binary WebSocket seam. One binary message per frame.339  */34-export const LEGACY_LAN_PORT = 49142;35-36-export type PairingDialError =37-  | "invalid_code" // socket closed before PEACH_HELLO_ACK (wrong/expired)38-  | "no_host_found" // nothing answered at the endpoint39-  | "handshake_failed"; // Noise NK failed or the ACK was malformed40-41-/**42- * Minimal binary WebSocket seam. Production wires the real `WebSocket`; tests43- * inject a fake. One binary message per frame.44- */4510 export interface PairingSocket {4611   send(bytes: Uint8Array): void;4712   /** Resolves with the next inbound binary frame, or rejects on close. */@@ -51,169 +16,8 @@5116   closedBeforeFrame(): boolean;5217 }5318 54-export interface PairingDialDeps {55-  openSocket(ip: string, port: number): Promise<PairingSocket>;56-}57-58-export interface PairingDialOptions {59-  flow?: "adoption";60-}61-62-export interface RelayPairingDialDeps {63-  openSocket(url: string): Promise<PairingSocket>;64-}65-66-export interface DialledPairingChannel {67-  /** Encrypted channel — everything written to it is Noise-protected. */68-  channel: PairingTransport;69-  /** The 8-digit code the user must confirm matches the listener's screen. */70-  sas: string;71-  /** The address actually connected to, for the reconnect cache. */72-  host: string;73-  /** Present only for relay-first pairing/adoption. */74-  relayUrl?: string;75-  /** Pair-only reconnect capability derived independently from the SAS. */76-  rendezvousSecretHex: string;77-  /** Static key authenticated by Noise NK. PAIR_IDENTITY must repeat this key. */78-  remoteStaticPublicKey: Uint8Array;79-  /**80-   * Prove possession of the initiator key before any identity or consent frame.81-   * Device pairing must call this exactly once; adoption cannot use it until it82-   * has a long-term key and remains a separately gated protocol.83-   */84-  authenticateInitiator(85-    privateKey: Uint8Array,86-    publicKey: Uint8Array,87-  ): Promise<void>;88-  socket: PairingSocket;89-}90-91-export type DialResult =92-  | { ok: true; dialled: DialledPairingChannel }93-  | { ok: false; error: PairingDialError };94-95-export async function dialRelayPairingChannel(96-  encodedInvitation: string,97-  deps: RelayPairingDialDeps,98-): Promise<DialResult> {99-  const invitation = parsePairingInvitation(encodedInvitation);100-  if (!invitation) return { ok: false, error: "invalid_code" };101-102-  let sock: PairingSocket;103-  try {104-    sock = await deps.openSocket(pairingInvitationUrl(invitation));105-  } catch {106-    return { ok: false, error: "no_host_found" };107-  }108-  const result = await finishPairingHandshake(109-    sock,110-    invitation.token,111-    new URL(invitation.relayUrl).hostname,112-  );113-  if (result.ok) result.dialled.relayUrl = invitation.relayUrl;114-  return result;115-}116-117-async function finishPairingHandshake(118-  sock: PairingSocket,119-  token: string,120-  host: string,121-  flow?: "adoption",122-): Promise<DialResult> {123-  let noise: NoiseNKSession | undefined;124-  try {125-    sock.send(encodeJson({126-      type: "PEACH_HELLO",127-      token,128-      ...(flow ? { flow } : {}),129-    }));130-131-    // A wrong or expired code gets NO error frame — the listener silently132-    // closes and rotates. That silence IS the wrong-code signal.133-    let ackBytes: Uint8Array;134-    try {135-      ackBytes = await sock.receive();136-    } catch {137-      sock.close();138-      return { ok: false, error: "invalid_code" };139-    }140-    if (sock.closedBeforeFrame() || ackBytes.length === 0) {141-      sock.close();142-      return { ok: false, error: "invalid_code" };143-    }144-145-    let ephemeralPublicKeyHex: string;146-    try {147-      const ack = decodeJson<{ type?: string; ephemeralPublicKeyHex?: string }>(ackBytes);148-      if (ack.type !== "PEACH_HELLO_ACK" || typeof ack.ephemeralPublicKeyHex !== "string") {149-        // A non-ACK frame on a matching code should not happen; treat a missing150-        // ACK exactly like a wrong code.151-        sock.close();152-        return { ok: false, error: "invalid_code" };153-      }154-      ephemeralPublicKeyHex = ack.ephemeralPublicKeyHex;155-    } catch {156-      sock.close();157-      return { ok: false, error: "invalid_code" };158-    }159-160-    const remoteStaticPublicKey = hexToBytes32(ephemeralPublicKeyHex);161-    const session = await noiseNKInitiator(162-      remoteStaticPublicKey,163-      async (msg) => sock.send(msg),164-      () => sock.receive(),165-    );166-    noise = session;167-168-    const channel: PairingTransport = {169-      async send(plaintext) { sock.send(session.encrypt(plaintext)); },170-      async receive() { return session.decrypt(await sock.receive()); },171-    };172-173-    return {174-      ok: true,175-      dialled: {176-        channel,177-        sas: derivePairingSAS(session.handshakeHash),178-        host,179-        rendezvousSecretHex: bytesToHex(deriveRendezvousSecret(session.handshakeHash)),180-        remoteStaticPublicKey,181-        authenticateInitiator: (privateKey, publicKey) =>182-          performInitiatorAuth(183-            session,184-            privateKey,185-            publicKey,186-            remoteStaticPublicKey,187-            channel.send,188-            channel.receive,189-          ),190-        socket: {191-          send: (bytes) => sock.send(bytes),192-          receive: () => sock.receive(),193-          close() {194-            session.close();195-            sock.close();196-          },197-          closedBeforeFrame: () => sock.closedBeforeFrame(),198-        },199-      },200-    };201-  } catch (err) {202-    // Coarse for the UI; log the real cause so a failed dial is diagnosable203-    // from the service-worker console.204-    if (typeof console !== "undefined") console.error("[pairing-dial] handshake failed:", err);205-    noise?.close();206-    sock.close();207-    return { ok: false, error: "handshake_failed" };208-  }209-}210-211-/** Relay-first pairing uses the same binary socket seam over `wss://`. */19+/** Relay pairing uses this binary socket seam over `wss://`. */21220 export function openRelayPairingSocket(url: string): Promise<PairingSocket> {213-  return openPairingSocketUrl(url);214-}215-216-function openPairingSocketUrl(url: string): Promise<PairingSocket> {21721   return new Promise((resolve, reject) => {21822     let ws: WebSocket;21923     try {@@ -266,19 +70,4 @@26670       closedBeforeFrame() { return sawCloseBeforeFrame; },26771     };26872   });269-}270-271-function encodeJson(obj: unknown): Uint8Array {272-  return new TextEncoder().encode(JSON.stringify(obj));273-}274-275-function decodeJson<T>(bytes: Uint8Array): T {276-  return JSON.parse(new TextDecoder().decode(bytes)) as T;277-}278-279-function hexToBytes32(hex: string): Uint8Array {280-  if (!/^[0-9a-f]{64}$/.test(hex)) throw new Error("expected 64-char lowercase hex");281-  const out = new Uint8Array(32);282-  for (let i = 0; i < 32; i++) out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);283-  return out;28473 }

view file

src/shared/recovery-word-list.ts+0 −3
@@ -271,6 +271,3 @@271271   "wrap", "wreck", "wrestle", "wrist", "write", "wrong", "yard", "year",272272   "yellow", "you", "young", "youth", "zebra", "zero", "zone", "zoo"273273 ];274-275-/** Total word count — must be exactly 2048 for 11-bit indices. */276-export const WORD_COUNT = WORD_LIST.length;\ No newline at end of file

view file

src/shared/sync-types.ts+0 −12
@@ -11,16 +11,6 @@1111 1212 export type DeviceType = "extension" | "android" | "ios";1313 14-// Numeric tags for DeviceType on the wire — not the string form.15-export const DEVICE_TYPE_EXTENSION = 0x01 as const;16-export const DEVICE_TYPE_ANDROID = 0x02 as const;17-export const DEVICE_TYPE_IOS = 0x03 as const;18-19-export type DeviceTypeByte =20-  | typeof DEVICE_TYPE_EXTENSION21-  | typeof DEVICE_TYPE_ANDROID22-  | typeof DEVICE_TYPE_IOS;23-2414 // ---------------------------------------------------------------------------2515 // Long-term paired device registry2616 // ---------------------------------------------------------------------------@@ -214,5 +204,3 @@214204   /** Hex-encoded 64-byte Ed25519 signature over canonicalised body. */215205   signatureHex: string;216206 }217-218-export type SyncStatusV3 = "up_to_date" | "syncing" | "unreachable" | "never";

view file

src/shared/types.ts+0 −40
@@ -204,7 +204,6 @@204204   cvc: string;205205 }206206 207-208207 export interface PendingPasskeyRequest {209208   requestId: number;210209   type: "create" | "get";@@ -504,45 +503,6 @@504503   type: MessageType;505504   payload?: unknown;506505 }507-508-// Canonical snake_case error codes returned by message handlers. Keep this509-// union the authoritative catalogue — new handlers must reuse an entry or510-// add one here. Free-form strings still appear (network/operation failures511-// surface their raw .message), so error stays string at runtime; the union512-// is the documented set callers should switch on.513-export type ErrorCode =514-  | "vault_locked"515-  | "vault_corrupted"516-  | "no_vault"517-  | "entry_not_found"518-  | "invalid_payload"519-  | "invalid_message"520-  | "invalid_sender"521-  | "wrong_passphrase"522-  | "incorrect_current_password"523-  | "recovery_not_setup"524-  | "android_only"525-  | "not_available"526-  | "pro_required"527-  | "pairing_not_verified"528-  | "sync_rejected_unpaired_initiator"529-  | "vault_not_applied"530-  | "url_required"531-  | "domain_required"532-  | "rpid_required"533-  | "rpid_and_username_required"534-  | "publickeyhex_required"535-  | "missing_payload"536-  | "login_entries_only"537-  | "passkey_not_fillable"538-  | "invalid_webhook_url"539-  | "invalid_feedback_endpoint"540-  | "message_empty"541-  | "network_error"542-  | "operation_failed"543-  | "adopt_invalid_root_key"544-  | "adopt_vault_id_mismatch"545-  | "unknown_message_type";546506 547507 export interface MessageResponse {548508   ok: boolean;

view file

src/shared/vault-adopt.ts+0 −7
@@ -277,13 +277,6 @@277277     typeof manifest.signatureHex === "string";278278 }279279 280-export function isAdoptDeviceManifestApplied(281-  frame: unknown,282-): frame is AdoptDeviceManifestApplied {283-  return (frame as Partial<AdoptDeviceManifestApplied> | null)?.type ===284-    "ADOPT_DEVICE_MANIFEST_APPLIED";285-}286-287280 function isStringRecord(value: unknown): value is Record<string, string> {288281   if (typeof value !== "object" || value === null) return false;289282   return Object.values(value).every((v) => typeof v === "string");

view file