705a2fcd8d9a

L5/QUA-004: standardize error codes to snake_case across messages.ts, vault.ts and add ErrorCode type union

time
2026-05-16 05:14 UTC · 5 months ago
machine
koh2
parent
214299cfc6f6
11 files src/background/messages.adversarial.test.ts+2 −2src/background/messages.test.ts+9 −9src/background/messages.ts+36 −36src/background/pairing-completed-gate.test.ts+1 −1src/background/passkey.ts+2 −2src/background/vault.ts+16 −16src/popup/App.tsx+1 −1src/popup/views/Add.tsx+1 −1src/popup/views/Import.tsx+1 −1src/popup/views/RecoveryWordEntry.tsx+1 −1src/shared/types.ts+39 −0
src/background/messages.adversarial.test.ts+2 −2
@@ -297,7 +297,7 @@297297         );298298       });299299       expect(r.ok).toBe(false);300-      expect(r.error).toBe("invalid sender");300+      expect(r.error).toBe("invalid_sender");301301     });302302 303303     it("sender with no id (extension UI) is accepted", async () => {@@ -797,7 +797,7 @@797797           );798798         });799799         expect(r.ok).toBe(false);800-        expect(r.error).toBe("invalid sender");800+        expect(r.error).toBe("invalid_sender");801801       });802802     }803803   });

view file

src/background/messages.test.ts+9 −9
@@ -204,7 +204,7 @@204204           url: "https://github.com",205205         },206206       })207-    ).resolves.toMatchObject({ ok: false, error: "vault locked" });207+    ).resolves.toMatchObject({ ok: false, error: "vault_locked" });208208   });209209 210210   it("ADD_ENTRY with a missing password returns a validation error", async () => {@@ -292,7 +292,7 @@292292         type: "DELETE_ENTRY",293293         payload: { id: "missing" },294294       })295-    ).resolves.toMatchObject({ ok: false, error: "entry not found" });295+    ).resolves.toMatchObject({ ok: false, error: "entry_not_found" });296296   });297297 298298   it("GET_ENTRY_FOR_FILL returns a safe login entry without privateKeyJwk", async () => {@@ -331,7 +331,7 @@331331         type: "GET_ENTRY_FOR_FILL",332332         payload: { id: "missing" },333333       })334-    ).resolves.toMatchObject({ ok: false, error: "vault locked" });334+    ).resolves.toMatchObject({ ok: false, error: "vault_locked" });335335   });336336 337337   it("GET_ENTRY_FOR_FILL rejects passkey-only entries", async () => {@@ -364,7 +364,7 @@364364         type: "GET_ENTRY_FOR_FILL",365365         payload: { id: entry.id },366366       })367-    ).resolves.toMatchObject({ ok: false, error: "passkey not fillable" });367+    ).resolves.toMatchObject({ ok: false, error: "passkey_not_fillable" });368368   });369369 370370   it("AUTOFILL_QUERY returns matched domain metadata without secrets", async () => {@@ -491,7 +491,7 @@491491           newPassword: "another-pass",492492         },493493       })494-    ).resolves.toMatchObject({ ok: false, error: "incorrect current password" });494+    ).resolves.toMatchObject({ ok: false, error: "incorrect_current_password" });495495   });496496 497497   it("UPDATE_SETTINGS reschedules auto-lock using the new minutes value", async () => {@@ -546,7 +546,7 @@546546 547547     await expect(dispatch({ type: "GET_ENTRIES" }, { id: "other-extension" })).resolves.toMatchObject({548548       ok: false,549-      error: "invalid sender",549+      error: "invalid_sender",550550     });551551 552552     await expect(dispatch({ type: "GET_ENTRIES" }, {})).resolves.toMatchObject({@@ -614,7 +614,7 @@614614           trigger: "password_copied",615615         },616616       })617-    ).resolves.toMatchObject({ ok: false, error: "invalid webhook url" });617+    ).resolves.toMatchObject({ ok: false, error: "invalid_webhook_url" });618618 619619     expect(fetchMock).not.toHaveBeenCalled();620620   });@@ -645,7 +645,7 @@645645       },646646     });647647 648-    expect(response).toMatchObject({ ok: false, error: "message is empty" });648+    expect(response).toMatchObject({ ok: false, error: "message_empty" });649649     expect(fetchMock).not.toHaveBeenCalled();650650   });651651 @@ -655,7 +655,7 @@655655     vault.lock();656656     await expect(dispatch({ type: "GET_IDENTITIES" })).resolves.toMatchObject({657657       ok: false,658-      error: "vault locked",658+      error: "vault_locked",659659     });660660   });661661 

