This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / web / src / lib / auth / accounts.ts
3.6 kB 109 lines
1import { browser } from "$app/environment"; 2import type { Did } from "@atcute/lexicons/syntax"; 3 4// atcute owns oauth sessions; this stores metadata/order and active-account cookies. 5 6export const CURRENT_DID_KEY = "tangled.currentDid"; 7export const CURRENT_HANDLE_KEY = "tangled.currentHandle"; 8const ACCOUNTS_KEY = "tangled.accounts"; 9 10// appview account cap parity 11export const MAX_ACCOUNTS = 20; 12 13export interface AuthAccount { 14 did: Did; 15 handle: string; 16 avatar?: string; 17 // unix seconds; appview parity 18 addedAt: number; 19} 20 21const isDid = (value: unknown): value is Did => 22 typeof value === "string" && value.startsWith("did:"); 23 24const isAccount = (value: unknown): value is AuthAccount => 25 !!value && 26 typeof value === "object" && 27 isDid((value as AuthAccount).did) && 28 typeof (value as AuthAccount).handle === "string"; 29 30export const loadAccounts = (): AuthAccount[] => { 31 if (!browser) return []; 32 try { 33 const raw = localStorage.getItem(ACCOUNTS_KEY); 34 if (!raw) return []; 35 const parsed = JSON.parse(raw) as unknown; 36 if (!Array.isArray(parsed)) return []; 37 return parsed.filter(isAccount).map((account) => ({ 38 did: account.did, 39 handle: account.handle, 40 avatar: account.avatar, 41 addedAt: typeof account.addedAt === "number" ? account.addedAt : 0 42 })); 43 } catch { 44 return []; 45 } 46}; 47 48export const saveAccounts = (accounts: readonly AuthAccount[]): void => { 49 if (!browser) return; 50 localStorage.setItem(ACCOUNTS_KEY, JSON.stringify(accounts)); 51}; 52 53// stored sessions are authoritative; metadata supplies order, handle, and avatar. 54export const reconcileAccounts = ( 55 stored: readonly Did[], 56 meta: readonly AuthAccount[] 57): AuthAccount[] => { 58 const storedSet = new Set(stored); 59 const known = new Set(meta.map((account) => account.did)); 60 const ordered = meta.filter((account) => storedSet.has(account.did)); 61 for (const did of stored) { 62 if (!known.has(did)) { 63 ordered.push({ did, handle: did, addedAt: Math.floor(Date.now() / 1000) }); 64 } 65 } 66 return ordered; 67}; 68 69// dedupe by did, preserve insertion order, and keep the original addedAt. 70export const upsertAccount = ( 71 accounts: readonly AuthAccount[], 72 account: AuthAccount 73): AuthAccount[] => { 74 const index = accounts.findIndex((existing) => existing.did === account.did); 75 if (index >= 0) { 76 const next = accounts.slice(); 77 next[index] = { ...account, addedAt: accounts[index].addedAt }; 78 return next; 79 } 80 if (accounts.length >= MAX_ACCOUNTS) return accounts.slice(); 81 return [...accounts, account]; 82}; 83 84export const dropAccount = (accounts: readonly AuthAccount[], did: Did): AuthAccount[] => 85 accounts.filter((account) => account.did !== did); 86 87export const persistActive = (did: string, handle: string): void => { 88 if (!browser) return; 89 localStorage.setItem(CURRENT_DID_KEY, did); 90 localStorage.setItem(CURRENT_HANDLE_KEY, handle); 91 const secure = location.protocol === "https:" ? "; secure" : ""; 92 const attrs = `; path=/; max-age=31536000; samesite=lax${secure}`; 93 document.cookie = `${CURRENT_DID_KEY}=${encodeURIComponent(did)}${attrs}`; 94 document.cookie = `${CURRENT_HANDLE_KEY}=${encodeURIComponent(handle)}${attrs}`; 95}; 96 97export const clearActive = (): void => { 98 if (!browser) return; 99 localStorage.removeItem(CURRENT_DID_KEY); 100 localStorage.removeItem(CURRENT_HANDLE_KEY); 101 document.cookie = `${CURRENT_DID_KEY}=; path=/; max-age=0; samesite=lax`; 102 document.cookie = `${CURRENT_HANDLE_KEY}=; path=/; max-age=0; samesite=lax`; 103}; 104 105export const readActiveDid = (): Did | null => { 106 if (!browser) return null; 107 const value = localStorage.getItem(CURRENT_DID_KEY); 108 return isDid(value) ? value : null; 109};