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