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