// PB-26-610 — the source manifests are security-reviewed; the built manifests // are what ships. @crxjs/vite-plugin rewrites them at build time, so a change // to the plugin or to vite.config.ts could add a permission or weaken the CSP // in the artifact without touching the reviewed source. This pins the security // properties across that transform, naming every expected rewrite. import { existsSync, readFileSync } from "node:fs"; import { resolve } from "node:path"; import { describe, expect, it } from "vitest"; type ContentScript = { matches?: string[]; js?: string[]; run_at?: string; all_frames?: boolean; }; type ExtensionManifest = { permissions?: string[]; host_permissions?: string[]; content_security_policy?: Record; content_scripts?: ContentScript[]; background?: { service_worker?: string; scripts?: string[]; type?: string; }; }; /** @crxjs/vite-plugin injects this; the sibling WAR test asserts its scope. */ const CRXJS_ADDED_KEYS = ["web_accessible_resources"]; const sourcePath = (target: "chrome" | "firefox") => resolve(process.cwd(), target === "chrome" ? "manifest.json" : "manifest.firefox.json"); const builtPath = (target: "chrome" | "firefox") => resolve(process.cwd(), `dist-${target}`, "manifest.json"); function read(path: string): ExtensionManifest { return JSON.parse(readFileSync(path, "utf8")) as ExtensionManifest; } for (const target of ["chrome", "firefox"] as const) { describe.skipIf(!existsSync(builtPath(target)))( `built ${target} manifest matches the reviewed source manifest`, () => { it("adds no permission and no host permission", () => { const source = read(sourcePath(target)); const built = read(builtPath(target)); expect(built.permissions ?? []).toEqual(source.permissions ?? []); expect(built.host_permissions ?? []).toEqual(source.host_permissions ?? []); }); it("keeps the content security policy byte-identical", () => { const source = read(sourcePath(target)); const built = read(builtPath(target)); expect(built.content_security_policy).toEqual(source.content_security_policy); }); it("keeps content_scripts matches, run_at and frame scope identical", () => { const source = read(sourcePath(target)); const built = read(builtPath(target)); const shape = (scripts: ContentScript[] | undefined) => (scripts ?? []).map((script) => ({ matches: script.matches, run_at: script.run_at, all_frames: script.all_frames, scriptCount: script.js?.length ?? 0, })); expect(shape(built.content_scripts)).toEqual(shape(source.content_scripts)); }); it("keeps the same background kind and module type", () => { const source = read(sourcePath(target)); const built = read(builtPath(target)); expect(Object.keys(built.background ?? {}).sort()).toEqual( Object.keys(source.background ?? {}).sort(), ); expect(built.background?.type).toBe(source.background?.type); }); it("introduces no top-level key beyond the known crxjs addition", () => { const source = read(sourcePath(target)); const built = JSON.parse(readFileSync(builtPath(target), "utf8")) as Record< string, unknown >; const added = Object.keys(built).filter( (key) => !(key in source) && !CRXJS_ADDED_KEYS.includes(key), ); expect(added).toEqual([]); }); }, ); }