import { browser } from "$app/environment"; import type { Did } from "@atcute/lexicons/syntax"; // atcute owns oauth sessions; this stores metadata/order and active-account cookies. export const CURRENT_DID_KEY = "tangled.currentDid"; export const CURRENT_HANDLE_KEY = "tangled.currentHandle"; const ACCOUNTS_KEY = "tangled.accounts"; // appview account cap parity export const MAX_ACCOUNTS = 20; export interface AuthAccount { did: Did; handle: string; // unix seconds; appview parity addedAt: number; } const isDid = (value: unknown): value is Did => typeof value === "string" && value.startsWith("did:"); const isAccount = (value: unknown): value is AuthAccount => !!value && typeof value === "object" && isDid((value as AuthAccount).did) && typeof (value as AuthAccount).handle === "string"; export const loadAccounts = (): AuthAccount[] => { if (!browser) return []; try { const raw = localStorage.getItem(ACCOUNTS_KEY); if (!raw) return []; const parsed = JSON.parse(raw) as unknown; if (!Array.isArray(parsed)) return []; return parsed.filter(isAccount).map((account) => ({ did: account.did, handle: account.handle, addedAt: typeof account.addedAt === "number" ? account.addedAt : 0 })); } catch { return []; } }; export const saveAccounts = (accounts: readonly AuthAccount[]): void => { if (!browser) return; localStorage.setItem(ACCOUNTS_KEY, JSON.stringify(accounts)); }; // stored sessions are authoritative, metadata only supplies order and handle export const reconcileAccounts = ( stored: readonly Did[], meta: readonly AuthAccount[] ): AuthAccount[] => { const storedSet = new Set(stored); const known = new Set(meta.map((account) => account.did)); const ordered = meta.filter((account) => storedSet.has(account.did)); for (const did of stored) { if (!known.has(did)) { ordered.push({ did, handle: did, addedAt: Math.floor(Date.now() / 1000) }); } } return ordered; }; // dedupe by did, preserve insertion order, and keep the original addedAt. export const upsertAccount = ( accounts: readonly AuthAccount[], account: AuthAccount ): AuthAccount[] => { const index = accounts.findIndex((existing) => existing.did === account.did); if (index >= 0) { const next = accounts.slice(); next[index] = { ...account, addedAt: accounts[index].addedAt }; return next; } if (accounts.length >= MAX_ACCOUNTS) return accounts.slice(); return [...accounts, account]; }; export const dropAccount = (accounts: readonly AuthAccount[], did: Did): AuthAccount[] => accounts.filter((account) => account.did !== did); export const persistActive = (did: string, handle: string): void => { if (!browser) return; localStorage.setItem(CURRENT_DID_KEY, did); localStorage.setItem(CURRENT_HANDLE_KEY, handle); const secure = location.protocol === "https:" ? "; secure" : ""; const attrs = `; path=/; max-age=31536000; samesite=lax${secure}`; document.cookie = `${CURRENT_DID_KEY}=${encodeURIComponent(did)}${attrs}`; document.cookie = `${CURRENT_HANDLE_KEY}=${encodeURIComponent(handle)}${attrs}`; }; export const clearActive = (): void => { if (!browser) return; localStorage.removeItem(CURRENT_DID_KEY); localStorage.removeItem(CURRENT_HANDLE_KEY); document.cookie = `${CURRENT_DID_KEY}=; path=/; max-age=0; samesite=lax`; document.cookie = `${CURRENT_HANDLE_KEY}=; path=/; max-age=0; samesite=lax`; }; export const readActiveDid = (): Did | null => { if (!browser) return null; const value = localStorage.getItem(CURRENT_DID_KEY); return isDid(value) ? value : null; };