97d959c4b02e

feat: Native messaging host installer + Sync architecture docs (v2.13.31)

time
2026-04-30 15:22 UTC · 6 months ago
machine
koh2
parent
8d3a3cc67b03
5 files manifest.firefox.json+1 −1manifest.json+1 −1package.json+3 −2scripts/install-native-sync-host.mjs+178 −0src/shared/native-sync-installer.test.ts+79 −0
manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22   "manifest_version": 3,33   "name": "Peach",4-  "version": "2.13.30",4+  "version": "2.13.31",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.30",4+  "version": "2.13.31",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.30",3+  "version": "2.13.31",44   "description": "Zero-knowledge password manager",55   "type": "module",66   "scripts": {@@ -15,7 +15,8 @@1515     "test:watch": "vitest",1616     "test:passkeys": "npm run build:e2e && playwright test tests/passkeys.spec.ts",1717     "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"18+    "native:manifest": "node scripts/generate-native-sync-manifest.mjs",19+    "native:install": "node scripts/install-native-sync-host.mjs"1920   },2021   "dependencies": {2122     "@bokuweb/zstd-wasm": "^0.0.27",

view file

scripts/install-native-sync-host.mjs+178 −0
@@ -0,0 +1,178 @@1+#!/usr/bin/env node2+// Installs the com.peachpasswords.sync native messaging host manifest into3+// the per-browser NativeMessagingHosts directory so chrome.runtime.connectNative4+// can find the Peach sync host binary at runtime.5+//6+// Unix (macOS / Linux): writes a JSON file under the user's NativeMessagingHosts7+// directory. Windows requires a registry entry and is not handled directly —8+// the script prints the JSON path and the registry key the user must add.9+//10+// Usage:11+//   node scripts/install-native-sync-host.mjs --browser chrome --extension-id <id>12+//   node scripts/install-native-sync-host.mjs --browser firefox13+//14+// Optional flags:15+//   --host-path <abs-path>   Override the binary path (default: release build)16+//   --dry-run                Print what would be written; do not touch the FS17+import { mkdir, writeFile } from "node:fs/promises";18+import os from "node:os";19+import path from "node:path";20+import process from "node:process";21+import { pathToFileURL } from "node:url";22+23+import { buildNativeManifest } from "./generate-native-sync-manifest.mjs";24+25+const HOST_FILENAME = "com.peachpasswords.sync.json";26+27+function usage() {28+  return [29+    "Usage:",30+    "  node scripts/install-native-sync-host.mjs --browser chrome --extension-id <id>",31+    "  node scripts/install-native-sync-host.mjs --browser firefox [--extension-id <gecko-id>]",32+    "",33+    "Optional:",34+    "  --host-path <abs-path>   Override binary path (default: release build)",35+    "  --dry-run                Print the destination only; do not write",36+  ].join("\n");37+}38+39+function parseArgs(argv) {40+  const options = {};41+  for (let i = 0; i < argv.length; i += 1) {42+    const arg = argv[i];43+    if (arg === "--help" || arg === "-h") {44+      options.help = true;45+      continue;46+    }47+    if (arg === "--dry-run") {48+      options.dryRun = true;49+      continue;50+    }51+    if (arg === "--show-dir") {52+      options.showDir = true;53+      continue;54+    }55+    if (arg === "--platform-override") {56+      options.platformOverride = argv[i + 1];57+      i += 1;58+      continue;59+    }60+    if (arg === "--home-override") {61+      options.homeOverride = argv[i + 1];62+      i += 1;63+      continue;64+    }65+    if (!arg.startsWith("--")) {66+      throw new Error(`unexpected argument: ${arg}`);67+    }68+    const key = arg.slice(2);69+    const value = argv[i + 1];70+    if (!value || value.startsWith("--")) {71+      throw new Error(`missing value for --${key}`);72+    }73+    options[toCamel(key)] = value;74+    i += 1;75+  }76+  return options;77+}78+79+function toCamel(value) {80+  return value.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());81+}82+83+/**84+ * Resolves the OS-specific NativeMessagingHosts directory for the requested85+ * browser. Returns `{ dir, registryHint }` — `registryHint` is only set on86+ * Windows where a registry write is also required.87+ */88+export function resolveInstallDir(browser, platform = process.platform, home = os.homedir()) {89+  if (browser !== "chrome" && browser !== "firefox") {90+    throw new Error("--browser must be chrome or firefox");91+  }92+93+  if (platform === "darwin") {94+    if (browser === "chrome") {95+      return {96+        dir: path.join(home, "Library", "Application Support", "Google", "Chrome", "NativeMessagingHosts"),97+      };98+    }99+    return {100+      dir: path.join(home, "Library", "Application Support", "Mozilla", "NativeMessagingHosts"),101+    };102+  }103+104+  if (platform === "linux") {105+    if (browser === "chrome") {106+      return { dir: path.join(home, ".config", "google-chrome", "NativeMessagingHosts") };107+    }108+    return { dir: path.join(home, ".mozilla", "native-messaging-hosts") };109+  }110+111+  if (platform === "win32") {112+    // Windows uses the registry. We still write the JSON to a stable path so113+    // the registry value can point at it.114+    const dir = path.join(home, "AppData", "Local", "Peach", "NativeMessagingHosts");115+    const registryRoot =116+      browser === "chrome"117+        ? "HKEY_CURRENT_USER\\Software\\Google\\Chrome\\NativeMessagingHosts\\com.peachpasswords.sync"118+        : "HKEY_CURRENT_USER\\Software\\Mozilla\\NativeMessagingHosts\\com.peachpasswords.sync";119+    return {120+      dir,121+      registryHint: `Add a registry value at ${registryRoot} with the (Default) string set to ${path.join(122+        dir,123+        HOST_FILENAME124+      )}`,125+    };126+  }127+128+  throw new Error(`unsupported platform: ${platform}`);129+}130+131+async function main() {132+  const options = parseArgs(process.argv.slice(2));133+  if (options.help) {134+    console.log(usage());135+    return;136+  }137+  if (!options.browser) {138+    throw new Error("--browser is required");139+  }140+141+  const platform = options.platformOverride ?? process.platform;142+  const home = options.homeOverride ?? os.homedir();143+  const { dir, registryHint } = resolveInstallDir(options.browser, platform, home);144+145+  if (options.showDir) {146+    process.stdout.write(JSON.stringify({ dir, registryHint: registryHint ?? null }));147+    return;148+  }149+150+  const targetPath = path.join(dir, HOST_FILENAME);151+  const manifest = await buildNativeManifest(options);152+  const json = `${JSON.stringify(manifest, null, 2)}\n`;153+154+  if (options.dryRun) {155+    console.log(`Would write ${targetPath}`);156+    process.stdout.write(json);157+    if (registryHint) console.log(`\n${registryHint}`);158+    return;159+  }160+161+  await mkdir(dir, { recursive: true });162+  await writeFile(targetPath, json, { mode: 0o644 });163+  console.log(`Installed ${targetPath}`);164+  if (registryHint) {165+    console.log(registryHint);166+    console.log(167+      "Run `reg add` with the path above, or use the Registry Editor — Windows requires this entry to discover the host."168+    );169+  }170+}171+172+if (import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {173+  main().catch((err) => {174+    console.error(err instanceof Error ? err.message : String(err));175+    console.error(usage());176+    process.exitCode = 1;177+  });178+}

view file

src/shared/native-sync-installer.test.ts+79 −0
@@ -0,0 +1,79 @@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/install-native-sync-host.mjs";7+8+async function showDir(args: string[]): Promise<{ dir: string; registryHint: string | null }> {9+  const { stdout } = await execFileAsync(process.execPath, [10+    scriptPath,11+    "--show-dir",12+    "--browser",13+    args[0]!,14+    "--platform-override",15+    args[1]!,16+    "--home-override",17+    args[2]!,18+  ]);19+  return JSON.parse(stdout) as { dir: string; registryHint: string | null };20+}21+22+describe("native sync installer path resolution", () => {23+  const home = "/home/test";24+25+  it("resolves macOS Chrome under Application Support/Google/Chrome", async () => {26+    const { dir } = await showDir(["chrome", "darwin", home]);27+    expect(dir).toBe("/home/test/Library/Application Support/Google/Chrome/NativeMessagingHosts");28+  });29+30+  it("resolves macOS Firefox under Application Support/Mozilla", async () => {31+    const { dir } = await showDir(["firefox", "darwin", home]);32+    expect(dir).toBe("/home/test/Library/Application Support/Mozilla/NativeMessagingHosts");33+  });34+35+  it("resolves Linux Chrome under .config/google-chrome", async () => {36+    const { dir } = await showDir(["chrome", "linux", home]);37+    expect(dir).toBe("/home/test/.config/google-chrome/NativeMessagingHosts");38+  });39+40+  it("resolves Linux Firefox under .mozilla/native-messaging-hosts", async () => {41+    const { dir } = await showDir(["firefox", "linux", home]);42+    expect(dir).toBe("/home/test/.mozilla/native-messaging-hosts");43+  });44+45+  it("resolves Windows with a registry hint", async () => {46+    const { dir, registryHint } = await showDir(["chrome", "win32", "C:\\Users\\test"]);47+    expect(dir).toContain("Peach");48+    expect(registryHint).toContain("HKEY_CURRENT_USER");49+    expect(registryHint).toContain("com.peachpasswords.sync");50+  });51+52+  it("rejects unsupported platforms", async () => {53+    await expect(showDir(["chrome", "freebsd", home])).rejects.toThrow("unsupported platform");54+  });55+});56+57+describe("native sync installer script", () => {58+  it("--dry-run prints destination + manifest without writing", async () => {59+    const { stdout } = await execFileAsync(process.execPath, [60+      scriptPath,61+      "--browser",62+      "chrome",63+      "--extension-id",64+      "abcdefghijklmnopabcdefghijklmnop",65+      "--host-path",66+      "/opt/peach/peach-sync-host",67+      "--dry-run",68+    ]);69+    expect(stdout).toMatch(/Would write .*com\.peachpasswords\.sync\.json/);70+    expect(stdout).toContain('"name": "com.peachpasswords.sync"');71+    expect(stdout).toContain("/opt/peach/peach-sync-host");72+  });73+74+  it("rejects missing --browser", async () => {75+    await expect(76+      execFileAsync(process.execPath, [scriptPath, "--dry-run"])77+    ).rejects.toThrow("--browser is required");78+  });79+});

view file