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 { getEnabledNip95Apis } from './config'
|
||||
import { generateNip98Token, isNip98Available } from './nip98'
|
||||
|
||||
const MAX_IMAGE_BYTES = 5 * 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
|
||||
for (const endpoint of endpoints) {
|
||||
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
|
||||
// Pass endpoint as query parameter to proxy
|
||||
const proxyUrl = `/api/nip95-upload?endpoint=${encodeURIComponent(endpoint)}`
|
||||
// Pass endpoint and auth token as query parameters to proxy
|
||||
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)
|
||||
return { url, type: mediaType }
|
||||
} 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' })
|
||||
}
|
||||
|
||||
// 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 authToken = req.query.auth as string | undefined
|
||||
|
||||
try {
|
||||
// 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 }
|
||||
try {
|
||||
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) {
|
||||
reject(new Error(`Too many redirects (max ${MAX_REDIRECTS})`))
|
||||
return
|
||||
@ -80,6 +81,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
const isHttps = url.protocol === 'https:'
|
||||
const clientModule = isHttps ? https : http
|
||||
const headers = requestFormData.getHeaders()
|
||||
|
||||
// Add NIP-98 Authorization header if token is provided
|
||||
if (authToken) {
|
||||
headers['Authorization'] = `Nostr ${authToken}`
|
||||
}
|
||||
|
||||
const requestOptions = {
|
||||
hostname: url.hostname,
|
||||
port: url.port || (isHttps ? 443 : 80),
|
||||
@ -106,8 +113,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
})
|
||||
// Drain the response before redirecting
|
||||
proxyResponse.resume()
|
||||
// Make new request to redirect location
|
||||
makeRequest(redirectUrl, redirectCount + 1, fileField)
|
||||
// Make new request to redirect location (preserve auth token for redirects)
|
||||
makeRequest(redirectUrl, redirectCount + 1, fileField, authToken)
|
||||
return
|
||||
} catch (urlError) {
|
||||
console.error('NIP-95 proxy invalid redirect URL:', {
|
||||
@ -166,7 +173,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
requestFormData.pipe(proxyRequest)
|
||||
}
|
||||
|
||||
makeRequest(currentUrl, 0, fileField)
|
||||
makeRequest(currentUrl, 0, fileField, authToken)
|
||||
})
|
||||
} catch (requestError) {
|
||||
// Clean up temporary file before returning error
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user