3c90975bc48e

feat: Add native sync transport adapter (v2.13.23)

- Add createNativeSyncTransport: build a SyncSessionTransport for a connectionId on top of sync.ts - Frames and errors are filtered to the connection_id; global errors pass through - onClose handler fires for matching disconnects and for full host disconnect (undefined id) - send/close/getSessionKey proxy to sendSyncData/disconnectFromPeer/getNativeSessionKey - Bindings are injectable for tests; default path lazy-loads ./sync - Cover scoped routing and proxy wiring in sync-coordinator.test.ts (23 total) Verification: npm typecheck, focused coordinator 23/23, npm test 275/275, npm run build clean.

time
2026-04-30 14:51 UTC · 6 months ago
machine
koh2
parent
32907007805d
7 files android/.idea/deploymentTargetSelector.xml+11 −0android/.idea/workspace.xml+87 −16manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/background/sync-coordinator.test.ts+70 −0src/background/sync-coordinator.ts+49 −0
android/.idea/deploymentTargetSelector.xml+11 −0
@@ -0,0 +1,11 @@1+<?xml version="1.0" encoding="UTF-8"?>2+<project version="4">3+  <component name="deploymentTargetSelector">4+    <selectionStates>5+      <SelectionState runConfigName="app">6+        <option name="selectionMode" value="DROPDOWN" />7+        <DialogSelection />8+      </SelectionState>9+    </selectionStates>10+  </component>11+</project>\ No newline at end of file

view file

