(READ ONLY) Margin is an open annotation layer for the internet. Powered by the AT Protocol.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375import type { MarginSession, TextSelector, Annotation } from './types';import { apiUrlItem } from './storage';
async function getApiUrl(): Promise<string> { return await apiUrlItem.getValue();}
async function getSessionCookie(): Promise<string | null> { try { const apiUrl = await getApiUrl(); const cookie = await browser.cookies.get({ url: apiUrl, name: 'margin_session', }); return cookie?.value || null; } catch (error) { console.error('Get cookie error:', error); return null; }}
export async function checkSession(): Promise<MarginSession> { try { const apiUrl = await getApiUrl(); const cookie = await getSessionCookie();
if (!cookie) return { authenticated: false };
const res = await fetch(`${apiUrl}/auth/session`, { headers: { 'X-Session-Token': cookie }, });
if (!res.ok) return { authenticated: false };
const sessionData = await res.json(); if (!sessionData.did || !sessionData.handle) return { authenticated: false };
return { authenticated: true, did: sessionData.did, handle: sessionData.handle, accessJwt: sessionData.accessJwt, refreshJwt: sessionData.refreshJwt, }; } catch (error) { console.error('Session check error:', error); return { authenticated: false }; }}
async function apiRequest(path: string, options: RequestInit = {}): Promise<Response> { const apiUrl = await getApiUrl(); const cookie = await getSessionCookie();
const headers: Record<string, string> = { 'Content-Type': 'application/json', ...(options.headers as Record<string, string>), };
if (cookie) { headers['X-Session-Token'] = cookie; }
const apiPath = path.startsWith('/api') ? path : `/api${path}`;
const response = await fetch(`${apiUrl}${apiPath}`, { ...options, headers, credentials: 'include', });
return response;}
async function hashUrl(rawUrl: string): Promise<string> { let toHash: string; try { const parsed = new URL(rawUrl); let host = parsed.host.toLowerCase(); if (host.startsWith('www.')) host = host.slice(4); let normalized = host + parsed.pathname; if (parsed.search) normalized += parsed.search; normalized = normalized.replace(/\/$/, ''); toHash = normalized; } catch { toHash = rawUrl; } const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(toHash)); return Array.from(new Uint8Array(buf)) .map((b) => b.toString(16).padStart(2, '0')) .join('');}
export async function getAnnotations( url: string, citedUrls: string[] = [], cacheBust: boolean = false) { try { const apiUrl = await getApiUrl(); const uniqueUrls = [...new Set([url, ...citedUrls])];
const hashes = await Promise.all(uniqueUrls.map(hashUrl));
const params = new URLSearchParams(); hashes.forEach((h) => params.append('h', h)); if (cacheBust) params.append('t', Date.now().toString());
const res = await fetch(`${apiUrl}/api/targets/hash?${params}`); const data = res.ok ? await res.json() : { annotations: [], highlights: [], bookmarks: [] };
const allItems: any[] = []; const seenIds = new Set<string>(); const items = [ ...(data.annotations || []), ...(data.highlights || []), ...(data.bookmarks || []), ]; items.forEach((item: any) => { const id = item.uri || item.id; if (id && !seenIds.has(id)) { seenIds.add(id); allItems.push(item); } });
return allItems; } catch (error) { console.error('Get annotations error:', error); return []; }}
export async function createAnnotation(data: { url: string; text: string; title?: string; selector?: TextSelector; tags?: string[];}): Promise<{ success: boolean; data?: Annotation; error?: string }> { try { const res = await apiRequest('/annotations', { method: 'POST', body: JSON.stringify({ url: data.url, text: data.text, title: data.title, selector: data.selector, tags: data.tags, }), });
if (!res.ok) { const error = await res.text(); return { success: false, error }; }
return { success: true, data: await res.json() }; } catch (error) { return { success: false, error: String(error) }; }}
export async function createBookmark(data: { url: string; title?: string; tags?: string[] }) { try { const res = await apiRequest('/bookmarks', { method: 'POST', body: JSON.stringify({ url: data.url, title: data.title, tags: data.tags }), });
if (!res.ok) { const error = await res.text(); return { success: false, error }; }
return { success: true, data: await res.json() }; } catch (error) { return { success: false, error: String(error) }; }}
export async function createHighlight(data: { url: string; title?: string; selector: TextSelector; color?: string; tags?: string[];}) { try { const res = await apiRequest('/highlights', { method: 'POST', body: JSON.stringify({ url: data.url, title: data.title, selector: data.selector, color: data.color, tags: data.tags, }), });
if (!res.ok) { const error = await res.text(); return { success: false, error }; }
return { success: true, data: await res.json() }; } catch (error) { return { success: false, error: String(error) }; }}
export async function updateHighlightTags(uri: string, tags: string[]) { try { const res = await apiRequest(`/highlights?uri=${encodeURIComponent(uri)}`, { method: 'PUT', body: JSON.stringify({ tags }), }); if (!res.ok) { const error = await res.text(); return { success: false, error }; } return { success: true }; } catch (error) { return { success: false, error: String(error) }; }}
export async function getUserBookmarks(did: string) { try { const res = await apiRequest(`/users/${did}/bookmarks`); if (!res.ok) return []; const data = await res.json(); return data.items || data || []; } catch (error) { console.error('Get bookmarks error:', error); return []; }}
export async function getUserHighlights(did: string) { try { const res = await apiRequest(`/users/${did}/highlights`); if (!res.ok) return []; const data = await res.json(); return data.items || data || []; } catch (error) { console.error('Get highlights error:', error); return []; }}
export async function getUserCollections(did: string) { try { const res = await apiRequest(`/collections?author=${encodeURIComponent(did)}`); if (!res.ok) return []; const data = await res.json(); return data.items || data || []; } catch (error) { console.error('Get collections error:', error); return []; }}
export async function addToCollection(collectionUri: string, annotationUri: string) { try { const res = await apiRequest(`/collections/${encodeURIComponent(collectionUri)}/items`, { method: 'POST', body: JSON.stringify({ annotationUri, position: 0 }), });
if (!res.ok) { const error = await res.text(); return { success: false, error }; }
return { success: true }; } catch (error) { return { success: false, error: String(error) }; }}
export async function getItemCollections(annotationUri: string): Promise<string[]> { try { const res = await apiRequest( `/collections/containing?uri=${encodeURIComponent(annotationUri)}` ); if (!res.ok) return []; const data = await res.json(); return Array.isArray(data) ? data : []; } catch (error) { console.error('Get item collections error:', error); return []; }}
export async function deleteHighlight(uri: string) { try { const rkey = (uri || '').split('/').pop(); if (!rkey) return { success: false, error: 'Invalid URI' };
const res = await apiRequest(`/highlights?rkey=${rkey}`, { method: 'DELETE', });
if (!res.ok) { const error = await res.text(); return { success: false, error }; }
return { success: true }; } catch (error) { return { success: false, error: String(error) }; }}
export async function getUserTags(did: string) { try { const res = await apiRequest(`/users/${did}/tags?limit=50`); if (!res.ok) return []; const data = await res.json(); return (data || []).map((t: { tag: string }) => t.tag); } catch (error) { console.error('Get user tags error:', error); return []; }}
export async function getTrendingTags() { try { const res = await apiRequest('/trending-tags?limit=50'); if (!res.ok) return []; const data = await res.json(); return (data || []).map((t: { tag: string }) => t.tag); } catch (error) { console.error('Get trending tags error:', error); return []; }}
export async function getReplies(uri: string) { try { const res = await apiRequest(`/annotations/${encodeURIComponent(uri)}/replies`); if (!res.ok) return []; const data = await res.json(); return data.items || data || []; } catch (error) { console.error('Get replies error:', error); return []; }}
export async function createReply(data: { parentUri: string; parentCid: string; rootUri: string; rootCid: string; text: string;}) { try { const res = await apiRequest('/replies', { method: 'POST', body: JSON.stringify(data), });
if (!res.ok) { const error = await res.text(); return { success: false, error }; }
return { success: true }; } catch (error) { return { success: false, error: String(error) }; }}