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