android/.idea/workspace.xml+87 −16
@@ -78,6 +78,7 @@7878       <change beforePath="$PROJECT_DIR$/../peach_sync_spec.md" beforeDir="false" />7979       <change beforePath="$PROJECT_DIR$/../peach_vault_integrity_spec.md" beforeDir="false" />8080       <change beforePath="$PROJECT_DIR$/../scripts/clean-firefox-artifacts.mjs" beforeDir="false" afterPath="$PROJECT_DIR$/../scripts/clean-firefox-artifacts.mjs" afterDir="false" />81+      <change beforePath="$PROJECT_DIR$/../src/background/alarms.ts" beforeDir="false" afterPath="$PROJECT_DIR$/../src/background/alarms.ts" afterDir="false" />8182       <change beforePath="$PROJECT_DIR$/../src/background/clipboard.ts" beforeDir="false" afterPath="$PROJECT_DIR$/../src/background/clipboard.ts" afterDir="false" />8283       <change beforePath="$PROJECT_DIR$/../src/background/index.ts" beforeDir="false" afterPath="$PROJECT_DIR$/../src/background/index.ts" afterDir="false" />8384       <change beforePath="$PROJECT_DIR$/../src/background/license.ts" beforeDir="false" afterPath="$PROJECT_DIR$/../src/background/license.ts" afterDir="false" />@@ -103,6 +104,7 @@103104       <change beforePath="$PROJECT_DIR$/../src/crypto/cjkSerialise.ts" beforeDir="false" />104105       <change beforePath="$PROJECT_DIR$/../src/popup/App.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/../src/popup/App.tsx" afterDir="false" />105106       <change beforePath="$PROJECT_DIR$/../src/popup/components/GeneratorDrawer.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/../src/popup/components/GeneratorDrawer.tsx" afterDir="false" />107+      <change beforePath="$PROJECT_DIR$/../src/popup/index.html" beforeDir="false" afterPath="$PROJECT_DIR$/../src/popup/index.html" afterDir="false" />106108       <change beforePath="$PROJECT_DIR$/../src/popup/styles/popup.css" beforeDir="false" afterPath="$PROJECT_DIR$/../src/popup/styles/popup.css" afterDir="false" />107109       <change beforePath="$PROJECT_DIR$/../src/popup/styles/vars.css" beforeDir="false" afterPath="$PROJECT_DIR$/../src/popup/styles/vars.css" afterDir="false" />108110       <change beforePath="$PROJECT_DIR$/../src/popup/views/Add.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/../src/popup/views/Add.tsx" afterDir="false" />@@ -139,6 +141,7 @@139141   <component name="ClangdSettings">140142     <option name="formatViaClangd" value="false" />141143   </component>144+  <component name="ExecutionTargetManager" SELECTED_TARGET="device_and_snapshot_combo_box_target[LocalEmulator::path=/Users/june/.android/avd/Medium_Phone.avd]" />142145   <component name="ExternalProjectsData">143146     <projectState path="$PROJECT_DIR$">144147       <ProjectState />@@ -147,28 +150,96 @@147150   <component name="Git.Settings">148151     <option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$/.." />149152   </component>150-  <component name="ProjectColorInfo"><![CDATA[{151-  "associatedIndex": 6,152-  "fromUser": false153-}]]></component>153+  <component name="ProjectColorInfo">{154+  &quot;associatedIndex&quot;: 6,155+  &quot;fromUser&quot;: false156+}</component>154157   <component name="ProjectId" id="3D2xzQY1OHSnt7s7fRV4DvX0WLi" />155158   <component name="ProjectViewState">156159     <option name="hideEmptyMiddlePackages" value="true" />157160     <option name="showLibraryContents" value="true" />158161   </component>159-  <component name="PropertiesComponent"><![CDATA[{160-  "keyToString": {161-    "ModuleVcsDetector.initialDetectionPerformed": "true",162-    "RunOnceActivity.ShowReadmeOnStart": "true",163-    "RunOnceActivity.cidr.known.project.marker": "true",164-    "RunOnceActivity.git.unshallow": "true",165-    "RunOnceActivity.readMode.enableVisualFormatting": "true",166-    "cf.first.check.clang-format": "false",167-    "cidr.known.project.marker": "true",168-    "git-widget-placeholder": "main",169-    "kotlin-language-version-configured": "true"162+  <component name="PropertiesComponent">{163+  &quot;keyToString&quot;: {164+    &quot;ModuleVcsDetector.initialDetectionPerformed&quot;: &quot;true&quot;,165+    &quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,166+    &quot;RunOnceActivity.cidr.known.project.marker&quot;: &quot;true&quot;,167+    &quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;,168+    &quot;RunOnceActivity.readMode.enableVisualFormatting&quot;: &quot;true&quot;,169+    &quot;cf.first.check.clang-format&quot;: &quot;false&quot;,170+    &quot;cidr.known.project.marker&quot;: &quot;true&quot;,171+    &quot;git-widget-placeholder&quot;: &quot;main&quot;,172+    &quot;kotlin-language-version-configured&quot;: &quot;true&quot;170173   }171-}]]></component>174+}</component>175+  <component name="RunManager">176+    <configuration name="app" type="AndroidRunConfigurationType" factoryName="Android App" activateToolWindowBeforeRun="false">177+      <module name="android.app" />178+      <option name="ANDROID_RUN_CONFIGURATION_SCHEMA_VERSION" value="1" />179+      <option name="DEPLOY" value="true" />180+      <option name="DEPLOY_APK_FROM_BUNDLE" value="false" />181+      <option name="DEPLOY_AS_INSTANT" value="false" />182+      <option name="ARTIFACT_NAME" value="" />183+      <option name="PM_INSTALL_OPTIONS" value="" />184+      <option name="ALL_USERS" value="false" />185+      <option name="ALWAYS_INSTALL_WITH_PM" value="false" />186+      <option name="ALLOW_ASSUME_VERIFIED" value="false" />187+      <option name="CLEAR_APP_STORAGE" value="false" />188+      <option name="DYNAMIC_FEATURES_DISABLED_LIST" value="" />189+      <option name="ACTIVITY_EXTRA_FLAGS" value="" />190+      <option name="MODE" value="default_activity" />191+      <option name="RESTORE_ENABLED" value="false" />192+      <option name="RESTORE_FILE" value="" />193+      <option name="RESTORE_FRESH_INSTALL_ONLY" value="false" />194+      <option name="CLEAR_LOGCAT" value="false" />195+      <option name="SHOW_LOGCAT_AUTOMATICALLY" value="false" />196+      <option name="TARGET_SELECTION_MODE" value="DEVICE_AND_SNAPSHOT_COMBO_BOX" />197+      <option name="DEBUGGER_TYPE" value="Auto" />198+      <Auto>199+        <option name="USE_JAVA_AWARE_DEBUGGER" value="false" />200+        <option name="SHOW_STATIC_VARS" value="true" />201+        <option name="WORKING_DIR" value="" />202+        <option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />203+        <option name="SHOW_OPTIMIZED_WARNING" value="true" />204+        <option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />205+      </Auto>206+      <Hybrid>207+        <option name="USE_JAVA_AWARE_DEBUGGER" value="false" />208+        <option name="SHOW_STATIC_VARS" value="true" />209+        <option name="WORKING_DIR" value="" />210+        <option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />211+        <option name="SHOW_OPTIMIZED_WARNING" value="true" />212+        <option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />213+      </Hybrid>214+      <Java>215+        <option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />216+      </Java>217+      <Native>218+        <option name="USE_JAVA_AWARE_DEBUGGER" value="false" />219+        <option name="SHOW_STATIC_VARS" value="true" />220+        <option name="WORKING_DIR" value="" />221+        <option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />222+        <option name="SHOW_OPTIMIZED_WARNING" value="true" />223+        <option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />224+      </Native>225+      <Profilers>226+        <option name="ADVANCED_PROFILING_ENABLED" value="false" />227+        <option name="STARTUP_PROFILING_ENABLED" value="false" />228+        <option name="STARTUP_CPU_PROFILING_ENABLED" value="false" />229+        <option name="STARTUP_CPU_PROFILING_CONFIGURATION_NAME" value="Java/Kotlin Method Sample (legacy)" />230+        <option name="STARTUP_NATIVE_MEMORY_PROFILING_ENABLED" value="false" />231+        <option name="NATIVE_MEMORY_SAMPLE_RATE_BYTES" value="2048" />232+      </Profilers>233+      <option name="DEEP_LINK" value="" />234+      <option name="ACTIVITY" value="" />235+      <option name="ACTIVITY_CLASS" value="" />236+      <option name="SEARCH_ACTIVITY_IN_GLOBAL_SCOPE" value="false" />237+      <option name="SKIP_ACTIVITY_VALIDATION" value="false" />238+      <method v="2">239+        <option name="Android.Gradle.BeforeRunTask" enabled="true" />240+      </method>241+    </configuration>242+  </component>172243   <component name="TaskManager">173244     <task active="true" id="Default" summary="Default task">174245       <changelist id="692de111-524f-4f23-9bdf-f7ecd91ee221" name="Changes" comment="" />

