Fix: NIP-95 upload 500 error
This commit is contained in:
parent
d8311078bc
commit
b5ec69624c
90
features/nip98-authentication-nostrcheck.md
Normal file
90
features/nip98-authentication-nostrcheck.md
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
# Implémentation NIP-98 pour authentification nostrcheck.me
|
||||||
|
|
||||||
|
## Date
|
||||||
|
2025-01-27
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
Implémenter l'authentification NIP-98 pour permettre l'upload de médias vers nostrcheck.me qui nécessite une authentification.
|
||||||
|
|
||||||
|
## Motivations
|
||||||
|
- nostrcheck.me retourne 401 Unauthorized sans authentification
|
||||||
|
- NIP-98 est le standard d'authentification HTTP pour Nostr
|
||||||
|
- Permettre l'utilisation de nostrcheck.me comme endpoint NIP-95
|
||||||
|
|
||||||
|
## Impacts
|
||||||
|
- **Utilisateurs** : Peuvent maintenant uploader vers nostrcheck.me s'ils sont connectés avec une extension Nostr (Alby, nos2x, etc.)
|
||||||
|
- **Fonctionnalité** : Un endpoint supplémentaire disponible pour les uploads
|
||||||
|
- **Sécurité** : L'authentification utilise la signature Nostr standard
|
||||||
|
|
||||||
|
## Modifications
|
||||||
|
|
||||||
|
### Fichiers créés
|
||||||
|
- **`lib/nip98.ts`** : Implémentation de NIP-98 pour générer des tokens d'authentification HTTP
|
||||||
|
|
||||||
|
### Fichiers modifiés
|
||||||
|
- **`lib/nip95.ts`** : Détection automatique de nostrcheck.me et génération du token NIP-98
|
||||||
|
- **`pages/api/nip95-upload.ts`** : Ajout du header Authorization avec le token NIP-98
|
||||||
|
|
||||||
|
### Fonctionnalités implémentées
|
||||||
|
|
||||||
|
#### Génération du token NIP-98 (`lib/nip98.ts`)
|
||||||
|
- Création d'un événement Nostr de type 27235 (NIP-98 HTTP Auth)
|
||||||
|
- Tags requis :
|
||||||
|
- `u` : URL complète de la requête
|
||||||
|
- `method` : Méthode HTTP (POST)
|
||||||
|
- `payload` : Hash SHA256 du body (optionnel, non implémenté pour l'instant)
|
||||||
|
- Signature de l'événement via `nostrRemoteSigner` (support Alby/nos2x)
|
||||||
|
- Encodage base64 de l'événement signé
|
||||||
|
|
||||||
|
#### Intégration dans le flux d'upload (`lib/nip95.ts`)
|
||||||
|
- Détection automatique si l'endpoint est nostrcheck.me
|
||||||
|
- Vérification de la disponibilité de NIP-98 (utilisateur connecté)
|
||||||
|
- Génération du token pour l'URL finale (nostrcheck.me)
|
||||||
|
- Passage du token à l'API proxy via paramètre de requête
|
||||||
|
|
||||||
|
#### Support dans l'API proxy (`pages/api/nip95-upload.ts`)
|
||||||
|
- Récupération du token depuis les paramètres de requête
|
||||||
|
- Ajout du header `Authorization: Nostr <token>` aux requêtes HTTP
|
||||||
|
- Préservation du token lors des redirections HTTP
|
||||||
|
|
||||||
|
## Modalités de déploiement
|
||||||
|
1. Les modifications sont dans le code source
|
||||||
|
2. Rebuild de l'application : `npm run build`
|
||||||
|
3. Redémarrage du service Next.js
|
||||||
|
4. Aucune migration de données nécessaire
|
||||||
|
|
||||||
|
## Modalités d'analyse
|
||||||
|
Pour vérifier que l'authentification fonctionne :
|
||||||
|
|
||||||
|
1. **Vérifier la connexion Nostr** :
|
||||||
|
- L'utilisateur doit être connecté avec Alby, nos2x ou une autre extension NIP-07
|
||||||
|
- Vérifier que `nostrRemoteSigner.isAvailable()` retourne true
|
||||||
|
|
||||||
|
2. **Tester l'upload vers nostrcheck.me** :
|
||||||
|
- Activer nostrcheck.me dans les settings
|
||||||
|
- Tenter un upload d'image/vidéo
|
||||||
|
- Vérifier que le token NIP-98 est généré (logs console)
|
||||||
|
- Vérifier que le header Authorization est présent dans la requête
|
||||||
|
|
||||||
|
3. **Vérifier les logs serveur** :
|
||||||
|
- Chercher les logs de redirection si applicable
|
||||||
|
- Vérifier que le header Authorization est transmis
|
||||||
|
- Vérifier la réponse de nostrcheck.me (devrait être 200 au lieu de 401)
|
||||||
|
|
||||||
|
4. **En cas d'erreur** :
|
||||||
|
- Vérifier que l'utilisateur est bien connecté
|
||||||
|
- Vérifier que le compte nostrcheck.me est créé avec la même clé
|
||||||
|
- Vérifier les logs pour les erreurs de signature
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Le token NIP-98 est généré côté client (navigateur) car il nécessite la clé privée
|
||||||
|
- Le token est passé à l'API proxy via paramètre de requête (sécurisé car HTTPS)
|
||||||
|
- Le token est valide uniquement pour l'URL et la méthode spécifiées
|
||||||
|
- Le token inclut un timestamp et expire après un certain temps (selon la politique de nostrcheck.me)
|
||||||
|
- Pour l'instant, le hash du payload n'est pas calculé (peut être ajouté plus tard si nécessaire)
|
||||||
|
- Le kind 27235 est le kind standard pour NIP-98 HTTP Auth
|
||||||
|
|
||||||
|
## Références
|
||||||
|
- NIP-98 : https://github.com/nostr-protocol/nips/blob/master/98.md
|
||||||
|
- nostrcheck.me : https://nostrcheck.me/
|
||||||
|
- NIP-07 : https://github.com/nostr-protocol/nips/blob/master/07.md
|
||||||
128
fixKnowledge/nostrcheck-me-authentication.md
Normal file
128
fixKnowledge/nostrcheck-me-authentication.md
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
# Authentification pour nostrcheck.me API
|
||||||
|
|
||||||
|
## Date
|
||||||
|
2025-01-27
|
||||||
|
|
||||||
|
## Problème
|
||||||
|
L'endpoint `https://nostrcheck.me/api/v1/media` retourne une erreur 401 Unauthorized lors des tentatives d'upload, indiquant qu'une authentification est requise.
|
||||||
|
|
||||||
|
## Symptômes
|
||||||
|
- Erreur 401 lors des appels à `/api/nip95-upload?endpoint=https://nostrcheck.me/api/v1/media`
|
||||||
|
- Message d'erreur : `{"result":false,"description":"Authorization header not found"}`
|
||||||
|
- Les uploads vers nostrcheck.me échouent systématiquement
|
||||||
|
|
||||||
|
## Recherche effectuée
|
||||||
|
|
||||||
|
### Informations trouvées sur nostrcheck.me
|
||||||
|
|
||||||
|
1. **Authentification du site web** :
|
||||||
|
- Support de NIP-07 (extensions de navigateur comme Alby)
|
||||||
|
- Code à usage unique envoyé via message direct Nostr lors de l'inscription
|
||||||
|
- Nécessite la création d'un compte sur nostrcheck.me
|
||||||
|
|
||||||
|
2. **Services offerts** :
|
||||||
|
- Enregistrement d'adresses Nostr (NIP-05)
|
||||||
|
- Hébergement de médias
|
||||||
|
- Galeries privées et publiques
|
||||||
|
- Relais privé
|
||||||
|
|
||||||
|
3. **API d'upload** :
|
||||||
|
- Endpoint : `https://nostrcheck.me/api/v1/media`
|
||||||
|
- Nécessite un header `Authorization`
|
||||||
|
- Format du header non documenté publiquement
|
||||||
|
|
||||||
|
### NIP-98 (HTTP Auth pour Nostr)
|
||||||
|
|
||||||
|
NIP-98 est le standard d'authentification HTTP pour Nostr. Il permet d'authentifier des requêtes HTTP en signant un événement Nostr qui contient :
|
||||||
|
- La méthode HTTP (POST)
|
||||||
|
- L'URL de la requête
|
||||||
|
- Le hash du body (optionnel)
|
||||||
|
- Un timestamp
|
||||||
|
|
||||||
|
Le header `Authorization` devrait contenir un token au format NIP-98, généralement :
|
||||||
|
```
|
||||||
|
Authorization: Nostr <base64-encoded-event>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Hypothèses
|
||||||
|
|
||||||
|
1. **NIP-98 requis** : nostrcheck.me utilise probablement NIP-98 pour l'authentification de l'API
|
||||||
|
2. **Compte requis** : Un compte doit être créé sur nostrcheck.me avant d'utiliser l'API
|
||||||
|
3. **Signature d'événement** : Il faut probablement signer un événement Nostr avec la clé privée de l'utilisateur pour générer le token d'authentification
|
||||||
|
|
||||||
|
## À explorer
|
||||||
|
|
||||||
|
### 1. Documentation de l'API nostrcheck.me
|
||||||
|
- Vérifier s'il existe une documentation publique de l'API
|
||||||
|
- Contacter le support de nostrcheck.me pour obtenir les spécifications
|
||||||
|
- Examiner le code source si disponible
|
||||||
|
|
||||||
|
### 2. Implémentation de NIP-98
|
||||||
|
Si NIP-98 est requis, il faut :
|
||||||
|
- Créer une fonction pour générer un événement d'authentification NIP-98
|
||||||
|
- Signer cet événement avec la clé privée de l'utilisateur (via Alby ou clé locale)
|
||||||
|
- Encoder l'événement signé en base64
|
||||||
|
- Ajouter le header `Authorization: Nostr <token>` aux requêtes
|
||||||
|
|
||||||
|
### 3. Format de l'événement NIP-98
|
||||||
|
L'événement devrait contenir :
|
||||||
|
- `kind`: Probablement un kind spécifique pour l'authentification HTTP
|
||||||
|
- `tags`: Tags avec la méthode HTTP, l'URL, etc.
|
||||||
|
- `content`: Vide ou contenant des métadonnées
|
||||||
|
- `created_at`: Timestamp actuel
|
||||||
|
- `pubkey`: Clé publique de l'utilisateur
|
||||||
|
- `sig`: Signature de l'événement
|
||||||
|
|
||||||
|
### 4. Intégration dans l'API proxy
|
||||||
|
Modifier `pages/api/nip95-upload.ts` pour :
|
||||||
|
- Détecter si l'endpoint est nostrcheck.me
|
||||||
|
- Générer un token NIP-98 si l'utilisateur est authentifié
|
||||||
|
- Ajouter le header Authorization à la requête
|
||||||
|
|
||||||
|
## Code existant disponible
|
||||||
|
|
||||||
|
Le projet dispose déjà de :
|
||||||
|
- `lib/nostrRemoteSigner.ts` : Service de signature d'événements Nostr
|
||||||
|
- `lib/nostrAuth.ts` : Service d'authentification Nostr
|
||||||
|
- Support de NIP-07 via Alby extension
|
||||||
|
- Gestion des clés privées (stockage chiffré)
|
||||||
|
|
||||||
|
## Implémentation réalisée
|
||||||
|
|
||||||
|
✅ **NIP-98 implémenté** (2025-01-27)
|
||||||
|
|
||||||
|
1. **Création de `lib/nip98.ts`** :
|
||||||
|
- Fonction `generateNip98Token()` pour générer le token d'authentification
|
||||||
|
- Support de la signature via `nostrRemoteSigner` (Alby/nos2x)
|
||||||
|
- Encodage base64 de l'événement signé
|
||||||
|
|
||||||
|
2. **Intégration dans `lib/nip95.ts`** :
|
||||||
|
- Détection automatique de nostrcheck.me
|
||||||
|
- Génération du token NIP-98 avant l'upload
|
||||||
|
- Passage du token à l'API proxy
|
||||||
|
|
||||||
|
3. **Support dans `pages/api/nip95-upload.ts`** :
|
||||||
|
- Récupération du token depuis les paramètres de requête
|
||||||
|
- Ajout du header `Authorization: Nostr <token>`
|
||||||
|
- Préservation du token lors des redirections
|
||||||
|
|
||||||
|
## Utilisation
|
||||||
|
|
||||||
|
Pour utiliser nostrcheck.me :
|
||||||
|
1. Créer un compte sur nostrcheck.me avec la même clé Nostr (via nos2x, Alby, etc.)
|
||||||
|
2. Se connecter à l'application avec la même extension/clé
|
||||||
|
3. Activer nostrcheck.me dans les settings (`/settings`)
|
||||||
|
4. L'upload vers nostrcheck.me fonctionnera automatiquement avec l'authentification NIP-98
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- L'authentification NIP-98 nécessite que l'utilisateur soit connecté avec Alby ou ait une clé privée disponible
|
||||||
|
- Le token doit être généré côté client (navigateur) car il nécessite la clé privée
|
||||||
|
- L'API proxy côté serveur ne peut pas générer le token sans avoir accès à la clé privée de l'utilisateur
|
||||||
|
- Solution possible : Générer le token côté client et le passer en paramètre à l'API proxy
|
||||||
|
|
||||||
|
## Références
|
||||||
|
|
||||||
|
- nostrcheck.me : https://nostrcheck.me/
|
||||||
|
- NIP-07 : https://github.com/nostr-protocol/nips/blob/master/07.md
|
||||||
|
- NIP-98 : À rechercher dans le repository des NIPs
|
||||||
31
lib/nip95.ts
31
lib/nip95.ts
@ -1,5 +1,6 @@
|
|||||||
import type { MediaRef } from '@/types/nostr'
|
import type { MediaRef } from '@/types/nostr'
|
||||||
import { getEnabledNip95Apis } from './config'
|
import { getEnabledNip95Apis } from './config'
|
||||||
|
import { generateNip98Token, isNip98Available } from './nip98'
|
||||||
|
|
||||||
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
|
||||||
@ -124,9 +125,35 @@ export async function uploadNip95Media(file: File): Promise<MediaRef> {
|
|||||||
let lastError: Error | null = null
|
let lastError: Error | null = null
|
||||||
for (const endpoint of endpoints) {
|
for (const endpoint of endpoints) {
|
||||||
try {
|
try {
|
||||||
|
// Check if endpoint requires NIP-98 authentication (nostrcheck.me)
|
||||||
|
const needsAuth = endpoint.includes('nostrcheck.me')
|
||||||
|
let authToken: string | undefined
|
||||||
|
|
||||||
|
if (needsAuth) {
|
||||||
|
if (!isNip98Available()) {
|
||||||
|
console.warn('NIP-98 authentication required for nostrcheck.me but not available. Skipping endpoint.')
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// Generate NIP-98 token for the actual endpoint (not the proxy)
|
||||||
|
// The token must be for the final destination URL
|
||||||
|
authToken = await generateNip98Token('POST', endpoint)
|
||||||
|
} catch (authError) {
|
||||||
|
console.error('Failed to generate NIP-98 token:', authError)
|
||||||
|
// Continue to next endpoint if auth fails
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Always use proxy to avoid CORS, 405, and name resolution issues
|
// Always use proxy to avoid CORS, 405, and name resolution issues
|
||||||
// Pass endpoint as query parameter to proxy
|
// Pass endpoint and auth token as query parameters to proxy
|
||||||
const proxyUrl = `/api/nip95-upload?endpoint=${encodeURIComponent(endpoint)}`
|
const proxyUrlParams = new URLSearchParams({
|
||||||
|
endpoint: endpoint,
|
||||||
|
})
|
||||||
|
if (authToken) {
|
||||||
|
proxyUrlParams.set('auth', authToken)
|
||||||
|
}
|
||||||
|
const proxyUrl = `/api/nip95-upload?${proxyUrlParams.toString()}`
|
||||||
const url = await tryUploadEndpoint(proxyUrl, formData, true)
|
const url = await tryUploadEndpoint(proxyUrl, formData, true)
|
||||||
return { url, type: mediaType }
|
return { url, type: mediaType }
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
65
lib/nip98.ts
Normal file
65
lib/nip98.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
/**
|
||||||
|
* NIP-98 HTTP Auth implementation
|
||||||
|
* Generates authentication tokens for HTTP requests using Nostr events
|
||||||
|
* See: https://github.com/nostr-protocol/nips/blob/master/98.md
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { EventTemplate } from 'nostr-tools'
|
||||||
|
import { nostrRemoteSigner } from './nostrRemoteSigner'
|
||||||
|
import { nostrService } from './nostr'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate NIP-98 authentication token for HTTP request
|
||||||
|
* @param method HTTP method (GET, POST, etc.)
|
||||||
|
* @param url Full URL of the request
|
||||||
|
* @param payloadHash Optional SHA256 hash of the request body (for POST/PUT)
|
||||||
|
* @returns Base64-encoded signed event token
|
||||||
|
*/
|
||||||
|
export async function generateNip98Token(method: string, url: string, payloadHash?: string): Promise<string> {
|
||||||
|
const pubkey = nostrService.getPublicKey()
|
||||||
|
if (!pubkey) {
|
||||||
|
throw new Error('Public key required for NIP-98 authentication. Please connect with a Nostr extension.')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse URL to get components
|
||||||
|
const urlObj = new URL(url)
|
||||||
|
const path = urlObj.pathname + urlObj.search
|
||||||
|
|
||||||
|
// Build event template for NIP-98
|
||||||
|
const tags: string[][] = [
|
||||||
|
['u', urlObj.origin + path],
|
||||||
|
['method', method],
|
||||||
|
]
|
||||||
|
|
||||||
|
// Add payload hash if provided (for POST/PUT requests)
|
||||||
|
if (payloadHash) {
|
||||||
|
tags.push(['payload', payloadHash])
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventTemplate: EventTemplate = {
|
||||||
|
kind: 27235, // NIP-98 kind for HTTP auth
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
tags: tags,
|
||||||
|
content: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign the event
|
||||||
|
const signedEvent = await nostrRemoteSigner.signEvent(eventTemplate)
|
||||||
|
if (!signedEvent) {
|
||||||
|
throw new Error('Failed to sign NIP-98 authentication event')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode event as base64 JSON
|
||||||
|
const eventJson = JSON.stringify(signedEvent)
|
||||||
|
const eventBytes = new TextEncoder().encode(eventJson)
|
||||||
|
const base64Token = btoa(String.fromCharCode(...eventBytes))
|
||||||
|
|
||||||
|
return base64Token
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if NIP-98 authentication is available
|
||||||
|
*/
|
||||||
|
export function isNip98Available(): boolean {
|
||||||
|
return nostrRemoteSigner.isAvailable()
|
||||||
|
}
|
||||||
@ -24,8 +24,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
return res.status(405).json({ error: 'Method not allowed' })
|
return res.status(405).json({ error: 'Method not allowed' })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get target endpoint from query or use default
|
// Get target endpoint and auth token from query
|
||||||
const targetEndpoint = (req.query.endpoint as string) || 'https://void.cat/upload'
|
const targetEndpoint = (req.query.endpoint as string) || 'https://void.cat/upload'
|
||||||
|
const authToken = req.query.auth as string | undefined
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Parse multipart form data
|
// Parse multipart form data
|
||||||
@ -63,7 +64,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
let response: { statusCode: number; statusMessage: string; body: string }
|
let response: { statusCode: number; statusMessage: string; body: string }
|
||||||
try {
|
try {
|
||||||
response = await new Promise<{ statusCode: number; statusMessage: string; body: string }>((resolve, reject) => {
|
response = await new Promise<{ statusCode: number; statusMessage: string; body: string }>((resolve, reject) => {
|
||||||
function makeRequest(url: URL, redirectCount: number, fileField: FormidableFile): void {
|
function makeRequest(url: URL, redirectCount: number, fileField: FormidableFile, authToken?: string): void {
|
||||||
if (redirectCount > MAX_REDIRECTS) {
|
if (redirectCount > MAX_REDIRECTS) {
|
||||||
reject(new Error(`Too many redirects (max ${MAX_REDIRECTS})`))
|
reject(new Error(`Too many redirects (max ${MAX_REDIRECTS})`))
|
||||||
return
|
return
|
||||||
@ -80,6 +81,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
const isHttps = url.protocol === 'https:'
|
const isHttps = url.protocol === 'https:'
|
||||||
const clientModule = isHttps ? https : http
|
const clientModule = isHttps ? https : http
|
||||||
const headers = requestFormData.getHeaders()
|
const headers = requestFormData.getHeaders()
|
||||||
|
|
||||||
|
// Add NIP-98 Authorization header if token is provided
|
||||||
|
if (authToken) {
|
||||||
|
headers['Authorization'] = `Nostr ${authToken}`
|
||||||
|
}
|
||||||
|
|
||||||
const requestOptions = {
|
const requestOptions = {
|
||||||
hostname: url.hostname,
|
hostname: url.hostname,
|
||||||
port: url.port || (isHttps ? 443 : 80),
|
port: url.port || (isHttps ? 443 : 80),
|
||||||
@ -106,8 +113,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
})
|
})
|
||||||
// Drain the response before redirecting
|
// Drain the response before redirecting
|
||||||
proxyResponse.resume()
|
proxyResponse.resume()
|
||||||
// Make new request to redirect location
|
// Make new request to redirect location (preserve auth token for redirects)
|
||||||
makeRequest(redirectUrl, redirectCount + 1, fileField)
|
makeRequest(redirectUrl, redirectCount + 1, fileField, authToken)
|
||||||
return
|
return
|
||||||
} catch (urlError) {
|
} catch (urlError) {
|
||||||
console.error('NIP-95 proxy invalid redirect URL:', {
|
console.error('NIP-95 proxy invalid redirect URL:', {
|
||||||
@ -166,7 +173,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
requestFormData.pipe(proxyRequest)
|
requestFormData.pipe(proxyRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
makeRequest(currentUrl, 0, fileField)
|
makeRequest(currentUrl, 0, fileField, authToken)
|
||||||
})
|
})
|
||||||
} catch (requestError) {
|
} catch (requestError) {
|
||||||
// Clean up temporary file before returning error
|
// Clean up temporary file before returning error
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user