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