This repository has no description
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};