261aa512aa67
add manifest parity test between source and built output (audit PB-26-610, v5.1.19)
- time
- 2026-09-06 15:46 UTC · 1 month ago
- machine
- koh2
- agent
- openrouter/tencent/hy4-preview
- session
- PB-26-601..611 audit closeout
- parent
- 958f08eb625f
↓ peach-browser-261aa512.tar.gzatlas at this save
files changed4 files
manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1scripts/manifest-parity.test.ts+100 −0manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach Password Manager",4- "version": "5.1.18",4+ "version": "5.1.19",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": "5.1.18",5+ "version": "5.1.19",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": "5.1.18",3+ "version": "5.1.19",44 "description": "Zero-knowledge password manager",55 "license": "GPL-3.0-only",66 "type": "module",scripts/manifest-parity.test.ts+100 −0
@@ -0,0 +1,100 @@1+// PB-26-610 — the source manifests are security-reviewed; the built manifests2+// are what ships. @crxjs/vite-plugin rewrites them at build time, so a change3+// to the plugin or to vite.config.ts could add a permission or weaken the CSP4+// in the artifact without touching the reviewed source. This pins the security5+// properties across that transform, naming every expected rewrite.6+7+import { existsSync, readFileSync } from "node:fs";8+import { resolve } from "node:path";9+import { describe, expect, it } from "vitest";10+11+type ContentScript = {12+ matches?: string[];13+ js?: string[];14+ run_at?: string;15+ all_frames?: boolean;16+};17+18+type ExtensionManifest = {19+ permissions?: string[];20+ host_permissions?: string[];21+ content_security_policy?: Record<string, string>;22+ content_scripts?: ContentScript[];23+ background?: {24+ service_worker?: string;25+ scripts?: string[];26+ type?: string;27+ };28+};29+30+/** @crxjs/vite-plugin injects this; the sibling WAR test asserts its scope. */31+const CRXJS_ADDED_KEYS = ["web_accessible_resources"];32+33+const sourcePath = (target: "chrome" | "firefox") =>34+ resolve(process.cwd(), target === "chrome" ? "manifest.json" : "manifest.firefox.json");35+const builtPath = (target: "chrome" | "firefox") =>36+ resolve(process.cwd(), `dist-${target}`, "manifest.json");37+38+function read(path: string): ExtensionManifest {39+ return JSON.parse(readFileSync(path, "utf8")) as ExtensionManifest;40+}41+42+for (const target of ["chrome", "firefox"] as const) {43+ describe.skipIf(!existsSync(builtPath(target)))(44+ `built ${target} manifest matches the reviewed source manifest`,45+ () => {46+ it("adds no permission and no host permission", () => {47+ const source = read(sourcePath(target));48+ const built = read(builtPath(target));49+50+ expect(built.permissions ?? []).toEqual(source.permissions ?? []);51+ expect(built.host_permissions ?? []).toEqual(source.host_permissions ?? []);52+ });53+54+ it("keeps the content security policy byte-identical", () => {55+ const source = read(sourcePath(target));56+ const built = read(builtPath(target));57+58+ expect(built.content_security_policy).toEqual(source.content_security_policy);59+ });60+61+ it("keeps content_scripts matches, run_at and frame scope identical", () => {62+ const source = read(sourcePath(target));63+ const built = read(builtPath(target));64+65+ const shape = (scripts: ContentScript[] | undefined) =>66+ (scripts ?? []).map((script) => ({67+ matches: script.matches,68+ run_at: script.run_at,69+ all_frames: script.all_frames,70+ scriptCount: script.js?.length ?? 0,71+ }));72+73+ expect(shape(built.content_scripts)).toEqual(shape(source.content_scripts));74+ });75+76+ it("keeps the same background kind and module type", () => {77+ const source = read(sourcePath(target));78+ const built = read(builtPath(target));79+80+ expect(Object.keys(built.background ?? {}).sort()).toEqual(81+ Object.keys(source.background ?? {}).sort(),82+ );83+ expect(built.background?.type).toBe(source.background?.type);84+ });85+86+ it("introduces no top-level key beyond the known crxjs addition", () => {87+ const source = read(sourcePath(target));88+ const built = JSON.parse(readFileSync(builtPath(target), "utf8")) as Record<89+ string,90+ unknown91+ >;92+93+ const added = Object.keys(built).filter(94+ (key) => !(key in source) && !CRXJS_ADDED_KEYS.includes(key),95+ );96+ expect(added).toEqual([]);97+ });98+ },99+ );100+}