0607c6c271d5

chore: Add native sync manifest generator (v2.13.15)

time
2026-04-30 14:05 UTC · 6 months ago
machine
koh2
parent
7d8739461dec
5 files manifest.firefox.json+1 −1manifest.json+1 −1package.json+3 −2scripts/generate-native-sync-manifest.mjs+113 −0src/shared/native-sync-manifest-script.test.ts+54 −0
manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22   "manifest_version": 3,33   "name": "Peach",4-  "version": "2.13.14",4+  "version": "2.13.15",55   "description": "Zero-knowledge password manager",66   "browser_specific_settings": {77     "gecko": {

view file

manifest.json+1 −1
@@ -1,7 +1,7 @@11 {22   "manifest_version": 3,33   "name": "Peach",4-  "version": "2.13.14",4+  "version": "2.13.15",55   "description": "Zero-knowledge password manager",66   "action": {77     "default_popup": "src/popup/index.html",

view file

package.json+3 −2
@@ -1,6 +1,6 @@11 {22   "name": "peach",3-  "version": "2.13.14",3+  "version": "2.13.15",44   "description": "Zero-knowledge password manager",55   "type": "module",66   "scripts": {@@ -14,7 +14,8 @@1414     "test": "vitest run",1515     "test:watch": "vitest",1616     "test:passkeys": "npm run build:e2e && playwright test tests/passkeys.spec.ts",17-    "build:mobile": "pnpm vite build --config vite.config.mobile.ts && node scripts/copy-mobile-index.js"17+    "build:mobile": "pnpm vite build --config vite.config.mobile.ts && node scripts/copy-mobile-index.js",18+    "native:manifest": "node scripts/generate-native-sync-manifest.mjs"1819   },1920   "dependencies": {2021     "@bokuweb/zstd-wasm": "^0.0.27",

view file

scripts/generate-native-sync-manifest.mjs+113 −0
@@ -0,0 +1,113 @@1+#!/usr/bin/env node2+import { mkdir, readFile, writeFile } from "node:fs/promises";3+import path from "node:path";4+import process from "node:process";5+import { pathToFileURL } from "node:url";6+7+const HOST_NAME = "com.peachpasswords.sync";8+const DESCRIPTION = "Peach Sync native messaging host";9+const DEFAULT_HOST_PATH = "native/peach-sync-host/target/release/peach-sync-host";10+11+function usage() {12+  return [13+    "Usage:",14+    "  node scripts/generate-native-sync-manifest.mjs --browser chrome --extension-id <chrome-id> [--host-path <path>] [--out <path>]",15+    "  node scripts/generate-native-sync-manifest.mjs --browser firefox [--extension-id <gecko-id>] [--host-path <path>] [--out <path>]",16+  ].join("\n");17+}18+19+function parseArgs(argv) {20+  const options = {};21+  for (let i = 0; i < argv.length; i += 1) {22+    const arg = argv[i];23+    if (arg === "--help" || arg === "-h") {24+      options.help = true;25+      continue;26+    }27+    if (!arg.startsWith("--")) {28+      throw new Error(`unexpected argument: ${arg}`);29+    }30+    const key = arg.slice(2);31+    const value = argv[i + 1];32+    if (!value || value.startsWith("--")) {33+      throw new Error(`missing value for --${key}`);34+    }35+    options[toCamel(key)] = value;36+    i += 1;37+  }38+  return options;39+}40+41+function toCamel(value) {42+  return value.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());43+}44+45+async function defaultFirefoxExtensionId(projectRoot) {46+  const manifestPath = path.join(projectRoot, "manifest.firefox.json");47+  const manifest = JSON.parse(await readFile(manifestPath, "utf8"));48+  const id = manifest?.browser_specific_settings?.gecko?.id;49+  if (typeof id !== "string" || id.length === 0) {50+    throw new Error("manifest.firefox.json is missing browser_specific_settings.gecko.id");51+  }52+  return id;53+}54+55+async function buildNativeManifest(options, projectRoot = process.cwd()) {56+  const browser = options.browser;57+  if (browser !== "chrome" && browser !== "firefox") {58+    throw new Error("--browser must be chrome or firefox");59+  }60+61+  const hostPath = path.resolve(projectRoot, options.hostPath ?? DEFAULT_HOST_PATH);62+  const manifest = {63+    name: HOST_NAME,64+    description: DESCRIPTION,65+    path: hostPath,66+    type: "stdio",67+  };68+69+  if (browser === "chrome") {70+    const extensionId = options.extensionId ?? process.env.PEACH_CHROME_EXTENSION_ID;71+    if (!extensionId) {72+      throw new Error("--extension-id or PEACH_CHROME_EXTENSION_ID is required for Chrome");73+    }74+    return {75+      ...manifest,76+      allowed_origins: [`chrome-extension://${extensionId}/`],77+    };78+  }79+80+  return {81+    ...manifest,82+    allowed_extensions: [options.extensionId ?? (await defaultFirefoxExtensionId(projectRoot))],83+  };84+}85+86+async function main() {87+  const options = parseArgs(process.argv.slice(2));88+  if (options.help) {89+    console.log(usage());90+    return;91+  }92+93+  const manifest = await buildNativeManifest(options);94+  const json = `${JSON.stringify(manifest, null, 2)}\n`;95+  if (options.out) {96+    const outPath = path.resolve(process.cwd(), options.out);97+    await mkdir(path.dirname(outPath), { recursive: true });98+    await writeFile(outPath, json, { mode: 0o644 });99+    console.log(outPath);100+    return;101+  }102+  process.stdout.write(json);103+}104+105+if (import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {106+  main().catch((err) => {107+    console.error(err instanceof Error ? err.message : String(err));108+    console.error(usage());109+    process.exitCode = 1;110+  });111+}112+113+export { buildNativeManifest, parseArgs };

view file

src/shared/native-sync-manifest-script.test.ts+54 −0
@@ -0,0 +1,54 @@1+import { execFile } from "node:child_process";2+import { promisify } from "node:util";3+import { describe, expect, it } from "vitest";4+5+const execFileAsync = promisify(execFile);6+const scriptPath = "scripts/generate-native-sync-manifest.mjs";7+8+describe("native sync manifest generator", () => {9+  it("generates a Chrome native messaging host manifest", async () => {10+    const { stdout } = await execFileAsync(process.execPath, [11+      scriptPath,12+      "--browser",13+      "chrome",14+      "--extension-id",15+      "abcdefghijklmnopabcdefghijklmnop",16+      "--host-path",17+      "/opt/peach/peach-sync-host",18+    ]);19+20+    expect(JSON.parse(stdout)).toEqual({21+      name: "com.peachpasswords.sync",22+      description: "Peach Sync native messaging host",23+      path: "/opt/peach/peach-sync-host",24+      type: "stdio",25+      allowed_origins: ["chrome-extension://abcdefghijklmnopabcdefghijklmnop/"],26+    });27+  });28+29+  it("generates a Firefox native messaging host manifest using the Gecko id", async () => {30+    const { stdout } = await execFileAsync(process.execPath, [31+      scriptPath,32+      "--browser",33+      "firefox",34+      "--host-path",35+      "/opt/peach/peach-sync-host",36+    ]);37+38+    expect(JSON.parse(stdout)).toEqual({39+      name: "com.peachpasswords.sync",40+      description: "Peach Sync native messaging host",41+      path: "/opt/peach/peach-sync-host",42+      type: "stdio",43+      allowed_extensions: ["[email protected]"],44+    });45+  });46+47+  it("requires a Chrome extension id", async () => {48+    await expect(49+      execFileAsync(process.execPath, [scriptPath, "--browser", "chrome"], {50+        env: { ...process.env, PEACH_CHROME_EXTENSION_ID: "" },51+      })52+    ).rejects.toThrow("--extension-id or PEACH_CHROME_EXTENSION_ID is required for Chrome");53+  });54+});

view file