view file

src/background/messages.ts+36 −36
@@ -306,7 +306,7 @@306306   chrome.runtime.onMessage.addListener(307307     (msg: unknown, sender, sendResponse) => {308308       if (!isMessage(msg)) {309-        sendResponse({ ok: false, error: "invalid message" });309+        sendResponse({ ok: false, error: "invalid_message" });310310         return true;311311       }312312       handleMessage(msg, sender)@@ -327,7 +327,7 @@327327   const reg = (globalThis as any).__peachRegisterMessageHandler;328328   if (typeof reg === "function") {329329     reg(async (m: unknown) => {330-      if (!isMessage(m)) return { ok: false, error: "invalid message" };330+      if (!isMessage(m)) return { ok: false, error: "invalid_message" };331331       try {332332         return await handleMessage(m);333333       } catch (err) {@@ -407,7 +407,7 @@407407       sender.id !== undefined &&408408       sender.id !== chrome.runtime.id409409     ) {410-      return { ok: false, error: "invalid sender" };410+      return { ok: false, error: "invalid_sender" };411411     }412412 413413     if (shouldRefreshAutoLock(msg.type) && vault.isUnlocked()) {@@ -453,7 +453,7 @@453453       }454454       case "GET_RECOVERY_WORDS": {455455         // Must be unlocked and have recovery set up (vault must be v2.12+).456-        if (!vault.isUnlocked()) return { ok: false, error: "vault locked" };456+        if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };457457         try {458458           const words = await vault.getRecoveryWords();459459           return { ok: true, data: words };@@ -470,7 +470,7 @@470470           newPassword: string;471471         };472472         const stored = await vault.loadSlotA();473-        if (!stored) return { ok: false, error: "no vault" };473+        if (!stored) return { ok: false, error: "no_vault" };474474         if (!stored.recovery_salt || !stored.recovery_wrapped_root_key) {475475           return { ok: false, error: "recovery_not_setup" };476476         }@@ -512,7 +512,7 @@512512         if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };513513         return { ok: true, data: vault.getEntries() };514514       case "ADD_ENTRY": {515-        if (!vault.isUnlocked()) return { ok: false, error: "vault locked" };515+        if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };516516         const payload = msg.payload;517517         if (!payload || typeof payload !== "object" || Array.isArray(payload)) {518518           return { ok: false, error: "invalid_payload" };@@ -591,10 +591,10 @@591591       case "ADD_ADDITIONAL_URL": {592592         const { id, url } = msg.payload as { id: string; url: string };593593         const trimmed = typeof url === "string" ? url.trim() : "";594-        if (!trimmed) return { ok: false, error: "url required" };594+        if (!trimmed) return { ok: false, error: "url_required" };595595         const existing = vault.getEntryById(id);596-        if (!existing) return { ok: false, error: "entry not found" };597-        if (existing.type !== "login") return { ok: false, error: "login entries only" };596+        if (!existing) return { ok: false, error: "entry_not_found" };597+        if (existing.type !== "login") return { ok: false, error: "login_entries_only" };598598         const merged = [...(existing.additionalUrls ?? []), trimmed];599599         await vault.updateEntry(id, { additionalUrls: merged });600600         return { ok: true };@@ -673,7 +673,7 @@673673           publicKeyHex?: string;674674           deviceIp?: string;675675         };676-        if (!publicKeyHex) return { ok: false, error: "publicKeyHex required" };676+        if (!publicKeyHex) return { ok: false, error: "publickeyhex_required" };677677         // Only accept publicKeyHex values that just completed a real Noise NK678678         // handshake (initiator or responder side). Without this gate, anyone679679         // who can sendMessage to the background can pair an arbitrary key@@ -707,7 +707,7 @@707707       }708708       case "AUTOFILL_QUERY": {709709         if (!vault.isUnlocked()) {710-          return { ok: false, error: "VAULT_LOCKED" };710+          return { ok: false, error: "vault_locked" };711711         }712712         const { domain, includeAll, kind } = msg.payload as {713713           domain: string;@@ -767,14 +767,14 @@767767         };768768       }769769       case "GET_ENTRY_FOR_FILL": {770-        if (!vault.isUnlocked()) return { ok: false, error: "vault locked" };770+        if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };771771         const { id } = msg.payload as { id: string };772772         const entry = vault.getEntryById(id);773-        if (!entry) return { ok: false, error: "entry not found" };773+        if (!entry) return { ok: false, error: "entry_not_found" };774774         // Passkey-only entries (passkey credential, no password) cannot be775775         // filled into a form — sign-in goes through the WebAuthn path. A776776         // login that ALSO has a passkey is still fillable for its password.777-        if (isPasskeyOnly(entry)) return { ok: false, error: "passkey not fillable" };777+        if (isPasskeyOnly(entry)) return { ok: false, error: "passkey_not_fillable" };778778         // Strip the passkey credential from the response — content scripts779779         // never need the private key, and there's no reason to expose the780780         // public credential metadata over the boundary either.@@ -854,7 +854,7 @@854854           trigger: "password_copied" | "username_copied" | "autofilled" | "submitted" | "test";855855         };856856         if (!isAllowedCanaryWebhookUrl(webhookUrl)) {857-          return { ok: false, error: "invalid webhook url" };857+          return { ok: false, error: "invalid_webhook_url" };858858         }859859         const body = JSON.stringify({860860           event: trigger === "test" ? "peach_canary_test" : "peach_canary_accessed",@@ -877,7 +877,7 @@877877             });878878             return { ok: res.ok, error: res.ok ? undefined : `HTTP ${res.status}` };879879           } catch (err) {880-            return { ok: false, error: err instanceof Error ? err.message : "network error" };880+            return { ok: false, error: err instanceof Error ? err.message : "network_error" };881881           }882882         }883883         // Non-test triggers are fire-and-forget@@ -942,7 +942,7 @@942942         return { ok: true };943943       }944944       case "GET_IDENTITIES": {945-        if (!vault.isUnlocked()) return { ok: false, error: "vault locked" };945+        if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };946946         return { ok: true, data: vault.getIdentities() };947947       }948948       case "TOUCH_AUTO_LOCK": {@@ -953,26 +953,26 @@953953       case "GET_ADVANCED_HEALTH_REPORT": {954954         const gate = await requirePro("advanced_health");955955         if (!gate.allowed) return { ok: false, error: "pro_required" };956-        if (!vault.isUnlocked()) return { ok: false, error: "vault locked" };956+        if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };957957         const report = await buildAdvancedHealthReport(vault.getEntries());958958         return { ok: true, data: report };959959       }960960       case "INITIATE_PASSWORD_CHANGE": {961961         const gate = await requirePro("smart_password_change");962962         if (!gate.allowed) return { ok: false, error: "pro_required" };963-        if (!vault.isUnlocked()) return { ok: false, error: "vault locked" };963+        if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };964964         const { id } = msg.payload as { id: string };965965         const entry = vault.getEntryById(id);966-        if (!entry) return { ok: false, error: "entry not found" };966+        if (!entry) return { ok: false, error: "entry_not_found" };967967         const url = resolveResetUrl(entry);968-        if (!url) return { ok: false, error: "no reset url" };968+        if (!url) return { ok: false, error: "no_reset_url" };969969         try {970970           await chrome.tabs.create({ url });971971           return { ok: true, data: { url } };972972         } catch (err) {973973           return {974974             ok: false,975-            error: err instanceof Error ? err.message : "failed to open tab",975+            error: err instanceof Error ? err.message : "failed_to_open_tab",976976           };977977         }978978       }@@ -1002,7 +1002,7 @@10021002         const changed = await vault.changePassword(currentPassword, newPassword);10031003         return changed10041004           ? { ok: true }1005-          : { ok: false, error: "incorrect current password" };1005+          : { ok: false, error: "incorrect_current_password" };10061006       }10071007       case "GET_SITE_PASSWORD_POLICY": {10081008         const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });@@ -1202,7 +1202,7 @@12021202           userName?: string;12031203           userDisplayName?: string;12041204         };1205-        if (!rpId || !userName) return { ok: false, error: "rpId and userName required" };1205+        if (!rpId || !userName) return { ok: false, error: "rpid_and_username_required" };12061206         return await initiateFirefoxPasskeyCreate({12071207           rpId,12081208           rpName: rpName ?? rpId,@@ -1213,20 +1213,20 @@12131213 12141214       case "COMPLETE_FIREFOX_PASSKEY_CREATE": {12151215         const payload = msg.payload as Parameters<typeof completeFirefoxPasskeyCreate>[0] | undefined;1216-        if (!payload) return { ok: false, error: "missing payload" };1216+        if (!payload) return { ok: false, error: "missing_payload" };12171217         return await completeFirefoxPasskeyCreate(payload);12181218       }12191219 12201220       case "PASSKEY_OFFER_DECLINE": {12211221         const { domain } = (msg.payload ?? {}) as { domain?: string };1222-        if (!domain) return { ok: false, error: "domain required" };1222+        if (!domain) return { ok: false, error: "domain_required" };12231223         await recordPasskeyOfferDecline(domain);12241224         return { ok: true };12251225       }12261226 12271227       case "GET_PASSKEY_OFFER_DECLINED": {12281228         const { domain } = (msg.payload ?? {}) as { domain?: string };1229-        if (!domain) return { ok: false, error: "domain required" };1229+        if (!domain) return { ok: false, error: "domain_required" };12301230         return { ok: true, data: await isPasskeyOfferDeclined(domain) };12311231       }12321232 @@ -1235,7 +1235,7 @@12351235           rpId?: string;12361236           allowCredentials?: Array<{ id: string; type?: string }>;12371237         } | undefined;1238-        if (!payload?.rpId) return { ok: false, error: "rpId required" };1238+        if (!payload?.rpId) return { ok: false, error: "rpid_required" };12391239         return await handlePasskeyHasCredentials({12401240           rpId: payload.rpId,12411241           allowCredentials: payload.allowCredentials ?? [],@@ -1245,14 +1245,14 @@12451245       case "PASSKEY_CREATE": {12461246         const payload = msg.payload as Parameters<typeof handleFirefoxPasskeyCreate>[0] | undefined;12471247         if (!payload?.rpId || !payload?.userName) {1248-          return { ok: false, error: "rpId and userName required" };1248+          return { ok: false, error: "rpid_and_username_required" };12491249         }12501250         return await handleFirefoxPasskeyCreate(payload);12511251       }12521252 12531253       case "PASSKEY_GET": {12541254         const payload = msg.payload as Parameters<typeof handleFirefoxPasskeyGet>[0] | undefined;1255-        if (!payload?.rpId) return { ok: false, error: "rpId required" };1255+        if (!payload?.rpId) return { ok: false, error: "rpid_required" };12561256         return await handleFirefoxPasskeyGet(payload);12571257       }12581258 @@ -1270,7 +1270,7 @@12701270 12711271       case "GET_PASSKEY_FOR_DOMAIN": {12721272         const { domain } = (msg.payload ?? {}) as { domain?: string };1273-        if (!domain) return { ok: false, error: "domain required" };1273+        if (!domain) return { ok: false, error: "domain_required" };12741274         if (!vault.isUnlocked()) return { ok: true, data: null };12751275         const match = vault12761276           .getEntries()@@ -1310,10 +1310,10 @@13101310           version: string;13111311           browser: string;13121312         };1313-        if (!message.trim()) return { ok: false, error: "message is empty" };1313+        if (!message.trim()) return { ok: false, error: "message_empty" };13141314         if (!FEEDBACK_ENDPOINT) return { ok: true };13151315         if (!isAllowedCanaryWebhookUrl(FEEDBACK_ENDPOINT)) {1316-          return { ok: false, error: "invalid feedback endpoint" };1316+          return { ok: false, error: "invalid_feedback_endpoint" };13171317         }13181318         try {13191319           const res = await fetch(FEEDBACK_ENDPOINT, {@@ -1323,7 +1323,7 @@13231323           });13241324           return { ok: res.ok, error: res.ok ? undefined : `HTTP ${res.status}` };13251325         } catch (err) {1326-          return { ok: false, error: err instanceof Error ? err.message : "network error" };1326+          return { ok: false, error: err instanceof Error ? err.message : "network_error" };13271327         }13281328       }13291329       case "TRIGGER_SYNC": {@@ -1504,12 +1504,12 @@15041504       }15051505 15061506       default:1507-        return { ok: false, error: "unknown message type" };1507+        return { ok: false, error: "unknown_message_type" };15081508     }15091509   } catch (err) {15101510     return {15111511       ok: false,1512-      error: err instanceof Error ? err.message : "operation failed",1512+      error: err instanceof Error ? err.message : "operation_failed",15131513     };15141514   }15151515 }

view file

src/background/pairing-completed-gate.test.ts+1 −1
@@ -148,6 +148,6 @@148148       payload: { deviceName: "X" },149149     });150150     expect(r.ok).toBe(false);151-    expect(r.error).toBe("publicKeyHex required");151+    expect(r.error).toBe("publickeyhex_required");152152   });153153 });

