A Chrome extension to quickly capture URLs into Semble Collections at https://semble.so
semble.so
at-proto
semble
chrome-extension
1/**
2 * Semble API Client Library
3 * Handles authentication and communication with Semble backend
4 */
5
6const SEMBLE_API_URL = 'https://api.semble.so';
7// For local development: 'http://127.0.0.1:3000'
8
9/**
10 * Login with Bluesky app password to get Semble tokens
11 * @param {string} identifier - User handle (e.g., user.bsky.social)
12 * @param {string} password - Bluesky app password
13 * @param {string} [service] - Optional PDS service URL (e.g., https://bsky.social)
14 * @returns {Promise<{accessToken: string, refreshToken: string}>}
15 */
16async function createSession(identifier, password, service) {
17 const payload = {
18 identifier,
19 appPassword: password,
20 };
21
22 // Include service parameter if provided (for custom PDS servers)
23 if (service) {
24 payload.service = service;
25 }
26
27 const response = await fetch(`${SEMBLE_API_URL}/api/users/login/app-password`, {
28 method: 'POST',
29 headers: {
30 'Content-Type': 'application/json',
31 },
32 body: JSON.stringify(payload),
33 });
34
35 if (!response.ok) {
36 const error = await response.json().catch(() => ({}));
37
38 // Enhanced error handling
39 let errorMessage = error.message || response.statusText;
40
41 // Provide more specific error messages
42 if (response.status === 401) {
43 errorMessage = 'Invalid identifier or password';
44 } else if (response.status === 400) {
45 errorMessage = error.message || 'Invalid request. Please check your inputs.';
46 } else if (response.status === 500) {
47 errorMessage = 'Server error. Please try again later.';
48 } else if (response.status === 503) {
49 errorMessage = 'Service temporarily unavailable. Please try again later.';
50 }
51
52 throw new Error(`Authentication failed: ${errorMessage}`);
53 }
54
55 return await response.json();
56}
57
58/**
59 * Refresh Semble access token
60 * @param {string} refreshToken - Refresh token
61 * @returns {Promise<{accessToken: string, refreshToken: string}>}
62 */
63async function refreshSession(refreshToken) {
64 const response = await fetch(`${SEMBLE_API_URL}/api/users/oauth/refresh`, {
65 method: 'POST',
66 headers: {
67 'Content-Type': 'application/json',
68 },
69 body: JSON.stringify({
70 refreshToken,
71 }),
72 });
73
74 if (!response.ok) {
75 throw new Error('Token refresh failed');
76 }
77
78 return await response.json();
79}
80
81/**
82 * Get user's Semble collections
83 * @param {string} accessToken - Semble access token
84 * @returns {Promise<Array>}
85 */
86async function listCollections(accessToken) {
87 const response = await fetch(`${SEMBLE_API_URL}/api/collections`, {
88 headers: {
89 'Authorization': `Bearer ${accessToken}`,
90 },
91 });
92
93 if (!response.ok) {
94 const error = await response.json().catch(() => ({}));
95 throw new Error(`Failed to fetch collections: ${error.message || response.statusText}`);
96 }
97
98 const data = await response.json();
99 return data.collections || [];
100}
101
102/**
103 * Add URL to library via Semble API (creates card, fetches metadata, adds to collections)
104 * @param {string} accessToken - Semble access token
105 * @param {string} url - URL to add
106 * @param {string} [note] - Optional note
107 * @param {string[]} [collectionIds] - Optional collection IDs to add card to
108 * @returns {Promise<{urlCardId: string, noteCardId?: string}>}
109 */
110async function addUrlToLibrary(accessToken, url, note, collectionIds) {
111 const payload = {
112 url,
113 };
114
115 if (note && note.trim()) {
116 payload.note = note.trim();
117 }
118
119 if (collectionIds && collectionIds.length > 0) {
120 payload.collectionIds = collectionIds;
121 }
122
123 console.log('Adding URL to Semble library:', {
124 url,
125 hasNote: !!payload.note,
126 collectionCount: collectionIds?.length || 0,
127 });
128
129 const response = await fetch(`${SEMBLE_API_URL}/api/cards/library/urls`, {
130 method: 'POST',
131 headers: {
132 'Authorization': `Bearer ${accessToken}`,
133 'Content-Type': 'application/json',
134 },
135 body: JSON.stringify(payload),
136 });
137
138 if (!response.ok) {
139 const error = await response.json().catch(() => ({}));
140 console.error('Failed to add URL to library:', {
141 status: response.status,
142 statusText: response.statusText,
143 error: error,
144 });
145 throw new Error(`Failed to add URL: ${error.message || response.statusText}`);
146 }
147
148 const result = await response.json();
149 console.log('URL added to Semble successfully:', result);
150 return result;
151}
152
153// Legacy function names for compatibility - no longer used
154async function createRecord() {
155 throw new Error('createRecord is deprecated - use addUrlToLibrary instead');
156}
157
158async function createUrlCard() {
159 throw new Error('createUrlCard is deprecated - use addUrlToLibrary instead');
160}
161
162async function createNoteCard() {
163 throw new Error('createNoteCard is deprecated - note is now part of addUrlToLibrary');
164}
165
166async function createCollectionLink() {
167 throw new Error('createCollectionLink is deprecated - collectionIds is now part of addUrlToLibrary');
168}
169
170// Export functions for use in other scripts
171if (typeof module !== 'undefined' && module.exports) {
172 module.exports = {
173 createSession,
174 refreshSession,
175 listCollections,
176 addUrlToLibrary,
177 createRecord,
178 createUrlCard,
179 createNoteCard,
180 createCollectionLink,
181 };
182}