UNPKG

nostr-dm-magiclink-utils

Version:

A comprehensive Nostr utility library for magic link authentication via direct messages, supporting both ESM and CommonJS. Features NIP-01/04 compliant message encryption, multi-relay support, internationalization (i18n) with RTL support, and TypeScript-f

164 lines (134 loc) 4.69 kB
import express from 'express'; import { NostrCrypto } from 'nostr-crypto-utils'; import { LocaleService } from '../../src/locales'; import UAParser from 'ua-parser-js'; import geoip from 'geoip-lite'; const app = express(); app.use(express.json()); // Initialize services const nostrCrypto = new NostrCrypto(); const localeService = new LocaleService('en'); // Generate service keys const SERVICE_PRIVKEY = process.env.SERVICE_PRIVKEY || nostrCrypto.generatePrivateKey(); const SERVICE_PUBKEY = nostrCrypto.getPublicKey(SERVICE_PRIVKEY); // Store magic links (use a proper database in production) const magicLinks = new Map<string, { pubkey: string; expiresAt: number; attempts: number; }>(); // Detect language from Accept-Language header function detectLanguage(acceptLanguage: string): string { const languages = acceptLanguage.split(',') .map(lang => { const [language, quality = '1'] = lang.trim().split(';q='); return { language: language.split('-')[0], // Get primary language quality: parseFloat(quality) }; }) .sort((a, b) => b.quality - a.quality); const supportedLocales = localeService.getSupportedLocales(); const detected = languages.find(lang => supportedLocales.includes(lang.language as any) ); return detected ? detected.language : 'en'; } // Get device and location info function getDeviceInfo(req: express.Request) { const ua = new UAParser(req.headers['user-agent']); const ip = req.ip || req.socket.remoteAddress || ''; const geo = geoip.lookup(ip); return { browser: ua.getBrowser().name || 'Unknown Browser', os: ua.getOS().name || 'Unknown OS', city: geo?.city || 'Unknown City', country: geo?.country || 'Unknown Country' }; } app.post('/auth/login', async (req, res) => { const { pubkey } = req.body; const acceptLanguage = req.headers['accept-language'] || 'en'; const locale = req.body.locale || detectLanguage(acceptLanguage); try { // Create magic link token const token = nostrCrypto.generateRandomBytes(32); const expiryMinutes = 15; const expiresAt = Math.floor(Date.now() / 1000) + (expiryMinutes * 60); // Store token with attempt counter magicLinks.set(token, { pubkey, expiresAt, attempts: 0 }); // Get device and location info const deviceInfo = getDeviceInfo(req); // Create magic link const magicLink = `${process.env.BASE_URL || 'http://localhost:3000'}/auth/verify?token=${token}`; // Set locale and generate message with all customizations localeService.setLocale(locale); const message = localeService.formatMagicLinkMessage({ appName: process.env.APP_NAME || 'TestApp', magicLink, expiryMinutes, deviceInfo, year: new Date().getFullYear() }); // Encrypt DM content const encryptedContent = await nostrCrypto.encryptDM( message, SERVICE_PRIVKEY, pubkey ); // Create DM event const event = await nostrCrypto.createEvent({ kind: 4, content: encryptedContent, tags: [['p', pubkey]], created_at: Math.floor(Date.now() / 1000) }); // Sign event const signedEvent = await nostrCrypto.signEvent(event, SERVICE_PRIVKEY); // In production, you would publish this to relays console.log('Signed DM event:', signedEvent); res.json({ success: true, supportedLocales: localeService.getSupportedLocales(), detectedLocale: locale, direction: localeService.getTextDirection() }); } catch (error) { console.error('Failed to create magic link:', error); res.status(500).json({ error: 'Failed to create magic link' }); } }); app.get('/auth/verify', async (req, res) => { const { token } = req.query; if (!token || typeof token !== 'string') { return res.status(400).json({ error: 'Invalid token' }); } const magicLink = magicLinks.get(token); if (!magicLink) { return res.status(404).json({ error: 'Token not found' }); } // Increment attempt counter magicLink.attempts += 1; if (magicLink.attempts > 3) { magicLinks.delete(token); return res.status(401).json({ error: 'Too many attempts' }); } if (magicLink.expiresAt < (Date.now() / 1000)) { magicLinks.delete(token); return res.status(401).json({ error: 'Token expired' }); } // Delete token (one-time use) magicLinks.delete(token); res.json({ success: true, pubkey: magicLink.pubkey }); }); const port = process.env.PORT || 3000; app.listen(port, () => { console.log(`Test server running at http://localhost:${port}`); });