api.js raw
1 /**
2 * API helper functions for ORLY Launcher admin endpoints
3 */
4
5 /**
6 * Get the API base URL (same as current page)
7 */
8 export function getApiBase() {
9 return window.location.origin;
10 }
11
12 /**
13 * Create NIP-98 authentication header
14 * @param {object} signer - The signer instance
15 * @param {string} pubkey - User's pubkey
16 * @param {string} method - HTTP method
17 * @param {string} url - Request URL
18 * @returns {Promise<string|null>} Base64 encoded auth header or null
19 */
20 export async function createNIP98Auth(signer, pubkey, method, url) {
21 if (!signer || !pubkey) {
22 return null;
23 }
24
25 try {
26 // Create unsigned auth event
27 const authEvent = {
28 kind: 27235,
29 created_at: Math.floor(Date.now() / 1000),
30 tags: [
31 ["u", url],
32 ["method", method.toUpperCase()],
33 ],
34 content: "",
35 };
36
37 // Sign using the signer
38 const signedEvent = await signer.signEvent(authEvent);
39
40 // Use URL-safe base64 encoding
41 const json = JSON.stringify(signedEvent);
42 const base64 = btoa(json).replace(/\+/g, '-').replace(/\//g, '_');
43 return base64;
44 } catch (error) {
45 console.error("createNIP98Auth error:", error);
46 return null;
47 }
48 }
49
50 /**
51 * Make an authenticated API request
52 * @param {string} path - API path
53 * @param {object} options - Fetch options
54 * @param {object} signer - Signer instance
55 * @param {string} pubkey - User's pubkey
56 * @returns {Promise<Response>}
57 */
58 async function authFetch(path, options = {}, signer, pubkey) {
59 const url = `${getApiBase()}${path}`;
60 const method = options.method || 'GET';
61 const authHeader = await createNIP98Auth(signer, pubkey, method, url);
62
63 const headers = {
64 ...options.headers,
65 };
66
67 if (authHeader) {
68 headers['Authorization'] = `Nostr ${authHeader}`;
69 }
70
71 return fetch(url, { ...options, headers });
72 }
73
74 /**
75 * Fetch launcher status
76 */
77 export async function fetchStatus(signer, pubkey) {
78 const response = await authFetch('/api/status', {}, signer, pubkey);
79 if (!response.ok) {
80 throw new Error(`Failed to fetch status: ${response.statusText}`);
81 }
82 return response.json();
83 }
84
85 /**
86 * Fetch launcher configuration
87 */
88 export async function fetchConfig(signer, pubkey) {
89 const response = await authFetch('/api/config', {}, signer, pubkey);
90 if (!response.ok) {
91 throw new Error(`Failed to fetch config: ${response.statusText}`);
92 }
93 return response.json();
94 }
95
96 /**
97 * Save launcher configuration
98 * @param {object} config - Configuration object to save
99 */
100 export async function saveConfig(signer, pubkey, config) {
101 const response = await authFetch('/api/config', {
102 method: 'POST',
103 headers: { 'Content-Type': 'application/json' },
104 body: JSON.stringify(config),
105 }, signer, pubkey);
106
107 if (!response.ok) {
108 const data = await response.json().catch(() => ({}));
109 throw new Error(data.message || `Save failed: ${response.statusText}`);
110 }
111 return response.json();
112 }
113
114 /**
115 * Fetch available binaries
116 */
117 export async function fetchBinaries(signer, pubkey) {
118 const response = await authFetch('/api/binaries', {}, signer, pubkey);
119 if (!response.ok) {
120 throw new Error(`Failed to fetch binaries: ${response.statusText}`);
121 }
122 return response.json();
123 }
124
125 /**
126 * Fetch available releases from official repo (proxied to avoid CORS)
127 */
128 export async function fetchReleases(signer, pubkey) {
129 const response = await authFetch('/api/releases', {}, signer, pubkey);
130 if (!response.ok) {
131 throw new Error(`Failed to fetch releases: ${response.statusText}`);
132 }
133 return response.json();
134 }
135
136 /**
137 * Update binaries from URLs
138 */
139 export async function updateBinaries(signer, pubkey, version, urls) {
140 const response = await authFetch('/api/update', {
141 method: 'POST',
142 headers: { 'Content-Type': 'application/json' },
143 body: JSON.stringify({ version, urls }),
144 }, signer, pubkey);
145
146 if (!response.ok) {
147 const data = await response.json();
148 throw new Error(data.message || `Update failed: ${response.statusText}`);
149 }
150 return response.json();
151 }
152
153 /**
154 * Restart all services
155 */
156 export async function restartServices(signer, pubkey) {
157 const response = await authFetch('/api/restart', {
158 method: 'POST',
159 }, signer, pubkey);
160
161 if (!response.ok) {
162 throw new Error(`Restart failed: ${response.statusText}`);
163 }
164 return response.json();
165 }
166
167 /**
168 * Restart a specific service with dependency handling
169 * @param {string} service - The service name (e.g., 'orly-db-badger', 'orly-acl-follows', 'orly')
170 */
171 export async function restartService(signer, pubkey, service) {
172 const response = await authFetch('/api/restart-service', {
173 method: 'POST',
174 headers: { 'Content-Type': 'application/json' },
175 body: JSON.stringify({ service }),
176 }, signer, pubkey);
177
178 if (!response.ok) {
179 const data = await response.json().catch(() => ({}));
180 throw new Error(data.message || `Restart failed: ${response.statusText}`);
181 }
182 return response.json();
183 }
184
185 /**
186 * Rollback to previous version
187 */
188 export async function rollbackVersion(signer, pubkey) {
189 const response = await authFetch('/api/rollback', {
190 method: 'POST',
191 }, signer, pubkey);
192
193 if (!response.ok) {
194 const data = await response.json();
195 throw new Error(data.message || `Rollback failed: ${response.statusText}`);
196 }
197 return response.json();
198 }
199
200 /**
201 * Start all services
202 */
203 export async function startServices(signer, pubkey) {
204 const response = await authFetch('/api/start-services', {
205 method: 'POST',
206 }, signer, pubkey);
207
208 if (!response.ok) {
209 const data = await response.json().catch(() => ({}));
210 throw new Error(data.message || `Start failed: ${response.statusText}`);
211 }
212 return response.json();
213 }
214
215 /**
216 * Stop all services
217 */
218 export async function stopServices(signer, pubkey) {
219 const response = await authFetch('/api/stop-services', {
220 method: 'POST',
221 }, signer, pubkey);
222
223 if (!response.ok) {
224 const data = await response.json().catch(() => ({}));
225 throw new Error(data.message || `Stop failed: ${response.statusText}`);
226 }
227 return response.json();
228 }
229
230 /**
231 * Start a specific service
232 * @param {string} service - The service name (e.g., 'orly-db', 'orly-acl', 'orly')
233 */
234 export async function startService(signer, pubkey, service) {
235 const response = await authFetch('/api/start-service', {
236 method: 'POST',
237 headers: { 'Content-Type': 'application/json' },
238 body: JSON.stringify({ service }),
239 }, signer, pubkey);
240
241 if (!response.ok) {
242 const data = await response.json().catch(() => ({}));
243 throw new Error(data.message || `Start failed: ${response.statusText}`);
244 }
245 return response.json();
246 }
247
248 /**
249 * Stop a specific service (and its dependents)
250 * @param {string} service - The service name (e.g., 'orly-db', 'orly-acl', 'orly')
251 */
252 export async function stopService(signer, pubkey, service) {
253 const response = await authFetch('/api/stop-service', {
254 method: 'POST',
255 headers: { 'Content-Type': 'application/json' },
256 body: JSON.stringify({ service }),
257 }, signer, pubkey);
258
259 if (!response.ok) {
260 const data = await response.json().catch(() => ({}));
261 throw new Error(data.message || `Stop failed: ${response.statusText}`);
262 }
263 return response.json();
264 }
265