Important: This documentation covers Yarn 1 (Classic).
For Yarn 2+ docs and migration guide, see yarnpkg.com.

Package detail

nostr-dm-magiclink-utils

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

nostr, magic-link, authentication, direct-messages, typescript, esm, commonjs, dual-module, nostr-protocol, nip-01, nip-04, i18n, rtl-support

readme

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

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

changelog

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

[0.2.0] - 2025-01-23

Breaking Changes

  • Renamed MagicLinkService to MagicLinkManager for consistency with consumer usage
  • All instances of MagicLinkService in types and documentation have been updated to MagicLinkManager

Dependencies

  • Updated nostr-crypto-utils from ^0.4.10 to ^0.4.13
    • Includes enhanced logging system
    • Improved error handling and stack traces
    • Better TypeScript type exports

Migration Guide

If you were using MagicLinkService directly, you'll need to update your imports and type references to use MagicLinkManager instead. If you were using the createMagicLinkService factory function, no changes are required as it will now return the correctly named type.

Note: This package is still in MVP phase, hence the 0.x.x versioning. While we maintain backward compatibility within minor versions, the API may undergo significant changes before reaching 1.0.0.

[0.1.4] - 2025-01-02

Changed

  • Updated dependencies to use published versions:
    • nostr-crypto-utils: ^0.4.10
    • nostr-websocket-utils: ^0.3.10
  • Removed local file references for better package distribution

[0.1.0] - 2024-12-28

Added

  • Initial release of the Nostr DM Magic Link Middleware
  • Core magic link functionality for secure authentication
  • Support for multiple languages (en, es, fr, ar, ja, pt, zh, ko, ru)
  • RTL language support with proper text direction handling
  • Optional context information in magic link messages
  • Text validation and URL sanitization
  • Fastify and Express middleware support
  • Comprehensive TypeScript type definitions
  • Localization service with message templating
  • Security best practices implementation

Security

  • Input validation for all user-provided content
  • URL sanitization for magic links
  • Text content validation to prevent injection
  • Proper handling of RTL/LTR text markers