3c90975bc48e
feat: Add native sync transport adapter (v2.13.23)
- time
- 2026-04-30 14:51 UTC · 6 months ago
- machine
- koh2
- parent
- 32907007805d
↓ peach-browser-3c90975b.tar.gzatlas at this save
files changed7 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 −0android/.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 fileandroid/.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+ "associatedIndex": 6,155+ "fromUser": 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+ "keyToString": {164+ "ModuleVcsDetector.initialDetectionPerformed": "true",165+ "RunOnceActivity.ShowReadmeOnStart": "true",166+ "RunOnceActivity.cidr.known.project.marker": "true",167+ "RunOnceActivity.git.unshallow": "true",168+ "RunOnceActivity.readMode.enableVisualFormatting": "true",169+ "cf.first.check.clang-format": "false",170+ "cidr.known.project.marker": "true",171+ "git-widget-placeholder": "main",172+ "kotlin-language-version-configured": "true"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="" />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": {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",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": {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 () => {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> {