relay-admin.service.ts raw

   1  /**
   2   * Relay Admin Service
   3   *
   4   * Provides NIP-98 authenticated HTTP calls to ORLY relay management endpoints.
   5   * Only works when smesh is served from the same origin as the relay (embedded mode).
   6   */
   7  import client from '@/services/client.service'
   8  
   9  class RelayAdminService {
  10    static instance: RelayAdminService
  11  
  12    constructor() {
  13      if (!RelayAdminService.instance) {
  14        RelayAdminService.instance = this
  15      }
  16      return RelayAdminService.instance
  17    }
  18  
  19    private getApiBase(): string {
  20      return window.location.origin
  21    }
  22  
  23    private async authGet(path: string): Promise<Response> {
  24      const url = `${this.getApiBase()}${path}`
  25      const auth = await client.signHttpAuth(url, 'GET')
  26      return fetch(url, { headers: { Authorization: auth } })
  27    }
  28  
  29    private async authPost(path: string, body?: BodyInit, contentType?: string): Promise<Response> {
  30      const url = `${this.getApiBase()}${path}`
  31      const auth = await client.signHttpAuth(url, 'POST')
  32      const headers: Record<string, string> = { Authorization: auth }
  33      if (contentType) headers['Content-Type'] = contentType
  34      return fetch(url, { method: 'POST', headers, body })
  35    }
  36  
  37    private async authPut(path: string, body: BodyInit, contentType: string): Promise<Response> {
  38      const url = `${this.getApiBase()}${path}`
  39      const auth = await client.signHttpAuth(url, 'PUT')
  40      return fetch(url, {
  41        method: 'PUT',
  42        headers: { Authorization: auth, 'Content-Type': contentType },
  43        body
  44      })
  45    }
  46  
  47    private async authDelete(path: string): Promise<Response> {
  48      const url = `${this.getApiBase()}${path}`
  49      const auth = await client.signHttpAuth(url, 'DELETE')
  50      return fetch(url, { method: 'DELETE', headers: { Authorization: auth } })
  51    }
  52  
  53    // ==================== Embedded Mode Detection ====================
  54  
  55    async isEmbeddedInRelay(): Promise<boolean> {
  56      try {
  57        const res = await fetch(this.getApiBase(), {
  58          headers: { Accept: 'application/nostr+json' }
  59        })
  60        if (!res.ok) return false
  61        const data = await res.json()
  62        return !!(data && data.name)
  63      } catch {
  64        return false
  65      }
  66    }
  67  
  68    // ==================== Role & ACL ====================
  69  
  70    async fetchUserRole(): Promise<string> {
  71      try {
  72        const res = await this.authGet('/api/role')
  73        if (res.ok) {
  74          const data = await res.json()
  75          return data.role || ''
  76        }
  77      } catch (e) {
  78        console.error('fetchUserRole error:', e)
  79      }
  80      return ''
  81    }
  82  
  83    async fetchACLMode(): Promise<string> {
  84      try {
  85        const res = await fetch(`${this.getApiBase()}/api/acl-mode`)
  86        if (res.ok) {
  87          const data = await res.json()
  88          return data.mode || ''
  89        }
  90      } catch (e) {
  91        console.error('fetchACLMode error:', e)
  92      }
  93      return ''
  94    }
  95  
  96    // ==================== Relay Info (NIP-11) ====================
  97  
  98    async fetchRelayInfo(): Promise<Record<string, unknown> | null> {
  99      try {
 100        const res = await fetch(this.getApiBase(), {
 101          headers: { Accept: 'application/nostr+json' }
 102        })
 103        if (res.ok) return await res.json()
 104      } catch (e) {
 105        console.error('fetchRelayInfo error:', e)
 106      }
 107      return null
 108    }
 109  
 110    // ==================== Sprocket API ====================
 111  
 112    async loadSprocketConfig(): Promise<Record<string, unknown>> {
 113      const res = await this.authGet('/api/sprocket/config')
 114      if (!res.ok) throw new Error(`Failed to load config: ${res.statusText}`)
 115      return await res.json()
 116    }
 117  
 118    async loadSprocketStatus(): Promise<Record<string, unknown>> {
 119      const res = await this.authGet('/api/sprocket/status')
 120      if (!res.ok) throw new Error(`Failed to load status: ${res.statusText}`)
 121      return await res.json()
 122    }
 123  
 124    async loadSprocketScript(): Promise<string> {
 125      const res = await this.authGet('/api/sprocket')
 126      if (res.status === 404) return ''
 127      if (!res.ok) throw new Error(`Failed to load sprocket: ${res.statusText}`)
 128      return await res.text()
 129    }
 130  
 131    async saveSprocketScript(script: string): Promise<Record<string, unknown>> {
 132      const res = await this.authPut('/api/sprocket', script, 'text/plain')
 133      if (!res.ok) throw new Error(`Failed to save: ${res.statusText}`)
 134      return await res.json()
 135    }
 136  
 137    async restartSprocket(): Promise<Record<string, unknown>> {
 138      const res = await this.authPost('/api/sprocket/restart')
 139      if (!res.ok) throw new Error(`Failed to restart: ${res.statusText}`)
 140      return await res.json()
 141    }
 142  
 143    async deleteSprocket(): Promise<Record<string, unknown>> {
 144      const res = await this.authDelete('/api/sprocket')
 145      if (!res.ok) throw new Error(`Failed to delete: ${res.statusText}`)
 146      return await res.json()
 147    }
 148  
 149    async loadSprocketVersions(): Promise<Array<Record<string, unknown>>> {
 150      const res = await this.authGet('/api/sprocket/versions')
 151      if (!res.ok) throw new Error(`Failed to load versions: ${res.statusText}`)
 152      return await res.json()
 153    }
 154  
 155    async loadSprocketVersion(version: string): Promise<string> {
 156      const res = await this.authGet(`/api/sprocket/versions/${encodeURIComponent(version)}`)
 157      if (!res.ok) throw new Error(`Failed to load version: ${res.statusText}`)
 158      return await res.text()
 159    }
 160  
 161    async deleteSprocketVersion(filename: string): Promise<Record<string, unknown>> {
 162      const res = await this.authDelete(`/api/sprocket/versions/${encodeURIComponent(filename)}`)
 163      if (!res.ok) throw new Error(`Failed to delete version: ${res.statusText}`)
 164      return await res.json()
 165    }
 166  
 167    // ==================== Policy API ====================
 168  
 169    async loadPolicyConfig(): Promise<Record<string, unknown>> {
 170      const res = await this.authGet('/api/policy/config')
 171      if (!res.ok) throw new Error(`Failed to load policy config: ${res.statusText}`)
 172      return await res.json()
 173    }
 174  
 175    async loadPolicy(): Promise<Record<string, unknown>> {
 176      const res = await this.authGet('/api/policy')
 177      if (!res.ok) throw new Error(`Failed to load policy: ${res.statusText}`)
 178      return await res.json()
 179    }
 180  
 181    async validatePolicy(policyJson: string): Promise<Record<string, unknown>> {
 182      const res = await this.authPost('/api/policy/validate', policyJson, 'application/json')
 183      return await res.json()
 184    }
 185  
 186    async fetchPolicyFollows(): Promise<string[]> {
 187      const res = await this.authGet('/api/policy/follows')
 188      if (!res.ok) throw new Error(`Failed to fetch follows: ${res.statusText}`)
 189      const data = await res.json()
 190      return data.follows || []
 191    }
 192  
 193    // ==================== Export/Import API ====================
 194  
 195    async exportEvents(authorPubkeys: string[] = []): Promise<Blob> {
 196      const res = await this.authPost(
 197        '/api/export',
 198        JSON.stringify({ pubkeys: authorPubkeys }),
 199        'application/json'
 200      )
 201      if (!res.ok) throw new Error(`Export failed: ${res.statusText}`)
 202      return await res.blob()
 203    }
 204  
 205    async importEvents(file: File): Promise<Record<string, unknown>> {
 206      const formData = new FormData()
 207      formData.append('file', file)
 208      const url = `${this.getApiBase()}/api/import`
 209      const auth = await client.signHttpAuth(url, 'POST')
 210      const res = await fetch(url, {
 211        method: 'POST',
 212        headers: { Authorization: auth },
 213        body: formData
 214      })
 215      if (!res.ok) throw new Error(`Import failed: ${res.statusText}`)
 216      return await res.json()
 217    }
 218  
 219    // ==================== Log API ====================
 220  
 221    async getLogs(cursor?: string, limit = 100): Promise<Record<string, unknown>> {
 222      const params = new URLSearchParams()
 223      if (cursor) params.set('cursor', cursor)
 224      params.set('limit', String(limit))
 225      const res = await this.authGet(`/api/logs?${params}`)
 226      if (!res.ok) throw new Error(`Failed to get logs: ${res.statusText}`)
 227      return await res.json()
 228    }
 229  
 230    async clearLogs(): Promise<Record<string, unknown>> {
 231      const res = await this.authPost('/api/logs/clear')
 232      if (!res.ok) throw new Error(`Failed to clear logs: ${res.statusText}`)
 233      return await res.json()
 234    }
 235  
 236    async getLogLevel(): Promise<string> {
 237      const res = await this.authGet('/api/logs/level')
 238      if (!res.ok) throw new Error(`Failed to get log level: ${res.statusText}`)
 239      const data = await res.json()
 240      return data.level || 'info'
 241    }
 242  
 243    async setLogLevel(level: string): Promise<Record<string, unknown>> {
 244      const res = await this.authPost(
 245        '/api/logs/level',
 246        JSON.stringify({ level }),
 247        'application/json'
 248      )
 249      if (!res.ok) throw new Error(`Failed to set log level: ${res.statusText}`)
 250      return await res.json()
 251    }
 252  
 253    // ==================== NIP-86 Management ====================
 254  
 255    async nip86Request(method: string, params: unknown[] = []): Promise<Record<string, unknown>> {
 256      const res = await this.authPost(
 257        '/api/nip86',
 258        JSON.stringify({ method, params }),
 259        'application/json'
 260      )
 261      if (!res.ok) throw new Error(`NIP-86 ${method} failed: ${res.statusText}`)
 262      return await res.json()
 263    }
 264  
 265    // ==================== WireGuard API ====================
 266  
 267    async fetchWireGuardStatus(): Promise<Record<string, unknown>> {
 268      try {
 269        const res = await fetch(`${this.getApiBase()}/api/wireguard/status`)
 270        if (res.ok) return await res.json()
 271      } catch (e) {
 272        console.error('fetchWireGuardStatus error:', e)
 273      }
 274      return { wireguard_enabled: false, bunker_enabled: false, available: false }
 275    }
 276  
 277    async getWireGuardConfig(): Promise<Record<string, unknown>> {
 278      const res = await this.authGet('/api/wireguard/config')
 279      if (!res.ok) throw new Error(await res.text() || `Failed: ${res.statusText}`)
 280      return await res.json()
 281    }
 282  
 283    async regenerateWireGuard(): Promise<Record<string, unknown>> {
 284      const res = await this.authPost('/api/wireguard/regenerate')
 285      if (!res.ok) throw new Error(await res.text() || `Failed: ${res.statusText}`)
 286      return await res.json()
 287    }
 288  
 289    async getWireGuardAudit(): Promise<Record<string, unknown>> {
 290      const res = await this.authGet('/api/wireguard/audit')
 291      if (!res.ok) throw new Error(await res.text() || `Failed: ${res.statusText}`)
 292      return await res.json()
 293    }
 294  
 295    // ==================== NRC API ====================
 296  
 297    async fetchNRCConfig(): Promise<Record<string, unknown>> {
 298      try {
 299        const res = await fetch(`${this.getApiBase()}/api/nrc/config`)
 300        if (res.ok) return await res.json()
 301      } catch (e) {
 302        console.error('fetchNRCConfig error:', e)
 303      }
 304      return { enabled: false, badger_required: true }
 305    }
 306  
 307    async fetchNRCConnections(): Promise<Record<string, unknown>> {
 308      const res = await this.authGet('/api/nrc/connections')
 309      if (!res.ok) throw new Error(await res.text() || `Failed: ${res.statusText}`)
 310      return await res.json()
 311    }
 312  
 313    async createNRCConnection(label: string): Promise<Record<string, unknown>> {
 314      const res = await this.authPost(
 315        '/api/nrc/connections',
 316        JSON.stringify({ label }),
 317        'application/json'
 318      )
 319      if (!res.ok) throw new Error(await res.text() || `Failed: ${res.statusText}`)
 320      return await res.json()
 321    }
 322  
 323    async deleteNRCConnection(connId: string): Promise<Record<string, unknown>> {
 324      const res = await this.authDelete(`/api/nrc/connections/${connId}`)
 325      if (!res.ok) throw new Error(await res.text() || `Failed: ${res.statusText}`)
 326      return await res.json()
 327    }
 328  
 329    async getNRCConnectionURI(connId: string): Promise<Record<string, unknown>> {
 330      const res = await this.authGet(`/api/nrc/connections/${connId}/uri`)
 331      if (!res.ok) throw new Error(await res.text() || `Failed: ${res.statusText}`)
 332      return await res.json()
 333    }
 334  
 335    // ==================== Bunker API ====================
 336  
 337    async fetchBunkerInfo(): Promise<Record<string, unknown>> {
 338      try {
 339        const res = await fetch(`${this.getApiBase()}/api/bunker/info`)
 340        if (res.ok) return await res.json()
 341      } catch (e) {
 342        console.error('fetchBunkerInfo error:', e)
 343      }
 344      return {}
 345    }
 346  
 347    async fetchBunkerURL(): Promise<string> {
 348      try {
 349        const res = await this.authGet('/api/bunker/url')
 350        if (res.ok) {
 351          const data = await res.json()
 352          return data.url || ''
 353        }
 354      } catch (e) {
 355        console.error('fetchBunkerURL error:', e)
 356      }
 357      return ''
 358    }
 359  
 360    // ==================== Events API ====================
 361  
 362    async fetchMyEvents(cursor?: string, limit = 50): Promise<Record<string, unknown>> {
 363      const params = new URLSearchParams()
 364      if (cursor) params.set('cursor', cursor)
 365      params.set('limit', String(limit))
 366      const res = await this.authGet(`/api/events/mine?${params}`)
 367      if (!res.ok) throw new Error(`Failed: ${res.statusText}`)
 368      return await res.json()
 369    }
 370  }
 371  
 372  const relayAdmin = new RelayAdminService()
 373  export default relayAdmin
 374