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 // 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};