view file

src/background/passkey.ts+2 −2
@@ -631,7 +631,7 @@631631   }632632   if (!vault.isUnlocked()) {633633     await openApprovalPopup();634-    return { ok: false, error: "vault locked" };634+    return { ok: false, error: "vault_locked" };635635   }636636   if (!chrome.storage.session) {637637     return { ok: false, error: "session storage unavailable" };@@ -676,7 +676,7 @@676676   backupEligible?: boolean;677677   backupState?: boolean;678678 }): Promise<{ ok: boolean; error?: string }> {679-  if (!vault.isUnlocked()) return { ok: false, error: "vault locked" };679+  if (!vault.isUnlocked()) return { ok: false, error: "vault_locked" };680680 681681   const passkey: PasskeyData = {682682     rpId: payload.rpId,

view file

src/background/vault.ts+16 −16
@@ -1254,14 +1254,14 @@12541254 }12551255 12561256 export function getEntries(): VaultEntry[] {1257-  if (!derivedKey) throw new Error("vault locked");1257+  if (!derivedKey) throw new Error("vault_locked");12581258   return entries.filter((entry) => !entry.deletedAt);12591259 }12601260 12611261 const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;12621262 12631263 export function getIdentities(): SavedIdentity[] {1264-  if (!derivedKey) throw new Error("vault locked");1264+  if (!derivedKey) throw new Error("vault_locked");12651265   const active = entries.filter((e) => !e.deletedAt && e.type === "login");12661266 12671267   const emailMap = new Map<string, { lastUsed: number; count: number; domain: string }>();@@ -1293,7 +1293,7 @@12931293 }12941294 12951295 export async function batchAddEntries(drafts: EntryDraft[]): Promise<VaultEntry[]> {1296-  if (!derivedKey) throw new Error("vault locked");1296+  if (!derivedKey) throw new Error("vault_locked");12971297 12981298   const now = Date.now();12991299   const added: VaultEntry[] = [];@@ -1331,7 +1331,7 @@13311331   incoming: VaultEntry[],13321332   mode: "replace" | "merge"13331333 ): Promise<{ added: number; updated: number; skipped: number }> {1334-  if (!derivedKey) throw new Error("vault locked");1334+  if (!derivedKey) throw new Error("vault_locked");13351335 13361336   // A replace with zero incoming entries is almost certainly a caller bug13371337   // (e.g. an import that silently dropped everything). Refuse rather than@@ -1440,7 +1440,7 @@14401440   if (confirmation !== "REPLACE_ALL") {14411441     throw new Error("clearVaultEntries requires explicit confirmation");14421442   }1443-  if (!derivedKey) throw new Error("vault locked");1443+  if (!derivedKey) throw new Error("vault_locked");14441444   for (const entry of entries) {14451445     if (!entry.deletedAt) {14461446       entry.deletedAt = Date.now();@@ -1451,7 +1451,7 @@14511451 }14521452 14531453 export async function addEntry(entry: EntryDraft): Promise<VaultEntry> {1454-  if (!derivedKey) throw new Error("vault locked");1454+  if (!derivedKey) throw new Error("vault_locked");14551455 14561456   const sanitized = sanitizeEntryDraft(entry);14571457   const now = Date.now();@@ -1488,11 +1488,11 @@14881488   id: string,14891489   patch: Partial<VaultEntry>14901490 ): Promise<void> {1491-  if (!derivedKey) throw new Error("vault locked");1491+  if (!derivedKey) throw new Error("vault_locked");14921492 14931493   const idx = entries.findIndex((entry) => entry.id === id);14941494   if (idx === -1 || entries[idx].deletedAt) {1495-    throw new Error("entry not found");1495+    throw new Error("entry_not_found");14961496   }14971497 14981498   const current = entries[idx];@@ -1551,11 +1551,11 @@15511551 }15521552 15531553 export async function deleteEntry(id: string): Promise<void> {1554-  if (!derivedKey) throw new Error("vault locked");1554+  if (!derivedKey) throw new Error("vault_locked");15551555 15561556   const entry = entries.find((item) => item.id === id);15571557   if (!entry || entry.deletedAt) {1558-    throw new Error("entry not found");1558+    throw new Error("entry_not_found");15591559   }15601560 15611561   entry.deletedAt = Date.now();@@ -1569,7 +1569,7 @@15691569 }15701570 15711571 export async function restoreEntry(id: string): Promise<void> {1572-  if (!derivedKey) throw new Error("vault locked");1572+  if (!derivedKey) throw new Error("vault_locked");15731573   const entry = entries.find((e) => e.id === id && e.deletedAt);15741574   if (!entry) throw new Error("deleted entry not found");15751575   delete entry.deletedAt;@@ -1579,7 +1579,7 @@15791579 }15801580 15811581 export async function purgeEntry(id: string): Promise<void> {1582-  if (!derivedKey) throw new Error("vault locked");1582+  if (!derivedKey) throw new Error("vault_locked");15831583   const idx = entries.findIndex((e) => e.id === id && e.deletedAt);15841584   if (idx === -1) throw new Error("deleted entry not found");15851585   entries.splice(idx, 1);@@ -1608,7 +1608,7 @@16081608 async function persistSlotA(16091609   reason: VaultSnapshot["reason"] = "local_save",16101610 ): Promise<StoredVault> {1611-  if (!derivedKey) throw new Error("vault locked");1611+  if (!derivedKey) throw new Error("vault_locked");16121612 16131613   const slotA = await loadSlot(SLOT_A);16141614   if (!slotA) throw new Error("missing slot A");@@ -1642,7 +1642,7 @@16421642 }16431643 16441644 async function persistSlotB(): Promise<void> {1645-  if (!derivedKey) throw new Error("vault locked");1645+  if (!derivedKey) throw new Error("vault_locked");16461646 16471647   const slotB = await loadSlot(SLOT_B);16481648   if (!slotB) throw new Error("missing slot B");@@ -1659,7 +1659,7 @@16591659 async function persistEntries(16601660   reason: VaultSnapshot["reason"] = "local_save",16611661 ): Promise<void> {1662-  if (!derivedKey) throw new Error("vault locked");1662+  if (!derivedKey) throw new Error("vault_locked");16631663 16641664   if (vaultMode === "decoy") {16651665     await persistSlotB();@@ -1971,7 +1971,7 @@19711971  * each group. Returns the number of entries moved to trash.19721972  */19731973 export async function deduplicateEntries(): Promise<number> {1974-  if (!derivedKey) throw new Error("vault locked");1974+  if (!derivedKey) throw new Error("vault_locked");19751975   // Dedup reshapes slot B permanently under the decoy key, which is19761976   // forensically distinguishable from a freshly-seeded decoy vault. Refuse.19771977   if (vaultMode === "decoy") {

view file

src/popup/App.tsx+1 −1
@@ -890,7 +890,7 @@890890     // showing an empty vault (which looks like data loss).891891     if (!res.ok) {892892       if (893-        res.error === "vault locked" ||893+        res.error === "vault_locked" ||894894         res.error === "SERVICE_WORKER_UNAVAILABLE" ||895895         (typeof res.error === "string" && res.error.includes("locked"))896896       ) {

view file

src/popup/views/Add.tsx+1 −1
@@ -256,7 +256,7 @@256256     const res = await sendMessage({ type: "ADD_ENTRY", payload });257257     const result = res as { ok: boolean; data?: VaultEntry; error?: string };258258     if (!result.ok) {259-      if (result.error === "vault locked") {259+      if (result.error === "vault_locked") {260260         setError("vault is locked — tap the lock icon to re-enter your master password");261261       } else {262262         setError(result.error ?? "could not save entry");

view file

src/popup/views/Import.tsx+1 −1
@@ -857,7 +857,7 @@857857       return "Choose a master password for your new vault first.";858858     case "RESTORE_FAILED":859859       return "Restore failed. Your existing vault is unchanged.";860-    case "vault locked":860+    case "vault_locked":861861       return "Peach is locked. Open Peach with your master password and try again.";862862     case "refuse to replace with empty entry set":863863       return "This PeachScript decoded to zero entries — refusing to wipe the vault.";

view file

src/popup/views/RecoveryWordEntry.tsx+1 −1
@@ -330,7 +330,7 @@330330 function getDisplayError(error: string | undefined, fallback: string): string {331331   if (!error) return fallback;332332   switch (error) {333-    case "no vault":333+    case "no_vault":334334       return "no vault found";335335     case "recovery_not_setup":336336       return "recovery not set up for this vault";

view file

src/shared/types.ts+39 −0
@@ -456,6 +456,45 @@456456   payload?: unknown;457457 }458458 459+// Canonical snake_case error codes returned by message handlers. Keep this460+// union the authoritative catalogue — new handlers must reuse an entry or461+// add one here. Free-form strings still appear (network/operation failures462+// surface their raw .message), so error stays string at runtime; the union463+// is the documented set callers should switch on.464+export type ErrorCode =465+  | "vault_locked"466+  | "vault_corrupted"467+  | "no_vault"468+  | "entry_not_found"469+  | "invalid_payload"470+  | "invalid_message"471+  | "invalid_sender"472+  | "wrong_passphrase"473+  | "incorrect_current_password"474+  | "recovery_not_setup"475+  | "android_only"476+  | "not_available"477+  | "pro_required"478+  | "pairing_not_verified"479+  | "sync_rejected_unpaired_initiator"480+  | "vault_not_applied"481+  | "url_required"482+  | "domain_required"483+  | "rpid_required"484+  | "rpid_and_username_required"485+  | "publickeyhex_required"486+  | "missing_payload"487+  | "login_entries_only"488+  | "passkey_not_fillable"489+  | "invalid_webhook_url"490+  | "invalid_feedback_endpoint"491+  | "message_empty"492+  | "network_error"493+  | "no_reset_url"494+  | "failed_to_open_tab"495+  | "operation_failed"496+  | "unknown_message_type";497+459498 export interface MessageResponse {460499   ok: boolean;461500   data?: unknown;

view file