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
↓ peach-browser-97d959c4.tar.gzatlas at this save
files changed5 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 −0manifest.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": {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",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",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+}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+});