api.js raw
1 /**
2 * API helper functions for ORLY relay management endpoints
3 */
4
5 import { getApiBase } from './config.js';
6
7 /**
8 * Create NIP-98 authentication header
9 * @param {object} signer - The signer instance
10 * @param {string} pubkey - User's pubkey
11 * @param {string} method - HTTP method
12 * @param {string} url - Request URL
13 * @returns {Promise<string|null>} Base64 encoded auth header or null
14 */
15 export async function createNIP98Auth(signer, pubkey, method, url) {
16 if (!signer || !pubkey) {
17 console.log("createNIP98Auth: No signer or pubkey available", { hasSigner: !!signer, hasPubkey: !!pubkey });
18 return null;
19 }
20
21 try {
22 // Create unsigned auth event
23 const authEvent = {
24 kind: 27235,
25 created_at: Math.floor(Date.now() / 1000),
26 tags: [
27 ["u", url],
28 ["method", method.toUpperCase()],
29 ],
30 content: "",
31 };
32
33 console.log("createNIP98Auth: Signing event for", method, url);
34
35 // Sign using the signer
36 const signedEvent = await signer.signEvent(authEvent);
37 console.log("createNIP98Auth: Signed event:", {
38 id: signedEvent.id,
39 pubkey: signedEvent.pubkey,
40 kind: signedEvent.kind,
41 created_at: signedEvent.created_at,
42 tags: signedEvent.tags,
43 hasSig: !!signedEvent.sig
44 });
45
46 // Use standard base64 encoding per BUD-01/NIP-98 spec
47 const json = JSON.stringify(signedEvent);
48 const base64 = btoa(json);
49 return base64;
50 } catch (error) {
51 console.error("createNIP98Auth: Error:", error);
52 return null;
53 }
54 }
55
56 /**
57 * Fetch user role from the relay
58 * @param {object} signer - The signer instance
59 * @param {string} pubkey - User's pubkey
60 * @returns {Promise<string>} User role
61 */
62 export async function fetchUserRole(signer, pubkey) {
63 try {
64 const url = `${getApiBase()}/api/role`;
65 const authHeader = await createNIP98Auth(signer, pubkey, "GET", url);
66 const response = await fetch(url, {
67 headers: authHeader ? { Authorization: `Nostr ${authHeader}` } : {},
68 });
69 if (response.ok) {
70 const data = await response.json();
71 return data.role || "";
72 }
73 } catch (error) {
74 console.error("Error fetching user role:", error);
75 }
76 return "";
77 }
78
79 /**
80 * Fetch ACL mode from the relay
81 * @returns {Promise<string>} ACL mode
82 */
83 export async function fetchACLMode() {
84 try {
85 const response = await fetch(`${getApiBase()}/api/acl-mode`);
86 if (response.ok) {
87 const data = await response.json();
88 return data.mode || "";
89 }
90 } catch (error) {
91 console.error("Error fetching ACL mode:", error);
92 }
93 return "";
94 }
95
96 // ==================== Sprocket API ====================
97
98 /**
99 * Load sprocket configuration
100 * @param {object} signer - The signer instance
101 * @param {string} pubkey - User's pubkey
102 * @returns {Promise<object>} Sprocket config data
103 */
104 export async function loadSprocketConfig(signer, pubkey) {
105 const url = `${getApiBase()}/api/sprocket/config`;
106 const authHeader = await createNIP98Auth(signer, pubkey, "GET", url);
107 const response = await fetch(url, {
108 headers: authHeader ? { Authorization: `Nostr ${authHeader}` } : {},
109 });
110 if (!response.ok) throw new Error(`Failed to load config: ${response.statusText}`);
111 return await response.json();
112 }
113
114 /**
115 * Load sprocket status
116 * @param {object} signer - The signer instance
117 * @param {string} pubkey - User's pubkey
118 * @returns {Promise<object>} Sprocket status data
119 */
120 export async function loadSprocketStatus(signer, pubkey) {
121 const url = `${getApiBase()}/api/sprocket/status`;
122 const authHeader = await createNIP98Auth(signer, pubkey, "GET", url);
123 const response = await fetch(url, {
124 headers: authHeader ? { Authorization: `Nostr ${authHeader}` } : {},
125 });
126 if (!response.ok) throw new Error(`Failed to load status: ${response.statusText}`);
127 return await response.json();
128 }
129
130 /**
131 * Load sprocket script
132 * @param {object} signer - The signer instance
133 * @param {string} pubkey - User's pubkey
134 * @returns {Promise<string>} Sprocket script content
135 */
136 export async function loadSprocketScript(signer, pubkey) {
137 const url = `${getApiBase()}/api/sprocket`;
138 const authHeader = await createNIP98Auth(signer, pubkey, "GET", url);
139 const response = await fetch(url, {
140 headers: authHeader ? { Authorization: `Nostr ${authHeader}` } : {},
141 });
142 if (response.status === 404) return "";
143 if (!response.ok) throw new Error(`Failed to load sprocket: ${response.statusText}`);
144 return await response.text();
145 }
146
147 /**
148 * Save sprocket script
149 * @param {object} signer - The signer instance
150 * @param {string} pubkey - User's pubkey
151 * @param {string} script - Script content
152 * @returns {Promise<object>} Save result
153 */
154 export async function saveSprocketScript(signer, pubkey, script) {
155 const url = `${getApiBase()}/api/sprocket`;
156 const authHeader = await createNIP98Auth(signer, pubkey, "PUT", url);
157 const response = await fetch(url, {
158 method: "PUT",
159 headers: {
160 "Content-Type": "text/plain",
161 ...(authHeader ? { Authorization: `Nostr ${authHeader}` } : {}),
162 },
163 body: script,
164 });
165 if (!response.ok) throw new Error(`Failed to save: ${response.statusText}`);
166 return await response.json();
167 }
168
169 /**
170 * Restart sprocket
171 * @param {object} signer - The signer instance
172 * @param {string} pubkey - User's pubkey
173 * @returns {Promise<object>} Restart result
174 */
175 export async function restartSprocket(signer, pubkey) {
176 const url = `${getApiBase()}/api/sprocket/restart`;
177 const authHeader = await createNIP98Auth(signer, pubkey, "POST", url);
178 const response = await fetch(url, {
179 method: "POST",
180 headers: authHeader ? { Authorization: `Nostr ${authHeader}` } : {},
181 });
182 if (!response.ok) throw new Error(`Failed to restart: ${response.statusText}`);
183 return await response.json();
184 }
185
186 /**
187 * Delete sprocket
188 * @param {object} signer - The signer instance
189 * @param {string} pubkey - User's pubkey
190 * @returns {Promise<object>} Delete result
191 */
192 export async function deleteSprocket(signer, pubkey) {
193 const url = `${getApiBase()}/api/sprocket`;
194 const authHeader = await createNIP98Auth(signer, pubkey, "DELETE", url);
195 const response = await fetch(url, {
196 method: "DELETE",
197 headers: authHeader ? { Authorization: `Nostr ${authHeader}` } : {},
198 });
199 if (!response.ok) throw new Error(`Failed to delete: ${response.statusText}`);
200 return await response.json();
201 }
202
203 /**
204 * Load sprocket versions
205 * @param {object} signer - The signer instance
206 * @param {string} pubkey - User's pubkey
207 * @returns {Promise<Array>} Version list
208 */
209 export async function loadSprocketVersions(signer, pubkey) {
210 const url = `${getApiBase()}/api/sprocket/versions`;
211 const authHeader = await createNIP98Auth(signer, pubkey, "GET", url);
212 const response = await fetch(url, {
213 headers: authHeader ? { Authorization: `Nostr ${authHeader}` } : {},
214 });
215 if (!response.ok) throw new Error(`Failed to load versions: ${response.statusText}`);
216 return await response.json();
217 }
218
219 /**
220 * Load specific sprocket version
221 * @param {object} signer - The signer instance
222 * @param {string} pubkey - User's pubkey
223 * @param {string} version - Version filename
224 * @returns {Promise<string>} Version content
225 */
226 export async function loadSprocketVersion(signer, pubkey, version) {
227 const url = `${getApiBase()}/api/sprocket/versions/${encodeURIComponent(version)}`;
228 const authHeader = await createNIP98Auth(signer, pubkey, "GET", url);
229 const response = await fetch(url, {
230 headers: authHeader ? { Authorization: `Nostr ${authHeader}` } : {},
231 });
232 if (!response.ok) throw new Error(`Failed to load version: ${response.statusText}`);
233 return await response.text();
234 }
235
236 /**
237 * Delete sprocket version
238 * @param {object} signer - The signer instance
239 * @param {string} pubkey - User's pubkey
240 * @param {string} filename - Version filename
241 * @returns {Promise<object>} Delete result
242 */
243 export async function deleteSprocketVersion(signer, pubkey, filename) {
244 const url = `${getApiBase()}/api/sprocket/versions/${encodeURIComponent(filename)}`;
245 const authHeader = await createNIP98Auth(signer, pubkey, "DELETE", url);
246 const response = await fetch(url, {
247 method: "DELETE",
248 headers: authHeader ? { Authorization: `Nostr ${authHeader}` } : {},
249 });
250 if (!response.ok) throw new Error(`Failed to delete version: ${response.statusText}`);
251 return await response.json();
252 }
253
254 /**
255 * Upload sprocket script file
256 * @param {object} signer - The signer instance
257 * @param {string} pubkey - User's pubkey
258 * @param {File} file - File to upload
259 * @returns {Promise<object>} Upload result
260 */
261 export async function uploadSprocketScript(signer, pubkey, file) {
262 const content = await file.text();
263 return await saveSprocketScript(signer, pubkey, content);
264 }
265
266 // ==================== Policy API ====================
267
268 /**
269 * Load policy configuration
270 * @param {object} signer - The signer instance
271 * @param {string} pubkey - User's pubkey
272 * @returns {Promise<object>} Policy config
273 */
274 export async function loadPolicyConfig(signer, pubkey) {
275 const url = `${getApiBase()}/api/policy/config`;
276 const authHeader = await createNIP98Auth(signer, pubkey, "GET", url);
277 const response = await fetch(url, {
278 headers: authHeader ? { Authorization: `Nostr ${authHeader}` } : {},
279 });
280 if (!response.ok) throw new Error(`Failed to load policy config: ${response.statusText}`);
281 return await response.json();
282 }
283
284 /**
285 * Load policy JSON
286 * @param {object} signer - The signer instance
287 * @param {string} pubkey - User's pubkey
288 * @returns {Promise<object>} Policy JSON
289 */
290 export async function loadPolicy(signer, pubkey) {
291 const url = `${getApiBase()}/api/policy`;
292 const authHeader = await createNIP98Auth(signer, pubkey, "GET", url);
293 const response = await fetch(url, {
294 headers: authHeader ? { Authorization: `Nostr ${authHeader}` } : {},
295 });
296 if (!response.ok) throw new Error(`Failed to load policy: ${response.statusText}`);
297 return await response.json();
298 }
299
300 /**
301 * Validate policy JSON
302 * @param {object} signer - The signer instance
303 * @param {string} pubkey - User's pubkey
304 * @param {string} policyJson - Policy JSON string
305 * @returns {Promise<object>} Validation result
306 */
307 export async function validatePolicy(signer, pubkey, policyJson) {
308 const url = `${getApiBase()}/api/policy/validate`;
309 const authHeader = await createNIP98Auth(signer, pubkey, "POST", url);
310 const response = await fetch(url, {
311 method: "POST",
312 headers: {
313 "Content-Type": "application/json",
314 ...(authHeader ? { Authorization: `Nostr ${authHeader}` } : {}),
315 },
316 body: policyJson,
317 });
318 return await response.json();
319 }
320
321 /**
322 * Fetch policy follows whitelist
323 * @param {object} signer - The signer instance
324 * @param {string} pubkey - User's pubkey
325 * @returns {Promise<Array>} List of followed pubkeys
326 */
327 export async function fetchPolicyFollows(signer, pubkey) {
328 const url = `${getApiBase()}/api/policy/follows`;
329 const authHeader = await createNIP98Auth(signer, pubkey, "GET", url);
330 const response = await fetch(url, {
331 headers: authHeader ? { Authorization: `Nostr ${authHeader}` } : {},
332 });
333 if (!response.ok) throw new Error(`Failed to fetch follows: ${response.statusText}`);
334 const data = await response.json();
335 return data.follows || [];
336 }
337
338 // ==================== Relay Info API ====================
339
340 /**
341 * Fetch relay info document (NIP-11)
342 * @returns {Promise<object>} Relay info including version
343 */
344 export async function fetchRelayInfo() {
345 try {
346 const response = await fetch(getApiBase(), {
347 headers: {
348 Accept: "application/nostr+json",
349 },
350 });
351 if (response.ok) {
352 return await response.json();
353 }
354 } catch (error) {
355 console.error("Error fetching relay info:", error);
356 }
357 return null;
358 }
359
360 // ==================== Export/Import API ====================
361
362 /**
363 * Export events
364 * @param {object} signer - The signer instance
365 * @param {string} pubkey - User's pubkey
366 * @param {Array} authorPubkeys - Filter by authors (empty for all)
367 * @returns {Promise<Blob>} JSONL blob
368 */
369 export async function exportEvents(signer, pubkey, authorPubkeys = []) {
370 const url = `${getApiBase()}/api/export`;
371 const authHeader = await createNIP98Auth(signer, pubkey, "POST", url);
372
373 const response = await fetch(url, {
374 method: "POST",
375 headers: {
376 "Content-Type": "application/json",
377 ...(authHeader ? { Authorization: `Nostr ${authHeader}` } : {}),
378 },
379 body: JSON.stringify({ pubkeys: authorPubkeys }),
380 });
381
382 if (!response.ok) throw new Error(`Export failed: ${response.statusText}`);
383 return await response.blob();
384 }
385
386 /**
387 * Import events from file
388 * @param {object} signer - The signer instance
389 * @param {string} pubkey - User's pubkey
390 * @param {File} file - JSONL file to import
391 * @returns {Promise<object>} Import result
392 */
393 export async function importEvents(signer, pubkey, file) {
394 const url = `${getApiBase()}/api/import`;
395 const authHeader = await createNIP98Auth(signer, pubkey, "POST", url);
396
397 const formData = new FormData();
398 formData.append("file", file);
399
400 const response = await fetch(url, {
401 method: "POST",
402 headers: authHeader ? { Authorization: `Nostr ${authHeader}` } : {},
403 body: formData,
404 });
405
406 if (!response.ok) throw new Error(`Import failed: ${response.statusText}`);
407 return await response.json();
408 }
409
410 // ==================== WireGuard API ====================
411
412 /**
413 * Fetch WireGuard status
414 * @returns {Promise<object>} WireGuard status
415 */
416 export async function fetchWireGuardStatus() {
417 try {
418 const response = await fetch(`${getApiBase()}/api/wireguard/status`);
419 if (response.ok) {
420 return await response.json();
421 }
422 } catch (error) {
423 console.error("Error fetching WireGuard status:", error);
424 }
425 return { wireguard_enabled: false, bunker_enabled: false, available: false };
426 }
427
428 /**
429 * Get WireGuard configuration for the authenticated user
430 * @param {object} signer - The signer instance
431 * @param {string} pubkey - User's pubkey
432 * @returns {Promise<object>} WireGuard config
433 */
434 export async function getWireGuardConfig(signer, pubkey) {
435 const url = `${getApiBase()}/api/wireguard/config`;
436 const authHeader = await createNIP98Auth(signer, pubkey, "GET", url);
437 const response = await fetch(url, {
438 headers: authHeader ? { Authorization: `Nostr ${authHeader}` } : {},
439 });
440 if (!response.ok) {
441 const error = await response.text();
442 throw new Error(error || `Failed to get WireGuard config: ${response.statusText}`);
443 }
444 return await response.json();
445 }
446
447 /**
448 * Regenerate WireGuard keypair for the authenticated user
449 * @param {object} signer - The signer instance
450 * @param {string} pubkey - User's pubkey
451 * @returns {Promise<object>} Regeneration result
452 */
453 export async function regenerateWireGuard(signer, pubkey) {
454 const url = `${getApiBase()}/api/wireguard/regenerate`;
455 const authHeader = await createNIP98Auth(signer, pubkey, "POST", url);
456 const response = await fetch(url, {
457 method: "POST",
458 headers: authHeader ? { Authorization: `Nostr ${authHeader}` } : {},
459 });
460 if (!response.ok) {
461 const error = await response.text();
462 throw new Error(error || `Failed to regenerate WireGuard: ${response.statusText}`);
463 }
464 return await response.json();
465 }
466
467
468 /**
469 * Get WireGuard audit log (revoked keys and access attempts)
470 * @param {object} signer - The signer instance
471 * @param {string} pubkey - User's pubkey
472 * @returns {Promise<object>} Audit data with revoked_keys and access_logs
473 */
474 export async function getWireGuardAudit(signer, pubkey) {
475 const url = `${getApiBase()}/api/wireguard/audit`;
476 const authHeader = await createNIP98Auth(signer, pubkey, "GET", url);
477 const response = await fetch(url, {
478 headers: authHeader ? { Authorization: `Nostr ${authHeader}` } : {},
479 });
480 if (!response.ok) {
481 const error = await response.text();
482 throw new Error(error || `Failed to get audit log: ${response.statusText}`);
483 }
484 return await response.json();
485 }
486
487 // ==================== NRC (Nostr Relay Connect) API ====================
488
489 /**
490 * Get NRC configuration status (no auth required)
491 * @returns {Promise<object>} NRC config status
492 */
493 export async function fetchNRCConfig() {
494 const apiBase = getApiBase();
495 console.log("[api] fetchNRCConfig using base URL:", apiBase);
496 try {
497 const response = await fetch(`${apiBase}/api/nrc/config`);
498 if (response.ok) {
499 return await response.json();
500 }
501 } catch (error) {
502 console.error("Error fetching NRC config:", error);
503 }
504 return { enabled: false, badger_required: true };
505 }
506
507 /**
508 * Get all NRC connections
509 * @param {object} signer - The signer instance
510 * @param {string} pubkey - User's pubkey
511 * @returns {Promise<object>} Connections list and config
512 */
513 export async function fetchNRCConnections(signer, pubkey) {
514 const url = `${getApiBase()}/api/nrc/connections`;
515 const authHeader = await createNIP98Auth(signer, pubkey, "GET", url);
516 const response = await fetch(url, {
517 headers: authHeader ? { Authorization: `Nostr ${authHeader}` } : {},
518 });
519 if (!response.ok) {
520 const error = await response.text();
521 throw new Error(error || `Failed to get NRC connections: ${response.statusText}`);
522 }
523 return await response.json();
524 }
525
526 /**
527 * Create a new NRC connection
528 * @param {object} signer - The signer instance
529 * @param {string} pubkey - User's pubkey
530 * @param {string} label - Connection label
531 * @returns {Promise<object>} Created connection with URI
532 */
533 export async function createNRCConnection(signer, pubkey, label) {
534 const url = `${getApiBase()}/api/nrc/connections`;
535 const authHeader = await createNIP98Auth(signer, pubkey, "POST", url);
536 const response = await fetch(url, {
537 method: "POST",
538 headers: {
539 "Content-Type": "application/json",
540 ...(authHeader ? { Authorization: `Nostr ${authHeader}` } : {}),
541 },
542 body: JSON.stringify({ label }),
543 });
544 if (!response.ok) {
545 const error = await response.text();
546 throw new Error(error || `Failed to create NRC connection: ${response.statusText}`);
547 }
548 return await response.json();
549 }
550
551 /**
552 * Delete an NRC connection
553 * @param {object} signer - The signer instance
554 * @param {string} pubkey - User's pubkey
555 * @param {string} connId - Connection ID to delete
556 * @returns {Promise<object>} Delete result
557 */
558 export async function deleteNRCConnection(signer, pubkey, connId) {
559 const url = `${getApiBase()}/api/nrc/connections/${connId}`;
560 const authHeader = await createNIP98Auth(signer, pubkey, "DELETE", url);
561 const response = await fetch(url, {
562 method: "DELETE",
563 headers: authHeader ? { Authorization: `Nostr ${authHeader}` } : {},
564 });
565 if (!response.ok) {
566 const error = await response.text();
567 throw new Error(error || `Failed to delete NRC connection: ${response.statusText}`);
568 }
569 return await response.json();
570 }
571
572 /**
573 * Get connection URI for an NRC connection
574 * @param {object} signer - The signer instance
575 * @param {string} pubkey - User's pubkey
576 * @param {string} connId - Connection ID
577 * @returns {Promise<object>} Connection URI
578 */
579 export async function getNRCConnectionURI(signer, pubkey, connId) {
580 const url = `${getApiBase()}/api/nrc/connections/${connId}/uri`;
581 const authHeader = await createNIP98Auth(signer, pubkey, "GET", url);
582 const response = await fetch(url, {
583 headers: authHeader ? { Authorization: `Nostr ${authHeader}` } : {},
584 });
585 if (!response.ok) {
586 const error = await response.text();
587 throw new Error(error || `Failed to get NRC URI: ${response.statusText}`);
588 }
589 return await response.json();
590 }
591
592
593