A Chrome extension to quickly capture URLs into Semble Collections at https://semble.so
semble.so
at-proto
semble
chrome-extension
1/**
2 * Background Service Worker
3 * Handles session management and Semble API communication
4 */
5
6// Import Semble API client functions
7importScripts('../lib/atproto.js');
8
9// Session state
10let session = null;
11
12/**
13 * Initialize session on extension startup
14 */
15chrome.runtime.onStartup.addListener(async () => {
16 await loadSession();
17});
18
19/**
20 * Load session from storage
21 */
22async function loadSession() {
23 try {
24 const result = await chrome.storage.local.get(['session']);
25 if (result.session) {
26 session = result.session;
27 console.log('Session loaded from storage');
28 }
29 } catch (error) {
30 console.error('Failed to load session:', error);
31 }
32}
33
34/**
35 * Save session to storage
36 */
37async function saveSession(sessionData) {
38 try {
39 session = sessionData;
40 await chrome.storage.local.set({ session: sessionData });
41 console.log('Session saved to storage');
42 } catch (error) {
43 console.error('Failed to save session:', error);
44 throw error;
45 }
46}
47
48/**
49 * Clear session from storage
50 */
51async function clearSession() {
52 try {
53 session = null;
54 await chrome.storage.local.remove('session');
55 console.log('Session cleared');
56 } catch (error) {
57 console.error('Failed to clear session:', error);
58 }
59}
60
61/**
62 * Authenticate with Semble using Bluesky credentials
63 * @param {string} identifier - User handle
64 * @param {string} password - App password
65 * @param {string} [service] - Optional PDS service URL
66 */
67async function authenticate(identifier, password, service) {
68 try {
69 console.log('Authenticating with:', {
70 identifier,
71 hasPassword: !!password,
72 service: service || 'default (bsky.social)'
73 });
74
75 const sessionData = await createSession(identifier, password, service);
76 await saveSession(sessionData);
77
78 console.log('Authentication successful');
79 return { success: true, session: sessionData };
80 } catch (error) {
81 console.error('Authentication failed:', error);
82 return { success: false, error: error.message };
83 }
84}
85
86/**
87 * Ensure we have a valid session
88 * Refreshes token if expired
89 */
90async function ensureValidSession() {
91 if (!session) {
92 await loadSession();
93 }
94
95 if (!session) {
96 throw new Error('Not authenticated. Please log in.');
97 }
98
99 // TODO: Add token expiration check and refresh logic using refreshSession()
100 // For now, we'll assume the token is valid
101
102 return session;
103}
104
105/**
106 * Get user's collections from Semble
107 */
108async function getCollections() {
109 try {
110 const session = await ensureValidSession();
111 const collections = await listCollections(session.accessToken);
112 return { success: true, collections };
113 } catch (error) {
114 console.error('Failed to fetch collections:', error);
115 return { success: false, error: error.message };
116 }
117}
118
119/**
120 * Save URL card to Semble collection
121 */
122async function saveCard(url, metadata, note, collectionId) {
123 try {
124 const session = await ensureValidSession();
125
126 // Semble API handles everything in one call:
127 // - Fetches URL metadata
128 // - Creates URL card
129 // - Creates note card (if note provided)
130 // - Adds to collection(s)
131 // - Publishes to ATproto
132 const collectionIds = collectionId ? [collectionId] : [];
133
134 const result = await addUrlToLibrary(
135 session.accessToken,
136 url,
137 note,
138 collectionIds
139 );
140
141 console.log('Card saved to Semble:', result);
142
143 return {
144 success: true,
145 urlCardId: result.urlCardId,
146 noteCardId: result.noteCardId,
147 };
148 } catch (error) {
149 console.error('Failed to save card:', error);
150 return { success: false, error: error.message };
151 }
152}
153
154/**
155 * Handle messages from popup and content scripts
156 */
157chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
158 console.log('Background received message:', request);
159
160 switch (request.action) {
161 case 'authenticate':
162 authenticate(request.identifier, request.password, request.service)
163 .then(sendResponse);
164 return true; // Keep channel open for async response
165
166 case 'getSession':
167 ensureValidSession()
168 .then(session => sendResponse({ success: true, session }))
169 .catch(error => sendResponse({ success: false, error: error.message }));
170 return true;
171
172 case 'clearSession':
173 clearSession()
174 .then(() => sendResponse({ success: true }))
175 .catch(error => sendResponse({ success: false, error: error.message }));
176 return true;
177
178 case 'getCollections':
179 getCollections().then(sendResponse);
180 return true;
181
182 case 'saveCard':
183 saveCard(
184 request.url,
185 request.metadata,
186 request.note,
187 request.collectionId
188 ).then(sendResponse);
189 return true;
190
191 default:
192 console.warn('Unknown action:', request.action);
193 sendResponse({ success: false, error: 'Unknown action' });
194 }
195});
196
197// Load session on script initialization
198loadSession();