1234567891011121314import { deriveSyncIdentityKeypair, pubkeyToHex } from "../shared/sync-identity";15import type { DeviceType } from "../shared/sync-types";1617const DEVICE_ID_KEY = "peach_chirp_device_id";18const DEVICE_NAME_KEY = "peach_chirp_device_name";1920export interface LocalChirpIdentity {21 deviceId: string;22 deviceName: string;23 deviceType: DeviceType;24 longTermPublicKeyHex: string;25}2627export async function getOrCreateDeviceId(): Promise<string> {28 const result = await chrome.storage.sync.get(DEVICE_ID_KEY);29 const current = (result as Record<string, unknown>)[DEVICE_ID_KEY];30 if (typeof current === "string" && current.length > 0) return current;31 const fresh = generateUuidV4();32 await chrome.storage.sync.set({ [DEVICE_ID_KEY]: fresh });33 return fresh;34}3536export async function getDeviceName(): Promise<string> {37 const result = await chrome.storage.sync.get(DEVICE_NAME_KEY);38 const stored = (result as Record<string, unknown>)[DEVICE_NAME_KEY];39 if (typeof stored === "string" && stored.length > 0) return stored;40 return defaultDeviceName();41}4243export async function setDeviceName(name: string): Promise<void> {44 const trimmed = name.trim();45 if (!trimmed) throw new Error("device name must not be empty");46 if (trimmed.length > 64) throw new Error("device name must be 64 chars or fewer");47 await chrome.storage.sync.set({ [DEVICE_NAME_KEY]: trimmed });48}49505152535455export async function buildLocalIdentity(56 masterKey: Uint8Array,57): Promise<LocalChirpIdentity> {58 const deviceId = await getOrCreateDeviceId();59 const deviceName = await getDeviceName();60 const kp = deriveSyncIdentityKeypair(masterKey, deviceId);61 return {62 deviceId,63 deviceName,64 deviceType: extensionDeviceType(),65 longTermPublicKeyHex: pubkeyToHex(kp.publicKey),66 };67}68697071727374export function deriveLocalSyncKeypair(masterKey: Uint8Array, deviceId: string) {75 return deriveSyncIdentityKeypair(masterKey, deviceId);76}7778function defaultDeviceName(): string {79 80 81 const ua = (typeof navigator !== "undefined" ? navigator.userAgent : "") || "";82 if (/Firefox/i.test(ua)) return "Firefox";83 if (/Edg\//i.test(ua)) return "Edge";84 if (/Chrome/i.test(ua)) return "Chrome";85 if (/Safari/i.test(ua)) return "Safari";86 return "Browser";87}8889function extensionDeviceType(): DeviceType {90 91 92 93 try {94 const native = (globalThis as unknown as { Capacitor?: { isNativePlatform?: () => boolean } }).Capacitor;95 if (native?.isNativePlatform?.()) {96 const platform = (globalThis as unknown as { Capacitor?: { getPlatform?: () => string } }).Capacitor?.getPlatform?.();97 if (platform === "ios") return "ios";98 return "android";99 }100 } catch { }101 return "extension";102}103104function generateUuidV4(): string {105 106 107 108 if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {109 return crypto.randomUUID();110 }111 const bytes = new Uint8Array(16);112 crypto.getRandomValues(bytes as Uint8Array<ArrayBuffer>);113 bytes[6] = (bytes[6] & 0x0f) | 0x40;114 bytes[8] = (bytes[8] & 0x3f) | 0x80;115 const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");116 return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;117}