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
4 files manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1scripts/manifest-parity.test.ts+100 −0
manifest.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": {

view file

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",

view file

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",

view file

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+}

view file