f284ee4f084d
refactor: Remove unused runtime seams (v4.10.35)
- time
- 2026-08-29 12:39 UTC · 1 month ago
- machine
- koh2
- parent
- 703d47477a7b
↓ peach-browser-f284ee4f.tar.gzatlas at this save
files changed19 files
manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/background/messages.test.ts+12 −13src/background/paired-devices.test.ts+0 −10src/background/paired-devices.ts+0 −13src/background/passkey-crypto.ts+0 −5src/background/pending-vouched-devices.test.ts+2 −9src/background/pending-vouched-devices.ts+3 −23src/background/sync-static.test.ts+0 −16src/background/vault.ts+0 −32src/content/content.detect.test.ts+2 −47src/content/detect.ts+0 −28src/content/inject.ts+1 −1src/shared/generate.empty-pool.test.ts+4 −36src/shared/generate.test.ts+1 −34src/shared/generate.ts+0 −22src/shared/sync-types.ts+0 −22src/shared/types.ts+0 −9manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach Password Manager",4- "version": "4.10.34",4+ "version": "4.10.35",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.34",5+ "version": "4.10.35",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.34",3+ "version": "4.10.35",44 "description": "Zero-knowledge password manager",55 "license": "GPL-3.0-only",66 "type": "module",src/background/messages.test.ts+12 −13
@@ -1073,19 +1073,18 @@10731073 // --- MV-7: vouched device confirmation handlers ---10741074 10751075 async function seedPendingVouched(deviceId: string) {1076- const { addPendingVouchedDevice } = await import(1077- "./pending-vouched-devices"1078- );1079- await addPendingVouchedDevice({1080- deviceId,1081- deviceName: "New Pixel",1082- deviceType: "android",1083- longTermPublicKeyHex: "e".repeat(64),1084- signingPublicKeyHex: "a".repeat(64),1085- wsPort: 51234,1086- pairedAt: 1_700_000_000_020,1087- receivedAt: 1_700_000_000_050,1088- vouchedByDeviceId: "extension-issuer",1076+ await chrome.storage.local.set({1077+ peach_pending_vouched_devices_v1: [{1078+ deviceId,1079+ deviceName: "New Pixel",1080+ deviceType: "android",1081+ longTermPublicKeyHex: "e".repeat(64),1082+ signingPublicKeyHex: "a".repeat(64),1083+ wsPort: 51234,1084+ pairedAt: 1_700_000_000_020,1085+ receivedAt: 1_700_000_000_050,1086+ vouchedByDeviceId: "extension-issuer",1087+ }],10891088 });10901089 }10911090 src/background/paired-devices.test.ts+0 −10
@@ -7,7 +7,6 @@77 getLastSyncAt,88 listPairedDevices,99 markLastSyncedAt,10- clearDeviceRevocation,1110 isDeviceRevoked,1211 listDeviceRevocations,1312 mergeDeviceRevocations,@@ -258,15 +257,6 @@258257 ]);259258 const [record] = await listDeviceRevocations();260259 expect(record.revokedAt).toBeLessThanOrEqual(Date.now() + 5 * 60 * 1000);261- });262-263- it("clearDeviceRevocation lifts the block so a direct re-pair can readmit", async () => {264- await upsertPairedDevice(SAMPLE);265- await revokeDeviceById(SAMPLE.deviceId, "this-device");266- await clearDeviceRevocation(SAMPLE.deviceId);267-268- expect(await isDeviceRevoked(SAMPLE.deviceId)).toBe(false);269- expect(await listDeviceRevocations()).toHaveLength(0);270260 });271261 272262 it("startup repair keeps a newer direct pair and clears its older revocation", async () => {src/background/paired-devices.ts+0 −13
@@ -236,19 +236,6 @@236236 }237237 238238 /**239- * Clear a revocation. The ONLY legitimate caller is a direct, SAS-confirmed240- * re-pair: a physically present user on both devices is a stronger consent241- * signal than whatever created the original pairing. Never call this from a242- * path a remote peer can reach.243- */244-export async function clearDeviceRevocation(deviceId: string): Promise<void> {245- const existing = await listDeviceRevocations();246- const filtered = existing.filter((r) => r.deviceId !== deviceId);247- if (filtered.length === existing.length) return;248- await storeRevocations(filtered);249-}250-251-/**252239 * Repair the invalid paired+revoked overlap produced by older direct-pair253240 * handlers.254241 *src/background/passkey-crypto.ts+0 −5
@@ -352,10 +352,6 @@352352 ]);353353 }354354 355-export async function sha256(data: Uint8Array): Promise<Uint8Array> {356- return new Uint8Array(await crypto.subtle.digest("SHA-256", toArrayBuffer(data)));357-}358-359355 /**360356 * Generate a fresh credential ID. WebAuthn requires the value be unguessable361357 * and unique per credential; 32 bytes from the CSPRNG satisfies both.@@ -367,4 +363,3 @@367363 const bytes = crypto.getRandomValues(new Uint8Array(32));368364 return { bytes, base64url: base64urlEncode(bytes) };369365 }370-src/background/pending-vouched-devices.test.ts+2 −9
@@ -1,6 +1,5 @@11 import { beforeEach, describe, expect, it } from "vitest";22 import {3- addPendingVouchedDevice,43 listPendingVouchedDevices,54 removePendingVouchedDevice,65 } from "./pending-vouched-devices";@@ -46,16 +45,10 @@4645 describe("pending vouched devices", () => {4746 beforeEach(() => makeStorageStub());4847 49- it("add + list + remove", async () => {50- await addPendingVouchedDevice(SAMPLE);48+ it("lists and removes a pending record created by an older build", async () => {49+ await chrome.storage.local.set({ peach_pending_vouched_devices_v1: [SAMPLE] });5150 expect(await listPendingVouchedDevices()).toEqual([SAMPLE]);5251 await removePendingVouchedDevice(SAMPLE.deviceId);5352 expect(await listPendingVouchedDevices()).toEqual([]);54- });55-56- it("caps oversized peer device names", async () => {57- await addPendingVouchedDevice({ ...SAMPLE, deviceName: "V".repeat(100) });58- const [d] = await listPendingVouchedDevices();59- expect(d.deviceName).toBe("V".repeat(64));6053 });6154 });src/background/pending-vouched-devices.ts+3 −23
@@ -1,19 +1,13 @@11 // Pending vouched devices — audit fix MV-7.22 //3-// A verified extension-signed device manifest may introduce a device this4-// install has never seen. Previously such devices were upserted straight5-// into the paired-devices registry (trustSource:"vouched") with no user6-// confirmation — making the extension a single point of total compromise7-// for the device graph. Now new vouched devices land HERE, sync-inert,8-// until the user confirms them in the popup (CONFIRM_VOUCHED_DEVICE) or9-// rejects them (REJECT_VOUCHED_DEVICE).3+// Compatibility reader for pending devices created by older builds. Current4+// same-vault manifests are admitted directly, but stored requests remain5+// actionable until the user confirms or rejects them in Settings.106 //117 // Storage: chrome.storage.local, same locality rules as paired-devices.ts —128 // these records contain public keys and must not roam through a third-party139 // sync back-end.1410 15-import { capDeviceName } from "../shared/sync-limits";16-1711 export interface PendingVouchedDevice {1812 deviceId: string;1913 deviceName: string;@@ -29,7 +23,6 @@2923 }3024 3125 const PENDING_KEY = "peach_pending_vouched_devices_v1";32-const MAX_PENDING = 50;3326 3427 export async function listPendingVouchedDevices(): Promise<PendingVouchedDevice[]> {3528 try {@@ -40,19 +33,6 @@4033 } catch {4134 return [];4235 }43-}44-45-/** Add or refresh a pending device (keyed by deviceId). Bounded queue. */46-export async function addPendingVouchedDevice(47- device: PendingVouchedDevice,48-): Promise<void> {49- // Vouch requests carry a peer-chosen name; bound it like every other50- // inbound name so the pending queue cannot be grown without limit.51- device = { ...device, deviceName: capDeviceName(device.deviceName) };52- const pending = await listPendingVouchedDevices();53- const next = pending.filter((d) => d.deviceId !== device.deviceId);54- next.push(device);55- await chrome.storage.local.set({ [PENDING_KEY]: next.slice(-MAX_PENDING) });5636 }5737 5838 export async function getPendingVouchedDevice(src/background/sync-static.test.ts+0 −16
@@ -34,22 +34,6 @@3434 expect(fnBody).not.toMatch(/triggerSyncIfReachable|TRIGGER_SYNC|fireSyncTrigger/);3535 });3636 37- it("addEntryDirect / updateEntryDirect / deleteEntryDirect do not call fireSyncTrigger", () => {38- const vaultSrc = src("./vault.ts");39- for (const fn of ["addEntryDirect", "updateEntryDirect", "deleteEntryDirect"]) {40- const start = vaultSrc.indexOf(`function ${fn}`);41- if (start === -1) continue;42- // Slice up to the start of the very next export declaration so we don't43- // bleed into adjacent function bodies or their JSDoc comments.44- const nextExport = vaultSrc.indexOf("\nexport ", start + fn.length);45- const end = nextExport > start ? nextExport : start + 600;46- const body = vaultSrc.slice(start, end);47- expect(body, `${fn} must not call fireSyncTrigger`).not.toMatch(48- /triggerSyncIfReachable|TRIGGER_SYNC|fireSyncTrigger/,49- );50- }51- });52-5337 it("device registry is stored in chrome.storage.local, not chrome.storage.sync", () => {5438 const registrySrc = src("./paired-devices.ts");5539 expect(registrySrc).toContain("storage.local");src/background/vault.ts+0 −32
@@ -2306,38 +2306,6 @@23062306 }23072307 23082308 /**2309- * Internal write variants — bypass `fireSyncTrigger` so the sync layer can2310- * apply entries without cascading into another outbound sync round.2311- *2312- * INVARIANT: callers must not pass entries whose IDs collide with existing2313- * active (non-deleted) entries unless the intent is an explicit overwrite.2314- * These functions do not validate field-level constraints (password required,2315- * card number required, etc.) — they are trust-the-caller paths for sync and2316- * test harness use only.2317- */2318-export async function addEntryDirect(entry: VaultEntry): Promise<void> {2319- if (!derivedKey) throw new Error("vault_locked");2320- entries = [...entries, entry];2321- await persistEntries("sync_received");2322-}2323-2324-export async function updateEntryDirect(patch: VaultEntry): Promise<void> {2325- if (!derivedKey) throw new Error("vault_locked");2326- const idx = entries.findIndex((e) => e.id === patch.id);2327- if (idx === -1) throw new Error("entry_not_found");2328- entries[idx] = { ...entries[idx], ...patch };2329- await persistEntries("sync_received");2330-}2331-2332-export async function deleteEntryDirect(id: string, deletedAt: number): Promise<void> {2333- if (!derivedKey) throw new Error("vault_locked");2334- const idx = entries.findIndex((e) => e.id === id);2335- if (idx === -1) throw new Error("entry_not_found");2336- entries[idx] = { ...entries[idx], deletedAt, modifiedAt: deletedAt };2337- await persistEntries("sync_received");2338-}2339-2340-/**23412309 * Apply a batch of remote entries + tombstones from a sync peer.23422310 * Last-write-wins by `modifiedAt`; a tombstone supersedes a live entry when23432311 * `deletedAt > modifiedAt` on the local side. Mutates `entries` directly andsrc/content/content.detect.test.ts+2 −47
@@ -1,7 +1,6 @@11 // @vitest-environment jsdom22 //33 // Test File 7 — detect.ts field detection (T-DT01–T-DT21).4-// Plus Addendum 1 — split-OTP grouping.54 //65 // detect.ts is the foundation of the whole autofill layer: every gate in76 // index.ts asks it "is this a login/signup/email-first/card/totp form?". A@@ -307,7 +306,7 @@307306 });308307 });309308 310-// ── Addendum 1 — split-OTP grouping ─────────────────────────────────────────309+// ── Split-OTP grouping ──────────────────────────────────────────────────────311310 // The existing detector is findOTPDigitBoxGroup: it returns the ordered set of312311 // single-character boxes (4–10 of them) or null. index.ts uses it to open ONE313312 // TOTP selector anchored on the first box instead of icon-spamming all six.@@ -335,9 +334,7 @@335334 expect(isTOTPField(box)).toBe(false);336335 });337336 338- // T-DT21: a group below the 4-box floor is not an OTP group (existing339- // threshold). Addendum 1 proposes a 3+ threshold for isSplitOtpGroup — see the340- // skipped block below.337+ // T-DT21: a group below the 4-box floor is not an OTP group.341338 it("T-DT21: only 2 single-char boxes → not an OTP group (null)", () => {342339 const f = form();343340 const boxes = [@@ -407,47 +404,5 @@407404 expect(isTOTPField(boxes[0]!)).toBe(false);408405 expect(findOtpSignalingGroup(boxes[0]!)).toBeNull();409406 expect(findOTPDigitBoxGroup(boxes[0]!)).not.toBeNull();410- });411-});412-413-// Addendum 1 PROPOSED API — isSplitOtpGroup(field) with a 3+ threshold and a414-// numeric-only guard so an initials form (3 single-char *alpha* inputs) is not415-// treated as OTP. This function does not exist yet; these tests activate the416-// moment it is exported from detect.ts. They are the executable spec for the417-// addendum. See INTEGRATION-CONTRACT.md.418-import * as detectModule from "./detect";419-const hasIsSplitOtpGroup =420- typeof (detectModule as unknown as { isSplitOtpGroup?: unknown }).isSplitOtpGroup === "function";421-422-describe.skipIf(!hasIsSplitOtpGroup)("Addendum 1 — proposed isSplitOtpGroup (requires detect.ts change)", () => {423- const isSplitOtpGroup = (detectModule as unknown as {424- isSplitOtpGroup: (f: HTMLInputElement) => boolean;425- }).isSplitOtpGroup;426-427- it("T-DT18 (proposed): 6 adjacent numeric single-char inputs → true", () => {428- const f = form();429- let first: HTMLInputElement | null = null;430- for (let i = 0; i < 6; i++) {431- const b = makeInput({ type: "text", maxlength: "1", inputmode: "numeric" }, f);432- first ??= b;433- }434- expect(isSplitOtpGroup(first!)).toBe(true);435- });436-437- it("T-DT20 (proposed): 3 single-char ALPHA inputs (initials) → false", () => {438- const f = form();439- let first: HTMLInputElement | null = null;440- for (let i = 0; i < 3; i++) {441- const b = makeInput({ type: "text", maxlength: "1" }, f); // no numeric mode442- first ??= b;443- }444- expect(isSplitOtpGroup(first!)).toBe(false);445- });446-447- it("T-DT21 (proposed): a 2-box group is below the 3+ threshold → false", () => {448- const f = form();449- const a = makeInput({ type: "text", maxlength: "1", inputmode: "numeric" }, f);450- makeInput({ type: "text", maxlength: "1", inputmode: "numeric" }, f);451- expect(isSplitOtpGroup(a)).toBe(false);452407 });453408 });src/content/detect.ts+0 −28
@@ -306,34 +306,6 @@306306 return digitBoxes;307307 }308308 309-// Addendum 1: true when `field` belongs to a split single-character OTP group —310-// 3+ maxlength=1 inputs in the same form that each carry a numeric signal311-// (inputmode=numeric, type=tel, pattern=[0-9]*/\d*, or autocomplete=one-time-code).312-// Distinct from findOTPDigitBoxGroup, which gates on a 4–10 box count and accepts313-// any text/number/tel box: this drops the floor to 3 but adds the numeric guard,314-// so a 3-letter "initials" form is never mistaken for OTP entry.315-//316-// No icon-suppression guard is wired at the injection sites: every injectFieldIcon317-// path already filters maxLength===1 fields upstream (detectLoginFields skips them,318-// isTOTPField rejects them), so a split-OTP box never reaches icon injection. This319-// export is the numeric-guarded classifier the T-DT18–21 spec pins and a shared320-// seam for any future gate that needs it.321-export function isSplitOtpGroup(field: HTMLInputElement): boolean {322- if (!isSingleDigitBox(field)) return false;323- const form = field.form;324- if (!form) return false;325- const siblings = Array.from(form.querySelectorAll<HTMLInputElement>("input")).filter(326- (el) =>327- isSingleDigitBox(el) &&328- (el.inputMode === "numeric" ||329- el.type === "tel" ||330- el.pattern === "[0-9]*" ||331- el.pattern === "\\d*" ||332- el.autocomplete === "one-time-code")333- );334- return siblings.length >= 3 && siblings.includes(field);335-}336-337309 // Returns the ordered OTP-signaling inputs sharing `field`'s form when there are338310 // 4–10 of them — the split-digit-box pattern used by sites whose individual339311 // boxes lack maxlength="1" (so isSingleDigitBox / findOTPDigitBoxGroup miss themsrc/content/inject.ts+1 −1
@@ -3655,7 +3655,7 @@36553655 36563656 interface CardSelectorOptions {36573657 domain?: string;3658- /** Caller-provided fill — receives the full CardEntry payload from the SW. */3658+ /** Caller-provided fill — receives the full card payload from the SW. */36593659 onFill: (entry: import("../shared/types").VaultEntry) => void;36603660 /** Called when the user explicitly dismisses (Escape, click-outside, ✕). */36613661 onDismiss?: () => void;src/shared/generate.empty-pool.test.ts+4 −36
@@ -1,7 +1,7 @@1-// T-025: generate.ts empty-pool guard, 128-char boundary, minLength > maxLength1+// T-025: generate.ts empty-pool guard and 128-char boundary22 //3-// Why these tests exist: generatePassword and generateSmartPassword have three4-// edge-case behaviors that are not tested:3+// Why these tests exist: generatePassword has two edge-case behaviors that are4+// not tested elsewhere:55 //66 // 1. All character classes forbidden → the pool falls back to lowercase77 // ("abcdefghijklmnopqrstuvwxyz") rather than looping forever or throwing.@@ -11,16 +11,12 @@1111 // exactly 128 characters when length=128 and must handle the boundary1212 // without a performance cliff or off-by-one.1313 //14-// 3. minLength > maxLength in a site policy — generate.ts clamps in15-// declaration order: raise to minLength first, then cap to maxLength.16-// When minLength=50 and maxLength=16, the result must be exactly 16 chars.17-//1814 // These tests also serve as a regression guard against any future refactor of1915 // the rejection-sampling loop: if the loop condition changes, the 128-char2016 // case catches a potential performance regression (loop would run for 30+s).2117 2218 import { describe, expect, it } from "vitest";23-import { generatePassword, generateSmartPassword } from "./generate";19+import { generatePassword } from "./generate";2420 2521 describe("T-025: generatePassword edge cases", () => {2622 // T-025a: all character classes excluded via forbiddenChars → falls back to@@ -98,33 +94,5 @@9894 const b = generatePassword({ length: 32 });9995 // Collision probability: 1/(pool^32) ≈ 0 for any reasonable pool10096 expect(a).not.toBe(b);101- });102-});103-104-describe("T-025: generateSmartPassword policy clamping", () => {105- // T-025f: minLength > maxLength — generateSmartPassword clamps to maxLength.106- // The evaluation order is: raise to minLength first (→ 50), then cap to107- // maxLength (→ 16). Site policy maxLength must win.108- //109- // Bug this catches: swapping the clamp order so minLength wins over maxLength110- // would produce a password that violates the site's stated maximum, causing111- // sign-up failure on strict sites.112- it("T-025f: minLength > maxLength → maxLength wins (site maximum clamped)", () => {113- const password = generateSmartPassword({ minLength: 50, maxLength: 16 });114- expect(password.length).toBeLessThanOrEqual(16);115- });116-117- // T-025g: maxLength below the safety floor of 8 → floor applies.118- // generateSmartPassword has `if (length < 8) length = 8` to prevent119- // trivially weak passwords being auto-generated for restrictive sites.120- it("T-025g: maxLength=4 → safety floor raises output to at least 8 chars", () => {121- const password = generateSmartPassword({ maxLength: 4 });122- expect(password.length).toBeGreaterThanOrEqual(8);123- });124-125- // T-025h: forbidsSymbols=true → no symbol characters in output.126- it("T-025h: forbidsSymbols=true → no !@#$%^&* in password", () => {127- const password = generateSmartPassword({ forbidsSymbols: true, minLength: 20 });128- expect(password).not.toMatch(/[!@#$%^&*]/);12997 });13098 });src/shared/generate.test.ts+1 −34
@@ -1,5 +1,5 @@11 import { describe, expect, it } from "vitest";2-import { generatePassword, generateSmartPassword } from "./generate";2+import { generatePassword } from "./generate";33 44 describe("generatePassword", () => {55 it("produces a password of the requested length", () => {@@ -39,39 +39,6 @@3939 includeSymbols: false,4040 });4141 expect(pw).toHaveLength(16);42- });43-});44-45-describe("generateSmartPassword", () => {46- it("clamps to policy.maxLength", () => {47- const pw = generateSmartPassword({ maxLength: 12 });48- expect(pw.length).toBeLessThanOrEqual(12);49- });50-51- it("raises to policy.minLength", () => {52- const pw = generateSmartPassword({ minLength: 32 });53- expect(pw.length).toBeGreaterThanOrEqual(32);54- });55-56- it("excludes forbidden chars (Oracle-style)", () => {57- const pw = generateSmartPassword({58- maxLength: 40,59- forbiddenChars: "`~<>\\",60- });61- expect(pw.length).toBeLessThanOrEqual(40);62- for (const c of ["`", "~", "<", ">", "\\"]) {63- expect(pw).not.toContain(c);64- }65- });66-67- it("honors forbidsSymbols", () => {68- const pw = generateSmartPassword({ forbidsSymbols: true });69- expect(pw).toMatch(/^[A-Za-z0-9]+$/);70- });71-72- it("defaults to length 20 when policy is empty", () => {73- const pw = generateSmartPassword({});74- expect(pw).toHaveLength(20);7542 });7643 7744 it("rejection sampling still yields exact length and only pool chars (audit INFO — modulo bias)", () => {src/shared/generate.ts+0 −22
@@ -1,5 +1,3 @@1-import type { PasswordPolicy } from "./types";2-31 export interface GeneratorOptions {42 length: number;53 includeUppercase: boolean;@@ -52,24 +50,4 @@5250 }5351 }5452 return password;55-}56-57-/**58- * Generate a password that satisfies a detected site policy — clamps length59- * to maxLength, raises to minLength, honors forbidsSymbols, and excludes60- * forbiddenChars. Used by the inline popup when the user clicks "regen" on a61- * signup form with parseable requirements.62- */63-export function generateSmartPassword(policy: PasswordPolicy = {}): string {64- const defaultLength = 20;65- let length = defaultLength;66- if (policy.minLength) length = Math.max(length, policy.minLength);67- if (policy.maxLength) length = Math.min(length, policy.maxLength);68- if (length < 8) length = 8;69-70- return generatePassword({71- length,72- includeSymbols: !policy.forbidsSymbols,73- forbiddenChars: policy.forbiddenChars,74- });7553 }src/shared/sync-types.ts+0 −22
@@ -101,28 +101,6 @@101101 | { type: "SYNC_ERROR"; error: string };102102 103103 // ---------------------------------------------------------------------------104-// Browser-to-browser relay frames (phone forwards; see peach-android's105-// RelayFrames.kt / RelayHub.kt, the source of truth this mirrors — NOT the106-// older 4-frame PEACH_SYNC_RELAY_DESIGN.md sketch, which is stale).107-//108-// Type-only for now: nothing in this repo sends or handles these yet — no109-// browser can listen, so relaying laptop-to-laptop traffic through a shared110-// phone is unbuilt browser-side work, tracked separately. This exists so111-// that work starts from the real wire contract instead of the stale doc.112-// ---------------------------------------------------------------------------113-114-/** RelayMessage.targetDeviceId/peerDeviceId are the destination's Noise115- * static public key as lowercase hex — NOT `PairedDevice.deviceId`. */116-export type RelayMessage =117- | { type: "RELAY_REQUEST"; targetDeviceId: string }118- | { type: "RELAY_ACCEPT" }119- | { type: "RELAY_UNAVAILABLE" }120- | { type: "RELAY_DISABLED" }121- | { type: "RELAY_OPEN"; peerDeviceId: string }122- | { type: "RELAY_FRAME"; payload: string }123- | { type: "RELAY_CLOSE" };124-125-// ---------------------------------------------------------------------------126104 // v3 device manifest — propagates trust between mobile devices (v2 brief §4.3)127105 // ---------------------------------------------------------------------------128106 src/shared/types.ts+0 −9
@@ -195,15 +195,6 @@195195 deletedAt?: number;196196 }197197 198-export interface CardEntry extends VaultEntry {199- type: "card";200- cardholderName: string;201- cardNumber: string;202- expMonth: string;203- expYear: string;204- cvc: string;205-}206-207198 export interface PendingPasskeyRequest {208199 requestId: number;209200 type: "create" | "get";