manifest-parity.test.ts

3.5 KB · raw

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.67import { existsSync, readFileSync } from "node:fs";8import { resolve } from "node:path";9import { describe, expect, it } from "vitest";1011type ContentScript = {12  matches?: string[];13  js?: string[];14  run_at?: string;15  all_frames?: boolean;16};1718type 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};2930/** @crxjs/vite-plugin injects this; the sibling WAR test asserts its scope. */31const CRXJS_ADDED_KEYS = ["web_accessible_resources"];3233const sourcePath = (target: "chrome" | "firefox") =>34  resolve(process.cwd(), target === "chrome" ? "manifest.json" : "manifest.firefox.json");35const builtPath = (target: "chrome" | "firefox") =>36  resolve(process.cwd(), `dist-${target}`, "manifest.json");3738function read(path: string): ExtensionManifest {39  return JSON.parse(readFileSync(path, "utf8")) as ExtensionManifest;40}4142for (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));4950        expect(built.permissions ?? []).toEqual(source.permissions ?? []);51        expect(built.host_permissions ?? []).toEqual(source.host_permissions ?? []);52      });5354      it("keeps the content security policy byte-identical", () => {55        const source = read(sourcePath(target));56        const built = read(builtPath(target));5758        expect(built.content_security_policy).toEqual(source.content_security_policy);59      });6061      it("keeps content_scripts matches, run_at and frame scope identical", () => {62        const source = read(sourcePath(target));63        const built = read(builtPath(target));6465        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          }));7273        expect(shape(built.content_scripts)).toEqual(shape(source.content_scripts));74      });7576      it("keeps the same background kind and module type", () => {77        const source = read(sourcePath(target));78        const built = read(builtPath(target));7980        expect(Object.keys(built.background ?? {}).sort()).toEqual(81          Object.keys(source.background ?? {}).sort(),82        );83        expect(built.background?.type).toBe(source.background?.type);84      });8586      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        >;9293        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}