Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181import { browser } from "$app/environment";import { isDid, 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";const ORG_DIDS_KEY = "tangled.orgDids";const ORG_CONTROLLERS_KEY = "tangled.orgControllers";
// appview account cap parityexport const MAX_ACCOUNTS = 20;
export interface AuthAccount { did: Did; handle: string; // unix seconds; appview parity addedAt: number;}
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.//// org dids are passed in so that they're excluded here// rather than showing up in the account switcherexport const reconcileAccounts = ( stored: readonly Did[], meta: readonly AuthAccount[], orgDids: readonly Did[] = []): AuthAccount[] => { const orgSet = new Set(orgDids); const storedSet = new Set(stored); const known = new Set(meta.map((account) => account.did)); const ordered = meta.filter( (account) => storedSet.has(account.did) && !orgSet.has(account.did) ); for (const did of stored) { if (!known.has(did) && !orgSet.has(did)) { ordered.push({ did, handle: did, addedAt: Math.floor(Date.now() / 1000) }); } } return ordered;};
const isDidArray = (value: unknown): value is Did[] => Array.isArray(value) && value.every(isDid);
export const loadOrgDids = (): Did[] => { if (!browser) return []; try { const raw = localStorage.getItem(ORG_DIDS_KEY); if (!raw) return []; const parsed = JSON.parse(raw) as unknown; return isDidArray(parsed) ? parsed : []; } catch { return []; }};
export const loadOrgControllers = (): Record<string, Did> => { if (!browser) return {}; try { const raw = localStorage.getItem(ORG_CONTROLLERS_KEY); if (!raw) return {}; const parsed = JSON.parse(raw) as unknown; if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {}; const controllers: Record<string, Did> = {}; for (const [delegatedDid, controllerDid] of Object.entries(parsed)) { if (isDid(delegatedDid) && isDid(controllerDid)) { controllers[delegatedDid] = controllerDid; } } return controllers; } catch { return {}; }};
export const orgControllerFor = (did: Did): Did | undefined => loadOrgControllers()[did];
export const orgDidsControlledBy = ( controllers: Readonly<Record<string, Did>>, controllerDid: Did): Did[] => Object.entries(controllers) .filter(([, controller]) => controller === controllerDid) .map(([orgDid]) => orgDid as Did);
export const forgetOrgDid = (did: Did): void => { if (!browser) return; localStorage.setItem( ORG_DIDS_KEY, JSON.stringify(loadOrgDids().filter((orgDid) => orgDid !== did)) ); const { [did]: _dropped, ...controllers } = loadOrgControllers(); localStorage.setItem(ORG_CONTROLLERS_KEY, JSON.stringify(controllers));};
export const markOrgDid = (did: Did, controllerDid?: Did): void => { if (!browser) return; const current = loadOrgDids(); if (!current.includes(did)) { localStorage.setItem(ORG_DIDS_KEY, JSON.stringify([...current, did])); } if (controllerDid) { localStorage.setItem( ORG_CONTROLLERS_KEY, JSON.stringify({ ...loadOrgControllers(), [did]: controllerDid }) ); }};
// 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;};