b13822a0ea50
fix: Restore Codex engine browser load (v3.6.43)
- time
- 2026-05-19 20:22 UTC · 5 months ago
- machine
- koh2
- parent
- 3991c2a5cd74
↓ peach-browser-b13822a0.tar.gzatlas at this save
files changed10 files
manifest.firefox.json+1 −1manifest.json+1 −1package-lock.json+2 −2package.json+1 −1src/background/vault.ts+9 −2src/popup/codex/browser-bundle.test.ts+20 −0src/popup/codex/erasure.ts+16 −11src/popup/codex/node-wasm.ts+35 −0src/popup/codex/reed-solomon.ts+153 −0src/popup/codex/rs-loader.ts+10 −9manifest.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.41",5+ "version": "3.6.43",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.41",4+ "version": "3.6.43",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.41",3+ "version": "3.6.43",44 "lockfileVersion": 3,55 "requires": true,66 "packages": {77 "": {88 "name": "peach",9- "version": "3.6.41",9+ "version": "3.6.43",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.41",3+ "version": "3.6.43",44 "description": "Zero-knowledge password manager",55 "type": "module",66 "scripts": {src/background/vault.ts+9 −2
@@ -521,8 +521,16 @@521521 }522522 523523 async function stampLocalVaultMutation(): Promise<void> {524+ const raw = await chrome.storage.local.get("peach_last_modified_at");525+ const previous = Number(526+ (raw as Record<string, unknown>).peach_last_modified_at,527+ );528+ const next = Math.max(529+ Date.now(),530+ Number.isFinite(previous) ? previous + 1 : 0,531+ );524532 await chrome.storage.local.set({525- peach_last_modified_at: String(Date.now()),533+ peach_last_modified_at: String(next),526534 });527535 }528536 @@ -2282,4 +2290,3 @@22822290 22832291 return true;22842292 }2285-src/popup/codex/browser-bundle.test.ts+20 −0
@@ -0,0 +1,20 @@1+import { readFileSync } from "node:fs";2+import { describe, expect, it } from "vitest";3+4+describe("Codex browser bundle hygiene", () => {5+ it("keeps direct Node built-in imports out of browser-loaded Codex modules", () => {6+ const browserLoadedFiles = [7+ "src/popup/codex/rs-loader.ts",8+ "src/popup/codex/erasure.ts",9+ ];10+11+ for (const file of browserLoadedFiles) {12+ const source = readFileSync(file, "utf8");13+ expect(source, file).not.toContain(14+ "@subspace/reed-solomon-erasure.wasm",15+ );16+ expect(source, file).not.toMatch(/import\s*\(\s*["']node:/);17+ expect(source, file).not.toMatch(/from\s+["']node:/);18+ }19+ });20+});src/popup/codex/erasure.ts+16 −11
@@ -13,13 +13,13 @@1313 * not re-entrant on a shared buffer.1414 */1515 16-import { ReedSolomonErasure } from "@subspace/reed-solomon-erasure.wasm";1716 import {1817 CHUNK_SIZE,1918 CodexError,2019 MAX_DATA_SHARDS_PER_STRIPE,2120 computeParityShards,2221 } from "../../shared/codex-types";22+import { ReedSolomonErasure } from "./reed-solomon";2323 import { getReedSolomon } from "./rs-loader";2424 2525 /**@@ -72,19 +72,24 @@7272 }7373 return new Uint8Array(await response.arrayBuffer());7474 }75- const fs = await import("node:fs");76- const path = await import("node:path");77- const wasmPath = path.resolve(78- "node_modules/@subspace/reed-solomon-erasure.wasm/dist/reed_solomon_erasure_bg.wasm",79- );80- return new Uint8Array(fs.readFileSync(wasmPath));75+ return readTestWasmBytes();8176 }8277 78+async function readTestWasmBytes(): Promise<Uint8Array<ArrayBuffer>> {79+ if (import.meta.env.MODE !== "test") {80+ throw new CodexError(81+ "ERASURE_CODING_VERIFICATION_FAILED",82+ "Reed-Solomon WASM can only be loaded through chrome.runtime.getURL outside tests",83+ );84+ }85+ const { readNodeReedSolomonWasmBytes } = await import("./node-wasm");86+ return readNodeReedSolomonWasmBytes();87+}88+8389 /**84- * @subspace/reed-solomon-erasure.wasm exposes the RESULT_* constants but not a85- * `getResultMessage(code)` helper (the @digitaldefiance fork added it). We map86- * codes to human-readable strings here so the surrounding CodexError messages87- * stay specific instead of leaking a bare number.90+ * We map Reed-Solomon result codes to human-readable strings here so the91+ * surrounding CodexError messages stay specific instead of leaking a bare92+ * number.8893 */8994 function resultMessage(code: number): string {9095 switch (code) {src/popup/codex/node-wasm.ts+35 −0
@@ -0,0 +1,35 @@1+/**2+ * Node-only WASM byte loader for Vitest.3+ *4+ * Browser extension builds must not see direct Node built-in imports here;5+ * Vite/Rolldown rewrites those to browser external shims that fail in6+ * extension pages. The split specifier plus @vite-ignore keeps this fallback7+ * out of the browser module graph while still letting Node tests read from8+ * node_modules.9+ */10+11+const WASM_NODE_MODULE_PATH =12+ "node_modules/@subspace/reed-solomon-erasure.wasm/dist/reed_solomon_erasure_bg.wasm";13+14+type NodeFs = {15+ readFileSync(path: string): Uint8Array;16+};17+18+type NodePath = {19+ resolve(...parts: string[]): string;20+};21+22+async function importNodeModule<T>(name: string): Promise<T> {23+ return import(/* @vite-ignore */ (`node:${name}`)) as Promise<T>;24+}25+26+export async function readNodeReedSolomonWasmBytes(): Promise<Uint8Array<ArrayBuffer>> {27+ const [fs, path] = await Promise.all([28+ importNodeModule<NodeFs>("fs"),29+ importNodeModule<NodePath>("path"),30+ ]);31+ const fileBytes = fs.readFileSync(path.resolve(WASM_NODE_MODULE_PATH));32+ const bytes: Uint8Array<ArrayBuffer> = new Uint8Array(fileBytes.byteLength);33+ bytes.set(fileBytes);34+ return bytes;35+}src/popup/codex/reed-solomon.ts+153 −0
@@ -0,0 +1,153 @@1+/**2+ * Browser-safe wrapper around the Reed-Solomon WASM exports.3+ *4+ * The published JS wrapper for @subspace/reed-solomon-erasure.wasm imports5+ * Node's `fs` module at top level, which breaks extension pages once bundled.6+ * This file talks directly to the WASM exports we use and contains no Node7+ * built-ins, so it is safe for popup chunks.8+ */9+10+interface ReedSolomonWasmExports extends WebAssembly.Exports {11+ memory: WebAssembly.Memory;12+ __wbindgen_malloc(size: number): number;13+ __wbindgen_free(pointer: number, size: number): void;14+ encode(15+ shardsPointer: number,16+ shardsLength: number,17+ dataShards: number,18+ parityShards: number,19+ ): number;20+ reconstruct(21+ shardsPointer: number,22+ shardsLength: number,23+ dataShards: number,24+ parityShards: number,25+ shardsAvailablePointer: number,26+ shardsAvailableLength: number,27+ ): number;28+}29+30+function isReedSolomonExports(31+ exports: WebAssembly.Exports,32+): exports is ReedSolomonWasmExports {33+ return (34+ exports.memory instanceof WebAssembly.Memory &&35+ typeof exports.__wbindgen_malloc === "function" &&36+ typeof exports.__wbindgen_free === "function" &&37+ typeof exports.encode === "function" &&38+ typeof exports.reconstruct === "function"39+ );40+}41+42+export class ReedSolomonErasure {43+ static readonly RESULT_OK = 0;44+ static readonly RESULT_ERROR_TOO_FEW_SHARDS = 1;45+ static readonly RESULT_ERROR_TOO_MANY_SHARDS = 2;46+ static readonly RESULT_ERROR_TOO_FEW_DATA_SHARDS = 3;47+ static readonly RESULT_ERROR_TOO_MANY_DATA_SHARDS = 4;48+ static readonly RESULT_ERROR_TOO_FEW_PARITY_SHARDS = 5;49+ static readonly RESULT_ERROR_TOO_MANY_PARITY_SHARDS = 6;50+ static readonly RESULT_ERROR_TOO_FEW_BUFFER_SHARDS = 7;51+ static readonly RESULT_ERROR_TOO_MANY_BUFFER_SHARDS = 8;52+ static readonly RESULT_ERROR_INCORRECT_SHARD_SIZE = 9;53+ static readonly RESULT_ERROR_TOO_FEW_SHARDS_PRESENT = 10;54+ static readonly RESULT_ERROR_EMPTY_SHARD = 11;55+ static readonly RESULT_ERROR_INVALID_SHARD_FLAGS = 12;56+ static readonly RESULT_ERROR_INVALID_INDEX = 13;57+58+ private memoryCache: Uint8Array | null = null;59+60+ private constructor(private readonly exports: ReedSolomonWasmExports) {}61+62+ static fromBytes(bytes: BufferSource): ReedSolomonErasure {63+ const module = new WebAssembly.Module(bytes);64+ const instance = new WebAssembly.Instance(module);65+ if (!isReedSolomonExports(instance.exports)) {66+ throw new Error("Reed-Solomon WASM exports are missing required functions");67+ }68+ return new ReedSolomonErasure(instance.exports);69+ }70+71+ encode(72+ shards: Uint8Array,73+ dataShards: number,74+ parityShards: number,75+ ): number {76+ const exports = this.exports;77+ const shardsLength = shards.length;78+ const shardsPointer = exports.__wbindgen_malloc(shardsLength);79+ this.getUint8Memory().set(shards, shardsPointer);80+81+ const shardSize = shardsLength / (dataShards + parityShards);82+ const result = exports.encode(83+ shardsPointer,84+ shardsLength,85+ dataShards,86+ parityShards,87+ );88+ if (result === ReedSolomonErasure.RESULT_OK) {89+ shards.set(90+ this.getUint8Memory().subarray(91+ shardsPointer + shardSize * dataShards,92+ shardsPointer + shardsLength,93+ ),94+ shardSize * dataShards,95+ );96+ }97+ exports.__wbindgen_free(shardsPointer, shardsLength);98+ return result;99+ }100+101+ reconstruct(102+ shards: Uint8Array,103+ dataShards: number,104+ parityShards: number,105+ shardsAvailable: boolean[],106+ ): number {107+ const exports = this.exports;108+ const shardsLength = shards.length;109+ const shardsPointer = exports.__wbindgen_malloc(shardsLength);110+ this.getUint8Memory().set(shards, shardsPointer);111+112+ const shardsAvailableBytes = new Uint8Array(shardsAvailable.length);113+ shardsAvailableBytes.set(shardsAvailable.map((value) => (value ? 1 : 0)));114+ const shardsAvailablePointer = exports.__wbindgen_malloc(115+ shardsAvailableBytes.length,116+ );117+ this.getUint8Memory().set(shardsAvailableBytes, shardsAvailablePointer);118+119+ const shardSize = shardsLength / (dataShards + parityShards);120+ const result = exports.reconstruct(121+ shardsPointer,122+ shardsLength,123+ dataShards,124+ parityShards,125+ shardsAvailablePointer,126+ shardsAvailableBytes.length,127+ );128+ if (result === ReedSolomonErasure.RESULT_OK) {129+ shards.set(130+ this.getUint8Memory().subarray(131+ shardsPointer,132+ shardsPointer + shardSize * dataShards,133+ ),134+ );135+ }136+ exports.__wbindgen_free(shardsPointer, shardsLength);137+ exports.__wbindgen_free(138+ shardsAvailablePointer,139+ shardsAvailableBytes.length,140+ );141+ return result;142+ }143+144+ private getUint8Memory(): Uint8Array {145+ if (146+ this.memoryCache === null ||147+ this.memoryCache.buffer !== this.exports.memory.buffer148+ ) {149+ this.memoryCache = new Uint8Array(this.exports.memory.buffer);150+ }151+ return this.memoryCache;152+ }153+}src/popup/codex/rs-loader.ts+10 −9
@@ -14,8 +14,8 @@1414 * library is safe to reuse across encode / reconstruct calls.1515 */1616 17-import { ReedSolomonErasure } from "@subspace/reed-solomon-erasure.wasm";1817 import { CodexError } from "../../shared/codex-types";18+import { ReedSolomonErasure } from "./reed-solomon";1919 2020 const WASM_RELATIVE_PATH = "codex/reed_solomon_erasure_bg.wasm";2121 @@ -38,14 +38,7 @@3838 return ReedSolomonErasure.fromBytes(bytes);3939 }4040 41- // Node / vitest path. Dynamic import so the bundler does not try to42- // resolve `node:fs` for the browser build.43- const fs = await import("node:fs");44- const path = await import("node:path");45- const wasmPath = path.resolve(46- "node_modules/@subspace/reed-solomon-erasure.wasm/dist/reed_solomon_erasure_bg.wasm",47- );48- const bytes = fs.readFileSync(wasmPath);41+ const bytes = await readTestWasmBytes();4942 return ReedSolomonErasure.fromBytes(bytes);5043 } catch (err) {5144 throw new CodexError(@@ -53,6 +46,14 @@5346 `Failed to load Reed-Solomon WASM module: ${err instanceof Error ? err.message : String(err)}`,5447 );5548 }49+}50+51+async function readTestWasmBytes(): Promise<Uint8Array<ArrayBuffer>> {52+ if (import.meta.env.MODE !== "test") {53+ throw new Error("Reed-Solomon WASM can only be loaded through chrome.runtime.getURL outside tests");54+ }55+ const { readNodeReedSolomonWasmBytes } = await import("./node-wasm");56+ return readNodeReedSolomonWasmBytes();5657 }5758 5859 function isExtensionPopup(): boolean {