1234567891011121314151617import { Capacitor } from "@capacitor/core";18import * as sync from "./sync";19import { isProUser } from "./license";20import * as vault from "./vault";21import { getPairedDevices, setIncomingPairingRequest } from "./sync-storage";22import { decodeSyncMessage } from "../shared/sync-wire";2324const DEVICE_ID_KEY = "peach_device_id";25const DEVICE_NAME_KEY = "peach_device_name";2627let advertisingActive = false;28let nativeHostConnected = false;29let incomingSubscriberInstalled = false;3031async function getOrCreateDeviceId(): Promise<string> {32 try {33 const stored = await chrome.storage.local.get(DEVICE_ID_KEY);34 const value = (stored as Record<string, string | undefined>)[DEVICE_ID_KEY];35 if (typeof value === "string" && value) return value;36 } catch {37 38 }39 40 41 const random = crypto.getRandomValues(new Uint8Array(16));42 let binary = "";43 for (const byte of random) binary += String.fromCharCode(byte);44 const id = btoa(binary)45 .replace(/\+/g, "-")46 .replace(/\//g, "_")47 .replace(/=+$/g, "");48 try {49 await chrome.storage.local.set({ [DEVICE_ID_KEY]: id });50 } catch {51 52 }53 return id;54}5556async function getDeviceName(): Promise<string> {57 try {58 const stored = await chrome.storage.local.get(DEVICE_NAME_KEY);59 const value = (stored as Record<string, string | undefined>)[DEVICE_NAME_KEY];60 if (typeof value === "string" && value.trim()) return value.trim();61 } catch {62 63 }64 if (Capacitor.getPlatform() === "android") return "Peach Android";65 if (Capacitor.getPlatform() === "ios") return "Peach iOS";66 return "Peach";67}6869707172export async function syncLifecycleTick(): Promise<void> {73 74 75 if (Capacitor.isNativePlatform()) return;7677 if (!nativeHostConnected) {78 advertisingActive = false;79 return;80 }8182 let shouldAdvertise = false;83 try {84 const [pro, paired] = await Promise.all([isProUser(), getPairedDevices()]);85 shouldAdvertise = pro && vault.isUnlocked() && paired.length > 0;86 } catch {87 shouldAdvertise = false;88 }8990 if (shouldAdvertise && !advertisingActive) {91 try {92 const [deviceId, name] = await Promise.all([getOrCreateDeviceId(), getDeviceName()]);93 await sync.advertise({ deviceId, name });94 advertisingActive = true;95 } catch (err) {96 console.warn("sync advertise failed:", err);97 }98 } else if (!shouldAdvertise && advertisingActive) {99 try {100 await sync.stopAdvertising();101 } catch {102 103 }104 advertisingActive = false;105 }106}107108109110111112113114export function setupIncomingPairingSubscriber(): void {115 if (incomingSubscriberInstalled) return;116 incomingSubscriberInstalled = true;117118 sync.onIncomingConnection((info) => {119 void handleIncomingConnection(info);120 });121}122123async function handleIncomingConnection(info: sync.IncomingConnectionInfo): Promise<void> {124 if (info.isPairingOnly) {125 await waitForPairRequest(info);126 return;127 }128 129 await runResponderSession(info.connectionId);130}131132async function waitForPairRequest(info: sync.IncomingConnectionInfo): Promise<void> {133 const unsubscribe = sync.onSyncMessage((id, frame) => {134 if (id !== info.connectionId) return;135 unsubscribe();136 let publicKeyHex = info.peerPublicKeyHex ?? "";137 let deviceName = "Unknown device";138 try {139 const decoded = decodeSyncMessage(frame);140 if (decoded.type === "PAIR_REQUEST") {141 publicKeyHex = decoded.public_key_hex || publicKeyHex;142 deviceName = decoded.device_name || deviceName;143 } else {144 145 try { sync.disconnectFromPeer(info.connectionId); } catch { }146 return;147 }148 } catch {149 try { sync.disconnectFromPeer(info.connectionId); } catch { }150 return;151 }152153 const now = Date.now();154 void setIncomingPairingRequest({155 deviceName,156 publicKeyHex,157 receivedAt: now,158 expiresAt: now + 60_000,159 connectionId: info.connectionId,160 });161162 163 164 chrome.runtime165 .sendMessage({ type: "INCOMING_PAIRING_REQUEST", payload: { deviceName } })166 .catch(() => {});167 });168169 170 const offClose = sync.onDisconnected((id) => {171 if (id === undefined || id === info.connectionId) {172 try { unsubscribe(); } catch { }173 try { offClose(); } catch { }174 }175 });176}177178async function runResponderSession(connectionId: string): Promise<void> {179 try {180 const { createNativeSyncTransport, runBlobSyncSession } = await import(181 "./sync-coordinator"182 );183 const { storageGet } = await import("../shared/storage");184 const transport = await createNativeSyncTransport(connectionId);185 await runBlobSyncSession("responder", transport, {186 isPro: () => isProUser(),187 isUnlocked: () => vault.isUnlocked(),188 getVaultBlobs: async () => ({189 vault_a: (await storageGet("peach_vault_a")) ?? "",190 vault_b: (await storageGet("peach_vault_b")) ?? "",191 }),192 });193 } catch (err) {194 console.warn("responder sync session failed:", err);195 }196}197198199200201202203export function onNativeHostConnected(): void {204 nativeHostConnected = true;205 setupIncomingPairingSubscriber();206 void syncLifecycleTick();207}208209export function onNativeHostDisconnected(): void {210 nativeHostConnected = false;211 advertisingActive = false;212}213214export async function onVaultUnlocked(): Promise<void> {215 await syncLifecycleTick();216}217218export async function onVaultLocked(): Promise<void> {219 await syncLifecycleTick();220}221222export async function onProActivated(): Promise<void> {223 await syncLifecycleTick();224}225226227228229230export function __resetForTests(): void {231 advertisingActive = false;232 nativeHostConnected = false;233 incomingSubscriberInstalled = false;234}