Skip to content

๐Ÿ” Secure, NIP-compliant magic link authentication for Nostr applications. Features encrypted DMs (NIP-04), multi-relay support, and i18n with RTL languages. Built with TypeScript for type-safe, passwordless authentication flows.

License

Notifications You must be signed in to change notification settings

HumanjavaEnterprises/nostr-dm-magiclink-utils

Repository files navigation

nostr-dm-magiclink-utils

npm version License: MIT TypeScript Node.js Version Test Coverage Build Status Dependencies Code Style: Prettier Author Bundle Size Downloads Languages Security

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.

Features

  • ๐Ÿ” 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

Installation

npm install nostr-dm-magiclink-utils

Quick Start

Here'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 service 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' 
      });
    }
  }
});

Advanced Usage

Custom Error Handling

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
}

Multi-Language Support

// 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: 'ุงู„ู…ุณุชุฎุฏู…'
    }
  }
});

Custom Token Generation

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;
    }
  }
});

Relay Management

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}`);
});

Security Best Practices

  1. 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');
}
  1. Token Security

    • Use short expiration times (15-30 minutes)
    • Include necessary claims (sub, exp, jti)
    • Store tokens securely for verification
  2. Error Handling

    • Never expose internal errors to users
    • Log errors securely
    • Implement rate limiting
  3. Relay Security

    • Use trusted relays
    • Implement connection timeouts
    • Handle connection errors gracefully

Supported Languages

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

Contributing

See CONTRIBUTING.md for development setup and guidelines.

License

MIT ยฉ vveerrgg