a86783166653

fix email field dropped in sanitizeEntryDraft; identity suggestions include entry.email; aztec retry on content mismatch with diagnostic logging; live Aztec gallery on generating screen (v3.6.56)

time
2026-05-20 02:55 UTC · 5 months ago
machine
koh2
parent
3d77e1828fa3
8 files manifest.firefox.json+1 −1manifest.json+1 −1src/background/vault.ts+17 −8src/popup/codex/aztec.ts+26 −9src/popup/codex/generate.ts+10 −1src/popup/styles/codex.css+23 −0src/popup/views/CodexGenerateView.tsx+35 −2src/shared/codex-types.ts+3 −0
manifest.firefox.json+1 −1
@@ -2,7 +2,7 @@22   "_comment_audio_capture": "Firefox does not honor the Chrome 'audioCapture' optional_permission — getUserMedia in WebExtensions is gated solely by the runtime permission prompt. Microphone access for chirp pairing works from the popup under the existing '<all_urls>' host permission; no manifest entry is required or accepted here.",33   "manifest_version": 3,44   "name": "Peach",5-  "version": "3.6.55",5+  "version": "3.6.56",66   "description": "Zero-knowledge password manager",77   "browser_specific_settings": {88     "gecko": {

view file

manifest.json+1 −1
@@ -1,7 +1,7 @@11 {22   "manifest_version": 3,33   "name": "Peach",4-  "version": "3.6.55",4+  "version": "3.6.56",55   "description": "Zero-knowledge password manager",66   "action": {77     "default_popup": "src/popup/index.html",

view file

src/background/vault.ts+17 −8
@@ -1336,17 +1336,25 @@13361336   const usernameMap = new Map<string, { lastUsed: number; count: number; domain: string }>();13371337 13381338   for (const entry of active) {1339-    const value = entry.username?.trim();1340-    if (!value) continue;1341-    const map = EMAIL_RE.test(value) ? emailMap : usernameMap;1342-    const existing = map.get(value);13431339     const domain = extractDomain(entry.url ?? "") ?? "";13441340     const modifiedAt = entry.modifiedAt ?? entry.createdAt ?? 0;1345-    if (!existing || modifiedAt > existing.lastUsed) {1346-      map.set(value, { lastUsed: modifiedAt, count: (existing?.count ?? 0) + 1, domain });1347-    } else {1348-      map.set(value, { ...existing, count: existing.count + 1 });1341+1342+    function tally(map: Map<string, { lastUsed: number; count: number; domain: string }>, v: string) {1343+      const existing = map.get(v);1344+      if (!existing || modifiedAt > existing.lastUsed) {1345+        map.set(v, { lastUsed: modifiedAt, count: (existing?.count ?? 0) + 1, domain });1346+      } else {1347+        map.set(v, { ...existing, count: existing.count + 1 });1348+      }13491349     }1350+1351+    const uVal = entry.username?.trim();1352+    if (uVal) tally(EMAIL_RE.test(uVal) ? emailMap : usernameMap, uVal);1353+1354+    // Also count the dedicated email field — entries created via the inline1355+    // popup new-entry form store email here rather than in username.1356+    const eVal = entry.email?.trim();1357+    if (eVal) tally(emailMap, eVal);13501358   }13511359 13521360   const toIdentity = (type: "email" | "username") =>@@ -1798,6 +1806,7 @@17981806     type,17991807     title,18001808     username: type === "login" ? entry.username?.trim() ?? "" : undefined,1809+    email: type === "login" ? entry.email?.trim() || undefined : undefined,18011810     password: type === "login" ? entry.password : undefined,18021811     url: type === "login" ? entry.url?.trim() ?? "" : undefined,18031812     additionalUrls: type === "login" ? cleanAdditionalUrls(entry.additionalUrls) : undefined,

view file

src/popup/codex/aztec.ts+26 −9
@@ -121,6 +121,11 @@121121     // image edge with no quiet zone, causing spurious AZTEC_ROUNDTRIP_FAILED.122122     padding: 2,123123     monochrome: true,124+    // Explicit white background so the PNG is fully opaque. Without this,125+    // bwip-js clears the canvas with clearRect (transparent), and although126+    // decodeAztecFromPng fills white before drawing, the alpha compositing127+    // path is one more source of pixel-level variance on some runtimes.128+    backgroundcolor: "ffffff",124129   } as const;125130 126131   try {@@ -254,24 +259,36 @@254259     return image;255260   }256261 257-  // @zxing occasionally returns null on a valid barcode due to transient258-  // rasterisation issues. Five attempts avoids aborting a full Codex build259-  // for a momentary DOM hiccup. Content-mismatch is never retried.262+  // @zxing occasionally returns null or wrong content on a valid barcode due263+  // to transient rasterisation variance. Retry up to 5 times on any failure —264+  // break immediately on a confirmed correct decode. Track the last non-null265+  // wrong result so we can surface diagnostic detail if all attempts fail.260266   let decoded: string | null = null;267+  let lastMismatch: string | null = null;261268   for (let attempt = 0; attempt < 5; attempt++) {262-    decoded = await decodeAztecFromPng(image.pngBytes);263-    if (decoded !== null) break;269+    const candidate = await decodeAztecFromPng(image.pngBytes);270+    if (candidate === envelopeJson) { decoded = candidate; break; }271+    if (candidate !== null) lastMismatch = candidate;264272   }265-  if (decoded === null) {273+  if (decoded === null && lastMismatch === null) {266274     throw new CodexError(267275       "AZTEC_ROUNDTRIP_FAILED",268-      `Chunk ${envelope.i} generated but cannot be decoded back after 3 attempts. This Codex cannot be trusted.`,276+      `Chunk ${envelope.i} generated but cannot be decoded back after 5 attempts. This Codex cannot be trusted.`,269277     );270278   }271-  if (decoded !== envelopeJson) {279+  if (decoded === null) {280+    // lastMismatch is non-null — decoded something but wrong content.281+    const el = envelopeJson.length;282+    const dl = lastMismatch!.length;283+    let diffAt = -1;284+    for (let k = 0; k < Math.min(el, dl); k++) {285+      if (envelopeJson[k] !== lastMismatch![k]) { diffAt = k; break; }286+    }287+    const ctx = (s: string) => JSON.stringify(s.slice(Math.max(0, diffAt - 8), diffAt + 20));272288     throw new CodexError(273289       "AZTEC_CONTENT_MISMATCH",274-      `Chunk ${envelope.i} decoded content does not match original envelope. Possible encoding corruption.`,290+      `Chunk ${envelope.i}: expected len=${el} decoded len=${dl} first diff at pos ${diffAt}. ` +291+        `expected: ${ctx(envelopeJson)} decoded: ${ctx(lastMismatch!)}`,275292     );276293   }277294   return image;

view file

src/popup/codex/generate.ts+10 −1
@@ -134,8 +134,17 @@134134     onProgress?.("aztec", i, envelopes.length);135135     const img = await generateVerifiedAztec(envelopes[i]!);136136     aztecPngs.push(img.pngBytes);137+    // Pass a blob URL so the view can render a live gallery of real codes.138+    // Only created when OffscreenCanvas is available (same guard as round-trip).139+    // Copy into a fresh Uint8Array to satisfy Blob's ArrayBuffer type constraint140+    // (img.pngBytes may be backed by a SharedArrayBuffer in some type environments).141+    let blobUrl: string | undefined;142+    if (typeof URL !== "undefined" && typeof Blob !== "undefined") {143+      const owned = new Uint8Array(img.pngBytes);144+      blobUrl = URL.createObjectURL(new Blob([owned], { type: "image/png" }));145+    }146+    onProgress?.("aztec", i + 1, envelopes.length, blobUrl);137147   }138-  onProgress?.("aztec", envelopes.length, envelopes.length);139148 140149   // STEP 7 — full reconstruction simulation (lose the maximum tolerated141150   // number of envelopes per stripe, then rebuild end-to-end)

view file

src/popup/styles/codex.css+23 −0
@@ -432,3 +432,26 @@432432   padding: 16px 0;433433   align-items: stretch;434434 }435+436+/* ── Live Aztec gallery (generating step) ─────────────────────────────────── */437+438+.peach-codex-aztec-gallery {439+  display: grid;440+  grid-template-columns: repeat(6, 1fr);441+  gap: 4px;442+  padding: 12px 12px 0;443+}444+445+.peach-codex-aztec-tile {446+  width: 100%;447+  aspect-ratio: 1;448+  display: block;449+  border-radius: 2px;450+  image-rendering: pixelated;451+  animation: codex-tile-in 280ms cubic-bezier(0.16, 1, 0.3, 1) both;452+}453+454+@keyframes codex-tile-in {455+  from { opacity: 0; transform: scale(0.82); }456+  to   { opacity: 1; transform: scale(1); }457+}

view file

src/popup/views/CodexGenerateView.tsx+35 −2
@@ -14,7 +14,7 @@1414 // Restoration (CodexRestoreView) stays free — a printed Codex must restore1515 // regardless of license state.1616 17-import { createSignal, createMemo, For, onMount, Show } from "solid-js";17+import { createSignal, createMemo, For, onMount, onCleanup, Show } from "solid-js";1818 import { sendMessage } from "../../shared/bridge";1919 import { isNativePlatform } from "../../shared/native";2020 import { RecoveryRuneBackground } from "../components/RecoveryRuneBackground";@@ -73,7 +73,15 @@7373 7474   const [genError, setGenError] = createSignal<string | null>(null);7575   const [completed, setCompleted] = createSignal<CodexManifest | null>(null);76+  // Live gallery of real Aztec PNGs as they're generated — keep the last 3677+  // (one page worth) so the display fills without using excessive memory.78+  const [aztecGallery, setAztecGallery] = createSignal<string[]>([]);79+  const GALLERY_MAX = 36;7680 81+  onCleanup(() => {82+    for (const url of aztecGallery()) URL.revokeObjectURL(url);83+  });84+7785   onMount(async () => {7886     if (!props.isPro) {7987       props.onUpgradePro();@@ -244,10 +252,19 @@244252     }245253   }246254 247-  const onProgress: CodexProgress = (s, current, total) => {255+  const onProgress: CodexProgress = (s, current, total, imageUrl) => {248256     setProgressStep(s);249257     setProgressCurrent(current ?? 0);250258     setProgressTotal(total ?? 0);259+    if (s === "aztec" && imageUrl) {260+      setAztecGallery(prev => {261+        if (prev.length >= GALLERY_MAX) {262+          URL.revokeObjectURL(prev[0]!);263+          return [...prev.slice(1), imageUrl];264+        }265+        return [...prev, imageUrl];266+      });267+    }251268   };252269 253270   async function handleGenerate() {@@ -255,6 +272,7 @@255272     if (!snap || !allCorrect() || step() === "generating") return;256273 257274     setGenError(null);275+    setAztecGallery([]);258276     setStep("generating");259277     setProgressStep("compress");260278 @@ -501,6 +519,21 @@501519                 />502520               </div>503521             </div>522+523+            <Show when={aztecGallery().length > 0}>524+              <div class="peach-codex-aztec-gallery">525+                <For each={aztecGallery()}>526+                  {(url) => (527+                    <img528+                      class="peach-codex-aztec-tile"529+                      src={url}530+                      alt=""531+                      aria-hidden="true"532+                    />533+                  )}534+                </For>535+              </div>536+            </Show>504537           </Show>505538 506539           <Show when={step() === "done"}>

view file

src/shared/codex-types.ts+3 −0
@@ -106,11 +106,14 @@106106 /**107107  * Progress reporter for the generator. Called at named milestones; for the108108  * Aztec step `current`/`total` advance per code so the UI can render a bar.109+ * `imageUrl` is a blob URL for the just-generated Aztec PNG — only present110+ * during the "aztec" step so the UI can show a live gallery of real codes.109111  */110112 export type CodexProgress = (111113   step: "compress" | "erasure" | "aztec" | "verify" | "pdf",112114   current?: number,113115   total?: number,116+  imageUrl?: string,114117 ) => void;115118 116119 /** Payload sent from the popup to the SW to perform the actual vault restoration. */

view file