import type { MediaRef } from '@/types/nostr' import { getEnabledNip95Apis } from './config' const MAX_IMAGE_BYTES = 5 * 1024 * 1024 const MAX_VIDEO_BYTES = 45 * 1024 * 1024 const IMAGE_TYPES = ['image/png', 'image/jpeg', 'image/jpg', 'image/webp'] const VIDEO_TYPES = ['video/mp4', 'video/webm', 'video/quicktime'] function assertBrowser(): void { if (typeof window === 'undefined') { throw new Error('NIP-95 upload is only available in the browser') } } function validateFile(file: File): MediaRef['type'] { if (IMAGE_TYPES.includes(file.type)) { if (file.size > MAX_IMAGE_BYTES) { throw new Error('Image exceeds 5MB limit') } return 'image' } if (VIDEO_TYPES.includes(file.type)) { if (file.size > MAX_VIDEO_BYTES) { throw new Error('Video exceeds 45MB limit') } return 'video' } throw new Error('Unsupported media type') } /** * Parse upload response from different NIP-95 providers * Supports multiple formats: * - Standard format: { url: string } * - void.cat format: { ok: true, file: { id, url } } * - nostrcheck.me format: { url: string } or { status: 'success', url: string } */ function parseUploadResponse(result: unknown, endpoint: string): string { if (typeof result !== 'object' || result === null) { throw new Error('Invalid upload response format') } const obj = result as Record // 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 } } // nostrcheck.me format: { status: 'success', url: string } if ('status' in obj && obj.status === 'success' && 'url' in obj && typeof obj.url === 'string') { return obj.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 * Uses proxy API route for endpoints that have CORS issues */ async function tryUploadEndpoint(endpoint: string, formData: FormData, useProxy: boolean = false): Promise { const targetUrl = useProxy ? '/api/nip95-upload' : endpoint const response = await fetch(targetUrl, { method: 'POST', body: formData, ...(useProxy && { headers: { 'Content-Type': 'multipart/form-data', }, }), }) if (!response.ok) { let errorMessage = 'Upload failed' try { const text = await response.text() errorMessage = text || `HTTP ${response.status} ${response.statusText}` } catch (_e) { errorMessage = `HTTP ${response.status} ${response.statusText}` } throw new Error(errorMessage) } let result: unknown try { result = await response.json() } catch (e) { const errorMessage = e instanceof Error ? e.message : 'Invalid JSON response' throw new Error(`Invalid upload response: ${errorMessage}`) } 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.' ) } const formData = new FormData() formData.append('file', file) let lastError: Error | null = null for (const endpoint of endpoints) { try { // Try direct upload first const url = await tryUploadEndpoint(endpoint, formData, false) return { url, type: mediaType } } catch (e) { const error = e instanceof Error ? e : new Error(String(e)) const errorMessage = error.message // If CORS error or network error, try via proxy const isCorsError = errorMessage.includes('CORS') || errorMessage.includes('Failed to fetch') if (isCorsError && endpoint.includes('nostr.build')) { try { console.log('Trying upload via proxy due to CORS error:', endpoint) const url = await tryUploadEndpoint(endpoint, formData, true) return { url, type: mediaType } } catch (proxyError) { console.error('NIP-95 upload proxy error:', { endpoint, error: proxyError instanceof Error ? proxyError.message : String(proxyError), fileSize: file.size, fileType: file.type, }) lastError = proxyError instanceof Error ? proxyError : new Error(String(proxyError)) continue } } 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') }