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
↓ peach-browser-78a43519.tar.gzatlas at this save
files changed21 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 −7manifest.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": {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",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",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;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 }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-}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-}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);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 ─────────────────────────────────────────────────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-}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-}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-}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-}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-}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();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- : () => {};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 }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 filesrc/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";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;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");