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