A comprehensive Nostr utility library for implementing secure, user-friendly authentication via magic links in direct messages. Built with TypeScript and following Nostr Improvement Proposals (NIPs) for maximum compatibility and security.
- ๐ NIP-04 Compliant: Secure, encrypted direct messages following Nostr standards
- ๐ Rich i18n Support: 9 languages with RTL support
- ๐ Multi-Relay Support: Reliable message delivery with automatic failover
- ๐ก๏ธ Type-Safe: Full TypeScript support with comprehensive types
- ๐ Flexible Templates: Customizable messages with variable interpolation
- ๐ Modern API: Promise-based, async/await friendly interface
- ๐ฏ Zero Config: Sensible defaults with optional deep customization
npm install nostr-dm-magiclink-utilsHere's a complete example showing how to set up and use the magic link service:
import { createNostrMagicLink, NostrError } from 'nostr-dm-magiclink-utils'; import { generatePrivateKey } from 'nostr-tools'; // For demo purposes async function setupAuthService() { // Create manager with secure configuration const magicLink = createNostrMagicLink({ nostr: { // In production, load from secure environment variable privateKey: process.env.NOSTR_PRIVATE_KEY || generatePrivateKey(), relayUrls: [ 'wss://relay.damus.io', 'wss://relay.nostr.band', 'wss://nos.lol' ], // Optional: Configure connection timeouts connectionTimeout: 5000 }, magicLink: { verifyUrl: 'https://your-app.com/verify', // Async token generation with expiry token: async () => { const token = await generateSecureToken({ expiresIn: '15m', length: 32 }); return token; }, defaultLocale: 'en', // Optional: Custom message templates templates: { en: { subject: 'Login to {{appName}}', body: 'Click this secure link to log in: {{link}}\nValid for 15 minutes.' } } } }); return magicLink; } // Example usage in an Express route handler app.post('/auth/magic-link', async (req, res) => { try { const { pubkey } = req.body; if (!pubkey) { return res.status(400).json({ error: 'Missing pubkey' }); } const magicLink = await setupAuthService(); const result = await magicLink.sendMagicLink({ recipientPubkey: pubkey, messageOptions: { locale: req.locale, // From i18n middleware variables: { appName: 'YourApp', username: req.body.username } } }); if (result.success) { res.json({ message: 'Magic link sent successfully', expiresIn: '15 minutes' }); } } catch (error) { if (error instanceof NostrError) { // Handle specific Nostr-related errors res.status(400).json({ error: error.message, code: error.code }); } else { // Handle unexpected errors res.status(500).json({ error: 'Failed to send magic link' }); } } });try { const result = await magicLink.sendMagicLink({ recipientPubkey: pubkey, messageOptions: { locale: 'en' } }); if (!result.success) { switch (result.error.code) { case 'RELAY_CONNECTION_FAILED': // Attempt reconnection or use fallback relay await magicLink.reconnect(); break; case 'ENCRYPTION_FAILED': // Log encryption errors for debugging logger.error('Encryption failed:', result.error); break; case 'INVALID_PUBKEY': // Handle invalid recipient public key throw new UserError('Invalid recipient'); break; } } } catch (error) { // Handle other errors }// Arabic (RTL) example const result = await magicLink.sendMagicLink({ recipientPubkey: pubkey, messageOptions: { locale: 'ar', // Optional: Override default template template: { subject: 'ุชุณุฌูู ุงูุฏุฎูู ุฅูู {{appName}}', body: 'ุงููุฑ ููู ูุฐุง ุงูุฑุงุจุท ุงูุขู
ู ูุชุณุฌูู ุงูุฏุฎูู: {{link}}' }, variables: { appName: 'ุชุทุจููู', username: 'ุงูู
ุณุชุฎุฏู
' } } });const magicLink = createNostrMagicLink({ // ... other config magicLink: { verifyUrl: 'https://your-app.com/verify', token: async (recipientPubkey: string) => { // Generate a secure, short-lived token const token = await generateJWT({ sub: recipientPubkey, exp: Math.floor(Date.now() / 1000) + (15 * 60), // 15 minutes jti: crypto.randomUUID(), iss: 'your-app' }); // Optional: Store token in database for verification await db.tokens.create({ token, pubkey: recipientPubkey, expiresAt: new Date(Date.now() + 15 * 60 * 1000) }); return token; } } });const magicLink = createNostrMagicLink({ nostr: { privateKey: process.env.NOSTR_PRIVATE_KEY, relayUrls: ['wss://relay1.com', 'wss://relay2.com'], // Advanced relay options relayOptions: { retryAttempts: 3, retryDelay: 1000, timeout: 5000, onError: async (error, relay) => { logger.error(`Relay ${relay} error:`, error); // Optionally switch to backup relay await magicLink.addRelay('wss://backup-relay.com'); } } } }); // Monitor relay status magicLink.on('relay:connected', (relay) => { logger.info(`Connected to relay: ${relay}`); }); magicLink.on('relay:disconnected', (relay) => { logger.warn(`Disconnected from relay: ${relay}`); });- Private Key Management
- Never hardcode private keys
- Use secure environment variables
- Rotate keys periodically
// Load private key securely const privateKey = await loadPrivateKeyFromSecureStore(); if (!privateKey) { throw new Error('Missing required private key'); }-
Token Security
- Use short expiration times (15-30 minutes)
- Include necessary claims (sub, exp, jti)
- Store tokens securely for verification
-
Error Handling
- Never expose internal errors to users
- Log errors securely
- Implement rate limiting
-
Relay Security
- Use trusted relays
- Implement connection timeouts
- Handle connection errors gracefully
The library includes built-in support for:
- ๐บ๐ธ English (en)
- ๐ช๐ธ Spanish (es)
- ๐ซ๐ท French (fr)
- ๐ฏ๐ต Japanese (ja)
- ๐ฐ๐ท Korean (ko)
- ๐จ๐ณ Chinese (zh)
- ๐ง๐ท Portuguese (pt)
- ๐ท๐บ Russian (ru)
- ๐ธ๐ฆ Arabic (ar) - with RTL support
See CONTRIBUTING.md for development setup and guidelines.
MIT ยฉ vveerrgg