diff --git a/features/nip98-authentication-nostrcheck.md b/features/nip98-authentication-nostrcheck.md new file mode 100644 index 0000000..051e77e --- /dev/null +++ b/features/nip98-authentication-nostrcheck.md @@ -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 ` 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 diff --git a/fixKnowledge/nostrcheck-me-authentication.md b/fixKnowledge/nostrcheck-me-authentication.md new file mode 100644 index 0000000..152ca00 --- /dev/null +++ b/fixKnowledge/nostrcheck-me-authentication.md @@ -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 +``` + +## 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 ` 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 ` + - 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 diff --git a/lib/nip95.ts b/lib/nip95.ts index 27afe70..eace907 100644 --- a/lib/nip95.ts +++ b/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 { 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) { diff --git a/lib/nip98.ts b/lib/nip98.ts new file mode 100644 index 0000000..868c407 --- /dev/null +++ b/lib/nip98.ts @@ -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 { + 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() +} diff --git a/pages/api/nip95-upload.ts b/pages/api/nip95-upload.ts index 073ba1d..d6694fa 100644 --- a/pages/api/nip95-upload.ts +++ b/pages/api/nip95-upload.ts @@ -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