Margin is an open annotation layer for the internet. Powered by the AT Protocol.
margin.at
extension
web
atproto
comments
1import type { MarginSession, TextSelector } from './types';
2import { apiUrlItem } from './storage';
3
4async function getApiUrl(): Promise<string> {
5 return await apiUrlItem.getValue();
6}
7
8async function getSessionCookie(): Promise<string | null> {
9 try {
10 const apiUrl = await getApiUrl();
11 const cookie = await browser.cookies.get({
12 url: apiUrl,
13 name: 'margin_session',
14 });
15 return cookie?.value || null;
16 } catch (error) {
17 console.error('Get cookie error:', error);
18 return null;
19 }
20}
21
22export async function checkSession(): Promise<MarginSession> {
23 try {
24 const apiUrl = await getApiUrl();
25 const cookie = await getSessionCookie();
26
27 if (!cookie) {
28 return { authenticated: false };
29 }
30
31 const res = await fetch(`${apiUrl}/auth/session`, {
32 headers: {
33 'X-Session-Token': cookie,
34 },
35 });
36
37 if (!res.ok) {
38 return { authenticated: false };
39 }
40
41 const sessionData = await res.json();
42
43 if (!sessionData.did || !sessionData.handle) {
44 return { authenticated: false };
45 }
46
47 return {
48 authenticated: true,
49 did: sessionData.did,
50 handle: sessionData.handle,
51 accessJwt: sessionData.accessJwt,
52 refreshJwt: sessionData.refreshJwt,
53 };
54 } catch (error) {
55 console.error('Session check error:', error);
56 return { authenticated: false };
57 }
58}
59
60async function apiRequest(path: string, options: RequestInit = {}): Promise<Response> {
61 const apiUrl = await getApiUrl();
62 const cookie = await getSessionCookie();
63
64 const headers: Record<string, string> = {
65 'Content-Type': 'application/json',
66 ...(options.headers as Record<string, string>),
67 };
68
69 if (cookie) {
70 headers['X-Session-Token'] = cookie;
71 }
72
73 const apiPath = path.startsWith('/api') ? path : `/api${path}`;
74
75 const response = await fetch(`${apiUrl}${apiPath}`, {
76 ...options,
77 headers,
78 credentials: 'include',
79 });
80
81 return response;
82}
83
84export async function getAnnotations(url: string, citedUrls: string[] = []) {
85 try {
86 const apiUrl = await getApiUrl();
87 const uniqueUrls = [...new Set([url, ...citedUrls])];
88
89 const fetchPromises = uniqueUrls.map(async (u) => {
90 try {
91 const res = await fetch(`${apiUrl}/api/targets?source=${encodeURIComponent(u)}`);
92 if (!res.ok) return { annotations: [], highlights: [], bookmarks: [] };
93 return await res.json();
94 } catch {
95 return { annotations: [], highlights: [], bookmarks: [] };
96 }
97 });
98
99 const results = await Promise.all(fetchPromises);
100 const allItems: any[] = [];
101 const seenIds = new Set<string>();
102
103 results.forEach((data) => {
104 const items = [
105 ...(data.annotations || []),
106 ...(data.highlights || []),
107 ...(data.bookmarks || []),
108 ];
109 items.forEach((item: any) => {
110 const id = item.uri || item.id;
111 if (id && !seenIds.has(id)) {
112 seenIds.add(id);
113 allItems.push(item);
114 }
115 });
116 });
117
118 return allItems;
119 } catch (error) {
120 console.error('Get annotations error:', error);
121 return [];
122 }
123}
124
125export async function createAnnotation(data: {
126 url: string;
127 text: string;
128 title?: string;
129 selector?: TextSelector;
130}) {
131 try {
132 const res = await apiRequest('/annotations', {
133 method: 'POST',
134 body: JSON.stringify({
135 url: data.url,
136 text: data.text,
137 title: data.title,
138 selector: data.selector,
139 }),
140 });
141
142 if (!res.ok) {
143 const error = await res.text();
144 return { success: false, error };
145 }
146
147 return { success: true, data: await res.json() };
148 } catch (error) {
149 return { success: false, error: String(error) };
150 }
151}
152
153export async function createBookmark(data: { url: string; title?: string }) {
154 try {
155 const res = await apiRequest('/bookmarks', {
156 method: 'POST',
157 body: JSON.stringify({ url: data.url, title: data.title }),
158 });
159
160 if (!res.ok) {
161 const error = await res.text();
162 return { success: false, error };
163 }
164
165 return { success: true, data: await res.json() };
166 } catch (error) {
167 return { success: false, error: String(error) };
168 }
169}
170
171export async function createHighlight(data: {
172 url: string;
173 title?: string;
174 selector: TextSelector;
175 color?: string;
176}) {
177 try {
178 const res = await apiRequest('/highlights', {
179 method: 'POST',
180 body: JSON.stringify({
181 url: data.url,
182 title: data.title,
183 selector: data.selector,
184 color: data.color,
185 }),
186 });
187
188 if (!res.ok) {
189 const error = await res.text();
190 return { success: false, error };
191 }
192
193 return { success: true, data: await res.json() };
194 } catch (error) {
195 return { success: false, error: String(error) };
196 }
197}
198
199export async function getUserBookmarks(did: string) {
200 try {
201 const res = await apiRequest(`/users/${did}/bookmarks`);
202 if (!res.ok) return [];
203 const data = await res.json();
204 return data.items || data || [];
205 } catch (error) {
206 console.error('Get bookmarks error:', error);
207 return [];
208 }
209}
210
211export async function getUserHighlights(did: string) {
212 try {
213 const res = await apiRequest(`/users/${did}/highlights`);
214 if (!res.ok) return [];
215 const data = await res.json();
216 return data.items || data || [];
217 } catch (error) {
218 console.error('Get highlights error:', error);
219 return [];
220 }
221}
222
223export async function getUserCollections(did: string) {
224 try {
225 const res = await apiRequest(`/collections?author=${encodeURIComponent(did)}`);
226 if (!res.ok) return [];
227 const data = await res.json();
228 return data.items || data || [];
229 } catch (error) {
230 console.error('Get collections error:', error);
231 return [];
232 }
233}
234
235export async function addToCollection(collectionUri: string, annotationUri: string) {
236 try {
237 const res = await apiRequest(`/collections/${encodeURIComponent(collectionUri)}/items`, {
238 method: 'POST',
239 body: JSON.stringify({ annotationUri, position: 0 }),
240 });
241
242 if (!res.ok) {
243 const error = await res.text();
244 return { success: false, error };
245 }
246
247 return { success: true };
248 } catch (error) {
249 return { success: false, error: String(error) };
250 }
251}
252
253export async function getItemCollections(annotationUri: string): Promise<string[]> {
254 try {
255 const res = await apiRequest(
256 `/collections/containing?uri=${encodeURIComponent(annotationUri)}`
257 );
258 if (!res.ok) return [];
259 const data = await res.json();
260 return Array.isArray(data) ? data : [];
261 } catch (error) {
262 console.error('Get item collections error:', error);
263 return [];
264 }
265}
266
267export async function getReplies(uri: string) {
268 try {
269 const res = await apiRequest(`/annotations/${encodeURIComponent(uri)}/replies`);
270 if (!res.ok) return [];
271 const data = await res.json();
272 return data.items || data || [];
273 } catch (error) {
274 console.error('Get replies error:', error);
275 return [];
276 }
277}
278
279export async function createReply(data: {
280 parentUri: string;
281 parentCid: string;
282 rootUri: string;
283 rootCid: string;
284 text: string;
285}) {
286 try {
287 const res = await apiRequest('/replies', {
288 method: 'POST',
289 body: JSON.stringify(data),
290 });
291
292 if (!res.ok) {
293 const error = await res.text();
294 return { success: false, error };
295 }
296
297 return { success: true };
298 } catch (error) {
299 return { success: false, error: String(error) };
300 }
301}