view file

manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22   "manifest_version": 3,33   "name": "Peach",4-  "version": "2.13.22",4+  "version": "2.13.23",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.22",4+  "version": "2.13.23",55   "description": "Zero-knowledge password manager",66   "action": {77     "default_popup": "src/popup/index.html",

view file

package.json+1 −1
@@ -1,6 +1,6 @@11 {22   "name": "peach",3-  "version": "2.13.22",3+  "version": "2.13.23",44   "description": "Zero-knowledge password manager",55   "type": "module",66   "scripts": {

view file

src/background/sync-coordinator.test.ts+70 −0
@@ -6,9 +6,11 @@66   buildLocalSyncSnapshot,77   cancelSyncIfVaultLocked,88   checkSyncPreflight,9+  createNativeSyncTransport,910   dispatchSyncFrame,1011   planManifestSync,1112   runSyncSession,13+  type NativeSyncTransportBindings,1214   type SyncSessionTransport,1315 } from "./sync-coordinator";1416 import { decodeSyncMessage, encodeSyncMessage } from "../shared/sync-wire";@@ -531,6 +533,74 @@531533         code: "transport_error",532534         message: "native host disconnected",533535       });536+    });537+538+    it("createNativeSyncTransport routes only matching frames and errors", async () => {539+      const sendSyncData = vi.fn();540+      const disconnectFromPeer = vi.fn();541+      const handlers: {542+        frame: ((id: string, data: string) => void) | null;543+        error: ((msg: string, id?: string) => void) | null;544+        disconnect: ((id?: string) => void) | null;545+      } = { frame: null, error: null, disconnect: null };546+547+      const bindings: NativeSyncTransportBindings = {548+        sendSyncData,549+        disconnectFromPeer,550+        onSyncMessage: (h) => {551+          handlers.frame = h;552+          return () => {553+            if (handlers.frame === h) handlers.frame = null;554+          };555+        },556+        onError: (h) => {557+          handlers.error = h;558+          return () => {559+            if (handlers.error === h) handlers.error = null;560+          };561+        },562+        onDisconnected: (h) => {563+          handlers.disconnect = h;564+          return () => {565+            if (handlers.disconnect === h) handlers.disconnect = null;566+          };567+        },568+        getNativeSessionKey: async () => new Uint8Array(32).fill(7),569+      };570+571+      const transport = await createNativeSyncTransport("target", bindings);572+573+      const frames: string[] = [];574+      const errors: string[] = [];575+      let closes = 0;576+      transport.onFrame((frame) => frames.push(frame));577+      transport.onError((message) => errors.push(message));578+      transport.onClose(() => {579+        closes += 1;580+      });581+582+      handlers.frame?.("other", "ignored");583+      handlers.frame?.("target", "kept");584+      expect(frames).toEqual(["kept"]);585+586+      handlers.error?.("scoped error", "other");587+      handlers.error?.("scoped error", "target");588+      handlers.error?.("global error", undefined);589+      expect(errors).toEqual(["scoped error", "global error"]);590+591+      handlers.disconnect?.("other");592+      handlers.disconnect?.("target");593+      handlers.disconnect?.(undefined);594+      expect(closes).toBe(2);595+596+      transport.send("frame");597+      expect(sendSyncData).toHaveBeenCalledWith("target", "frame");598+599+      transport.close();600+      expect(disconnectFromPeer).toHaveBeenCalledWith("target");601+602+      const key = await transport.getSessionKey();603+      expect(key).toEqual(new Uint8Array(32).fill(7));534604     });535605 536606     it("records merge totals from a SYNC_ENTRIES frame on the result", async () => {

view file

src/background/sync-coordinator.ts+49 −0
@@ -65,6 +65,55 @@6565   };6666 }6767 68+export interface NativeSyncTransportBindings {69+  sendSyncData: (connectionId: string, data: string) => void;70+  onSyncMessage: (71+    handler: (connectionId: string, data: string) => void72+  ) => () => void;73+  onError: (74+    handler: (message: string, connectionId?: string) => void75+  ) => () => void;76+  onDisconnected: (handler: (connectionId?: string) => void) => () => void;77+  disconnectFromPeer: (connectionId: string) => void;78+  getNativeSessionKey: (connectionId: string) => Promise<Uint8Array>;79+}80+81+async function loadNativeSyncBindings(): Promise<NativeSyncTransportBindings> {82+  const sync = await import("./sync");83+  return {84+    sendSyncData: sync.sendSyncData,85+    onSyncMessage: sync.onSyncMessage,86+    onError: sync.onError,87+    onDisconnected: sync.onDisconnected,88+    disconnectFromPeer: sync.disconnectFromPeer,89+    getNativeSessionKey: sync.getNativeSessionKey,90+  };91+}92+93+export async function createNativeSyncTransport(94+  connectionId: string,95+  bindings?: NativeSyncTransportBindings96+): Promise<SyncSessionTransport> {97+  const ready = bindings ?? (await loadNativeSyncBindings());98+  return {99+    send: (frame) => ready.sendSyncData(connectionId, frame),100+    onFrame: (handler) =>101+      ready.onSyncMessage((id, frame) => {102+        if (id === connectionId) handler(frame);103+      }),104+    onError: (handler) =>105+      ready.onError((message, id) => {106+        if (id === undefined || id === connectionId) handler(message);107+      }),108+    onClose: (handler) =>109+      ready.onDisconnected((id) => {110+        if (id === undefined || id === connectionId) handler();111+      }),112+    close: () => ready.disconnectFromPeer(connectionId),113+    getSessionKey: () => ready.getNativeSessionKey(connectionId),114+  };115+}116+68117 export async function checkSyncPreflight(69118   deps?: Pick<SyncCoordinatorDeps, "isProUser" | "isUnlocked">70119 ): Promise<SyncPreflightResult> {

view file