Fix CORS issue by changing default NIP-95 endpoint to void.cat and adding fallback to try multiple endpoints

This commit is contained in:
Nicolas Cantu 2025-12-28 23:41:52 +01:00
parent e5a95bc627
commit 333c8e4d7f
2 changed files with 89 additions and 53 deletions

View File

@ -41,11 +41,18 @@ export const DEFAULT_RELAYS: RelayConfig[] = [
export const DEFAULT_NIP95_APIS: Nip95Config[] = [ export const DEFAULT_NIP95_APIS: Nip95Config[] = [
{ {
id: 'default', id: 'default',
url: 'https://nostr.build/api/v2/upload', url: 'https://void.cat/upload',
enabled: true, enabled: true,
priority: 1, priority: 1,
createdAt: Date.now(), 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 = '' export const DEFAULT_PLATFORM_LIGHTNING_ADDRESS = ''

View File

@ -1,5 +1,5 @@
import type { MediaRef } from '@/types/nostr' import type { MediaRef } from '@/types/nostr'
import { getPrimaryNip95Api } from './config' import { getEnabledNip95Apis } from './config'
const MAX_IMAGE_BYTES = 5 * 1024 * 1024 const MAX_IMAGE_BYTES = 5 * 1024 * 1024
const MAX_VIDEO_BYTES = 45 * 1024 * 1024 const MAX_VIDEO_BYTES = 45 * 1024 * 1024
@ -29,40 +29,45 @@ function validateFile(file: File): MediaRef['type'] {
} }
/** /**
* Upload media via NIP-95. * Parse upload response from different NIP-95 providers
* This implementation validates size/type then delegates to a pluggable uploader. * Supports void.cat format: { ok: true, file: { id, url } } or { url: string }
* The actual upload endpoint must be provided via env/config; otherwise an error is thrown. * Supports nostr.build format: { url: string }
*/ */
export async function uploadNip95Media(file: File): Promise<MediaRef> { function parseUploadResponse(result: unknown, endpoint: string): string {
assertBrowser() if (typeof result !== 'object' || result === null) {
const mediaType = validateFile(file) throw new Error('Invalid upload response format')
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.'
)
} }
const formData = new FormData() const obj = result as Record<string, unknown>
formData.append('file', file)
let response: Response // void.cat format: { ok: true, file: { id, url } }
try { if ('ok' in obj && obj.ok === true && 'file' in obj) {
response = await fetch(endpoint, { const file = obj.file as Record<string, unknown>
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<string> {
const response = await fetch(endpoint, {
method: 'POST', method: 'POST',
body: formData, 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}`)
}
if (!response.ok) { if (!response.ok) {
let errorMessage = 'Upload failed' let errorMessage = 'Upload failed'
@ -72,37 +77,61 @@ export async function uploadNip95Media(file: File): Promise<MediaRef> {
} catch (_e) { } catch (_e) {
errorMessage = `HTTP ${response.status} ${response.statusText}` 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) throw new Error(errorMessage)
} }
let result: { url?: string } let result: unknown
try { try {
result = (await response.json()) as { url?: string } result = await response.json()
} catch (e) { } catch (e) {
const errorMessage = e instanceof Error ? e.message : 'Invalid JSON response' 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}`) throw new Error(`Invalid upload response: ${errorMessage}`)
} }
if (!result.url) { return parseUploadResponse(result, endpoint)
console.error('NIP-95 upload missing URL:', {
endpoint,
response: result,
})
throw new Error('Upload response missing URL')
} }
return { url: result.url, type: mediaType } /**
* 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<MediaRef> {
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 {
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')
} }