diff --git a/lib/configStorageTypes.ts b/lib/configStorageTypes.ts index 5850922..a3cfcfb 100644 --- a/lib/configStorageTypes.ts +++ b/lib/configStorageTypes.ts @@ -41,11 +41,18 @@ export const DEFAULT_RELAYS: RelayConfig[] = [ export const DEFAULT_NIP95_APIS: Nip95Config[] = [ { id: 'default', - url: 'https://nostr.build/api/v2/upload', + url: 'https://void.cat/upload', enabled: true, priority: 1, createdAt: Date.now(), }, + { + id: 'nostrbuild', + url: 'https://nostr.build/api/v2/upload', + enabled: false, + priority: 2, + createdAt: Date.now(), + }, ] export const DEFAULT_PLATFORM_LIGHTNING_ADDRESS = '' diff --git a/lib/nip95.ts b/lib/nip95.ts index 066381c..1e7b5ff 100644 --- a/lib/nip95.ts +++ b/lib/nip95.ts @@ -1,5 +1,5 @@ import type { MediaRef } from '@/types/nostr' -import { getPrimaryNip95Api } from './config' +import { getEnabledNip95Apis } from './config' const MAX_IMAGE_BYTES = 5 * 1024 * 1024 const MAX_VIDEO_BYTES = 45 * 1024 * 1024 @@ -29,41 +29,46 @@ function validateFile(file: File): MediaRef['type'] { } /** - * Upload media via NIP-95. - * This implementation validates size/type then delegates to a pluggable uploader. - * The actual upload endpoint must be provided via env/config; otherwise an error is thrown. + * Parse upload response from different NIP-95 providers + * Supports void.cat format: { ok: true, file: { id, url } } or { url: string } + * Supports nostr.build format: { url: string } */ -export async function uploadNip95Media(file: File): Promise { - assertBrowser() - const mediaType = validateFile(file) - - const endpoint = await getPrimaryNip95Api() - if (!endpoint) { - throw new Error( - 'NIP-95 upload endpoint is not configured. Please configure a NIP-95 API endpoint in the application settings.' - ) +function parseUploadResponse(result: unknown, endpoint: string): string { + if (typeof result !== 'object' || result === null) { + throw new Error('Invalid upload response format') } - const formData = new FormData() - formData.append('file', file) + const obj = result as Record - let response: Response - try { - response = await fetch(endpoint, { - method: 'POST', - body: formData, - }) - } catch (e) { - const errorMessage = e instanceof Error ? e.message : 'Network error' - console.error('NIP-95 upload fetch error:', { - endpoint, - error: errorMessage, - fileSize: file.size, - fileType: file.type, - }) - throw new Error(`Failed to fetch upload endpoint: ${errorMessage}`) + // void.cat format: { ok: true, file: { id, url } } + if ('ok' in obj && obj.ok === true && 'file' in obj) { + const file = obj.file as Record + if (typeof file.url === 'string') { + return file.url + } } + // Standard format: { url: string } + if ('url' in obj && typeof obj.url === 'string') { + return obj.url + } + + console.error('NIP-95 upload missing URL:', { + endpoint, + response: result, + }) + throw new Error('Upload response missing URL') +} + +/** + * Try uploading to a single endpoint + */ +async function tryUploadEndpoint(endpoint: string, formData: FormData): Promise { + const response = await fetch(endpoint, { + method: 'POST', + body: formData, + }) + if (!response.ok) { let errorMessage = 'Upload failed' try { @@ -72,37 +77,61 @@ export async function uploadNip95Media(file: File): Promise { } catch (_e) { errorMessage = `HTTP ${response.status} ${response.statusText}` } - console.error('NIP-95 upload response error:', { - endpoint, - status: response.status, - statusText: response.statusText, - errorMessage, - fileSize: file.size, - fileType: file.type, - }) throw new Error(errorMessage) } - let result: { url?: string } + let result: unknown try { - result = (await response.json()) as { url?: string } + result = await response.json() } catch (e) { const errorMessage = e instanceof Error ? e.message : 'Invalid JSON response' - console.error('NIP-95 upload JSON parse error:', { - endpoint, - error: errorMessage, - status: response.status, - }) throw new Error(`Invalid upload response: ${errorMessage}`) } - if (!result.url) { - console.error('NIP-95 upload missing URL:', { - endpoint, - response: result, - }) - throw new Error('Upload response missing URL') + return parseUploadResponse(result, endpoint) +} + +/** + * Upload media via NIP-95. + * Tries all enabled endpoints in order until one succeeds. + * This implementation validates size/type then delegates to a pluggable uploader. + */ +export async function uploadNip95Media(file: File): Promise { + assertBrowser() + const mediaType = validateFile(file) + + const endpoints = await getEnabledNip95Apis() + if (endpoints.length === 0) { + throw new Error( + 'NIP-95 upload endpoint is not configured. Please configure a NIP-95 API endpoint in the application settings.' + ) } - return { url: result.url, type: mediaType } + const formData = new FormData() + formData.append('file', file) + + let lastError: Error | null = null + for (const endpoint of endpoints) { + try { + const url = await tryUploadEndpoint(endpoint, formData) + return { url, type: mediaType } + } catch (e) { + const error = e instanceof Error ? e : new Error(String(e)) + const errorMessage = error.message + console.error('NIP-95 upload endpoint error:', { + endpoint, + error: errorMessage, + fileSize: file.size, + fileType: file.type, + }) + lastError = error + // Continue to next endpoint + } + } + + // All endpoints failed + if (lastError) { + throw new Error(`Failed to upload to all endpoints: ${lastError.message}`) + } + throw new Error('Failed to upload: no endpoints available') }