3991c2a5cd74
fix: Split Codex screen load from generator bundle (v3.6.41)
- time
- 2026-05-19 20:08 UTC · 5 months ago
- machine
- koh2
- parent
- c7effb2bc4b1
↓ peach-browser-3991c2a5.tar.gzatlas at this save
files changed8 files
manifest.firefox.json+1 −1manifest.json+1 −1package-lock.json+2 −2package.json+1 −1src/popup/codex/confirmation.ts+11 −0src/popup/codex/generate.ts+1 −12src/popup/views/CodexGenerateView.test.tsx+82 −0src/popup/views/CodexGenerateView.tsx+74 −37manifest.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.40",5+ "version": "3.6.41",66 "description": "Zero-knowledge password manager",77 "browser_specific_settings": {88 "gecko": {manifest.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach",4- "version": "3.6.40",4+ "version": "3.6.41",55 "description": "Zero-knowledge password manager",66 "action": {77 "default_popup": "src/popup/index.html",package-lock.json+2 −2
@@ -1,12 +1,12 @@11 {22 "name": "peach",3- "version": "3.6.37",3+ "version": "3.6.41",44 "lockfileVersion": 3,55 "requires": true,66 "packages": {77 "": {88 "name": "peach",9- "version": "3.6.37",9+ "version": "3.6.41",1010 "dependencies": {1111 "@bokuweb/zstd-wasm": "^0.0.27",1212 "@stablelib/blake2s": "^2.0.1",package.json+1 −1
@@ -1,6 +1,6 @@11 {22 "name": "peach",3- "version": "3.6.40",3+ "version": "3.6.41",44 "description": "Zero-knowledge password manager",55 "type": "module",66 "scripts": {src/popup/codex/confirmation.ts+11 −0
@@ -0,0 +1,11 @@1+/** Pick 3 distinct positions in [1, 12] for the recovery-words confirmation2+ * gate. Sorted ascending so they read naturally in the prompt. */3+export function pickConfirmationPositions(): [number, number, number] {4+ const positions = Array.from({ length: 12 }, (_, i) => i + 1);5+ for (let i = positions.length - 1; i > 0; i--) {6+ const j = Math.floor(Math.random() * (i + 1));7+ [positions[i], positions[j]] = [positions[j]!, positions[i]!];8+ }9+ const picked = [positions[0]!, positions[1]!, positions[2]!] as const;10+ return [...picked].sort((a, b) => a - b) as [number, number, number];11+}src/popup/codex/generate.ts+1 −12
@@ -39,6 +39,7 @@3939 sha256Hex,4040 } from "./helpers";4141 import { renderCodexPDF } from "./pdf";42+export { pickConfirmationPositions } from "./confirmation";4243 4344 /**4445 * Per-stripe scanned-chunk bundle. Restore code groups envelopes by stripe@@ -269,18 +270,6 @@269270 }270271 const pages = Math.ceil(totalChunks / CODES_PER_PAGE);271272 return { pages, parityShards: totalParity };272-}273-274-/** Pick 3 distinct positions in [1, 12] for the recovery-words confirmation275- * gate. Sorted ascending so they read naturally in the prompt. */276-export function pickConfirmationPositions(): [number, number, number] {277- const positions = Array.from({ length: 12 }, (_, i) => i + 1);278- for (let i = positions.length - 1; i > 0; i--) {279- const j = Math.floor(Math.random() * (i + 1));280- [positions[i], positions[j]] = [positions[j]!, positions[i]!];281- }282- const picked = [positions[0]!, positions[1]!, positions[2]!] as const;283- return [...picked].sort((a, b) => a - b) as [number, number, number];284273 }285274 286275 // ── Internal helpers ────────────────────────────────────────────────────────src/popup/views/CodexGenerateView.test.tsx+82 −0
@@ -0,0 +1,82 @@1+// @vitest-environment jsdom2+3+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";4+5+import { flushView, renderView } from "./view-test-utils";6+7+const sendMessageMock = vi.fn<(msg: unknown) => Promise<unknown>>();8+9+vi.mock("../../shared/bridge", () => ({10+ sendMessage: (msg: unknown) => sendMessageMock(msg),11+}));12+13+vi.mock("../components/RecoveryRuneBackground", () => ({14+ RecoveryRuneBackground: () => null,15+}));16+17+vi.mock("../codex/generate", () => {18+ throw new Error("blocked heavy Codex module");19+});20+21+vi.mock("../codex/rs-loader", () => ({22+ getReedSolomon: vi.fn(async () => ({})),23+}));24+25+describe("CodexGenerateView", () => {26+ let dispose: (() => void) | undefined;27+28+ beforeEach(() => {29+ document.body.innerHTML = "";30+ sendMessageMock.mockReset();31+ sendMessageMock.mockResolvedValue({32+ ok: true,33+ data: {34+ slotA: JSON.stringify({ slot: "a" }),35+ slotB: null,36+ lastModified: null,37+ entryCount: 3,38+ recoveryWords: [39+ "abandon", "ability", "able", "about", "above", "absent",40+ "absorb", "abstract", "absurd", "abuse", "access", "accident",41+ ],42+ verifyCode: "PX-TEST-CODE",43+ vaultUnlocked: true,44+ },45+ });46+47+ vi.stubGlobal("chrome", {48+ storage: {49+ local: {50+ set: vi.fn(),51+ },52+ },53+ } as unknown as typeof chrome);54+ });55+56+ afterEach(() => {57+ dispose?.();58+ dispose = undefined;59+ vi.unstubAllGlobals();60+ });61+62+ it("shows the recovery-word gate even if the heavy Codex module fails to load", async () => {63+ const { CodexGenerateView } = await import("./CodexGenerateView");64+ const mounted = renderView(65+ <CodexGenerateView66+ isPro={true}67+ onBack={vi.fn()}68+ onUpgradePro={vi.fn()}69+ onOpenSecurity={vi.fn()}70+ onVaultLocked={vi.fn()}71+ />,72+ );73+ dispose = mounted.dispose;74+75+ await flushView();76+ await flushView();77+78+ expect(mounted.container.textContent).toContain("your recovery words.");79+ expect(mounted.container.textContent).toContain("abandon");80+ expect(mounted.container.textContent).toContain("Failed to load Codex library");81+ });82+});src/popup/views/CodexGenerateView.tsx+74 −37
@@ -26,12 +26,7 @@2626 CodexVaultSnapshot,2727 } from "../../shared/codex-types";2828 import { STORAGE_KEY_LAST_GENERATED } from "../../shared/codex-types";29-import {30- estimateCodexPages,31- generateCodex,32- pickConfirmationPositions,33-} from "../codex/generate";34-import { getReedSolomon } from "../codex/rs-loader";29+import { pickConfirmationPositions } from "../codex/confirmation";3530 import type { VaultEntry } from "../../shared/types";3631 3732 interface CodexGenerateViewProps {@@ -43,14 +38,25 @@4338 }4439 4540 type Step = "words" | "generating" | "done";41+type CodexGenerateModule = typeof import("../codex/generate");4642 43+let codexGenerateModulePromise: Promise<CodexGenerateModule> | null = null;4744 45+function loadCodexGenerateModule(): Promise<CodexGenerateModule> {46+ codexGenerateModulePromise ??= import("../codex/generate");47+ return codexGenerateModulePromise;48+}49+50+4851 export function CodexGenerateView(props: CodexGenerateViewProps) {4952 const [snapshot, setSnapshot] = createSignal<CodexVaultSnapshot | null>(null);5053 const [estimatedPages, setEstimatedPages] = createSignal<number | null>(null);5154 const [estimatedParity, setEstimatedParity] = createSignal<number | null>(null);5255 const [loading, setLoading] = createSignal(true);5356 const [loadError, setLoadError] = createSignal<string | null>(null);57+ const [libraryLoading, setLibraryLoading] = createSignal(false);58+ const [libraryReady, setLibraryReady] = createSignal(false);59+ const [libraryError, setLibraryError] = createSignal<string | null>(null);5460 const [missingRecoveryWords, setMissingRecoveryWords] = createSignal(false);5561 5662 const [step, setStep] = createSignal<Step>("words");@@ -100,35 +106,8 @@100106 }101107 102108 setConfirmPositions(pickConfirmationPositions());103-104- // Pre-warm the Reed-Solomon WASM so load failures surface before the105- // user completes the word-confirmation flow.106- try {107- await getReedSolomon();108- } catch (err) {109- setLoadError(110- `Failed to load Codex library: ${err instanceof Error ? err.message : String(err)}`111- );112- setLoading(false);113- return;114- }115-116- if (res.data.slotA) {117- try {118- const { pages, parityShards } = await estimateCodexPages(119- res.data.slotA,120- res.data.slotB,121- );122- setEstimatedPages(pages);123- setEstimatedParity(parityShards);124- } catch {125- // Estimation is informational; failure is non-fatal.126- setEstimatedPages(null);127- setEstimatedParity(null);128- }129- }130-131109 setLoading(false);110+ void warmCodexLibrary(res.data);132111 });133112 134113 const allCorrect = createMemo(() => {@@ -161,6 +140,48 @@161140 };162141 }163142 143+ async function warmCodexLibrary(144+ snap?: CodexVaultSnapshot,145+ ): Promise<CodexGenerateModule | null> {146+ if (libraryReady()) return loadCodexGenerateModule();147+148+ setLibraryLoading(true);149+ setLibraryError(null);150+ try {151+ const [generateModule, rsModule] = await Promise.all([152+ loadCodexGenerateModule(),153+ import("../codex/rs-loader"),154+ ]);155+ await rsModule.getReedSolomon();156+ setLibraryReady(true);157+158+ if (snap?.slotA) {159+ try {160+ const { pages, parityShards } = await generateModule.estimateCodexPages(161+ snap.slotA,162+ snap.slotB,163+ );164+ setEstimatedPages(pages);165+ setEstimatedParity(parityShards);166+ } catch {167+ // Estimation is informational; failure is non-fatal.168+ setEstimatedPages(null);169+ setEstimatedParity(null);170+ }171+ }172+173+ return generateModule;174+ } catch (err) {175+ codexGenerateModulePromise = null;176+ setLibraryError(177+ `Failed to load Codex library: ${err instanceof Error ? err.message : String(err)}`,178+ );179+ return null;180+ } finally {181+ setLibraryLoading(false);182+ }183+ }184+164185 function codexFilename(): string {165186 const d = new Date();166187 const y = d.getFullYear();@@ -238,7 +259,12 @@238259 setProgressStep("compress");239260 240261 try {241- const result = await generateCodex(buildDeps(snap), onProgress);262+ const generateModule = await warmCodexLibrary(snap);263+ if (!generateModule) {264+ setStep("words");265+ return;266+ }267+ const result = await generateModule.generateCodex(buildDeps(snap), onProgress);242268 downloadPdf(result.pdfBytes);243269 setCompleted(result.manifest);244270 setStep("done");@@ -434,13 +460,23 @@434460 {genError()}435461 </p>436462 </Show>463+ <Show when={libraryError()}>464+ <p class="peach-hint peach-hint-error peach-codex-error">465+ {libraryError()}466+ </p>467+ </Show>468+ <Show when={libraryLoading() && !libraryReady()}>469+ <p class="peach-hint peach-codex-meta">470+ preparing Codex library…471+ </p>472+ </Show>437473 438474 <button439475 class="peach-btn peach-btn--accent peach-btn--md peach-btn--full peach-codex-generate-btn"440- disabled={!allCorrect()}476+ disabled={!allCorrect() || libraryLoading()}441477 onClick={() => void handleGenerate()}442478 >443- generate my Codex479+ {libraryLoading() && !libraryReady() ? "preparing…" : "generate my Codex"}444480 </button>445481 </Show>446482 @@ -546,6 +582,7 @@546582 onClick={() => {547583 setCompleted(null);548584 setGenError(null);585+ setLibraryError(null);549586 setConfirmInputs(["", "", ""]);550587 setConfirmPositions(pickConfirmationPositions());551588 setStep("words");