Skip to main content
Glama
conversion.ts10.8 kB
import { decode, encodePublicKey, encodePrivateKey, encodeNoteId, encodeProfile, encodeEvent, encodeAddress } from "snstr"; /** * Simple relay URL validation - checks for ws:// or wss:// protocol */ function isValidRelayUrl(url: string): boolean { try { const parsed = new URL(url); return (parsed.protocol === 'ws:' || parsed.protocol === 'wss:') && !!parsed.hostname && !parsed.username && // No credentials in URL !parsed.password; } catch { return false; } } /** * Filter invalid relay URLs from profile data */ function filterProfile(profile: any): any { if (!profile || typeof profile !== 'object') return profile; return { ...profile, relays: profile.relays ? profile.relays.filter(isValidRelayUrl) : [] }; } /** * Filter invalid relay URLs from event data */ function filterEvent(event: any): any { if (!event || typeof event !== 'object') return event; return { ...event, relays: event.relays ? event.relays.filter(isValidRelayUrl) : [] }; } /** * Filter invalid relay URLs from address data */ function filterAddress(address: any): any { if (!address || typeof address !== 'object') return address; return { ...address, relays: address.relays ? address.relays.filter(isValidRelayUrl) : [] }; } /** * Convert an npub or hex string to hex format * @param pubkey The pubkey in either npub or hex format * @returns The pubkey in hex format, or null if invalid */ export function npubToHex(pubkey: string): string | null { try { // Clean up input pubkey = pubkey.trim(); // If already hex if (/^[0-9a-fA-F]{64}$/.test(pubkey)) { return pubkey.toLowerCase(); } // If npub if (pubkey.startsWith('npub1')) { try { const result = decode(pubkey as `${string}1${string}`); if (result.type === 'npub') { return result.data; } } catch (e) { console.error('Error decoding npub:', e); return null; } } // Not a valid pubkey format return null; } catch (error) { console.error('Error in npubToHex:', error); return null; } } /** * Convert a hex pubkey to npub format * @param hex The pubkey in hex format * @returns The pubkey in npub format, or null if invalid */ export function hexToNpub(hex: string): string | null { try { // Clean up input hex = hex.trim(); // Validate hex format if (!/^[0-9a-fA-F]{64}$/.test(hex)) { return null; } // Convert to npub return encodePublicKey(hex.toLowerCase()); } catch (error) { console.error('Error in hexToNpub:', error); return null; } } /** * Universal NIP-19 entity converter * Converts between different NIP-19 formats and hex formats */ export interface ConversionInput { /** The input string to convert */ input: string; /** The target format to convert to */ targetType: 'npub' | 'nsec' | 'note' | 'hex' | 'nprofile' | 'nevent' | 'naddr'; /** Additional data for complex entities (nprofile, nevent, naddr) */ entityData?: { /** Relay URLs for the entity */ relays?: string[]; /** Author pubkey (for nevent/naddr) */ author?: string; /** Event kind (for nevent/naddr) */ kind?: number; /** Identifier for naddr */ identifier?: string; }; } export interface ConversionResult { success: boolean; result?: string; originalType?: string; targetType?: string; message?: string; data?: any; } /** * Convert any NIP-19 entity to any other format */ export function convertNip19Entity(options: ConversionInput): ConversionResult { try { const { input, targetType, entityData } = options; const cleanInput = input.trim(); // First, detect what type of input we have let sourceData: any; let sourceType: string | undefined; // Try to decode as NIP-19 entity first if (cleanInput.includes('1')) { try { const decoded = decode(cleanInput as `${string}1${string}`); sourceType = decoded.type; sourceData = decoded.data; } catch (e) { // Not a valid NIP-19 entity, might be hex } } // If not NIP-19, check if it's hex if (!sourceType) { if (/^[0-9a-fA-F]{64}$/.test(cleanInput)) { sourceType = 'hex'; sourceData = cleanInput.toLowerCase(); } else { return { success: false, message: 'Input is not a valid NIP-19 entity or hex string' }; } } // Apply security filtering for complex types if (['nprofile', 'nevent', 'naddr'].includes(sourceType)) { if (sourceType === 'nprofile') { sourceData = filterProfile(sourceData); } else if (sourceType === 'nevent') { sourceData = filterEvent(sourceData); } else if (sourceType === 'naddr') { sourceData = filterAddress(sourceData); } } // Now convert to target type let result: string; switch (targetType) { case 'hex': const hexResult = extractHexFromEntity(sourceType, sourceData); if (!hexResult) throw new Error('Cannot extract hex from input'); result = hexResult; break; case 'npub': const pubkeyHex = extractHexFromEntity(sourceType, sourceData); if (!pubkeyHex) throw new Error('Cannot extract pubkey from input'); result = encodePublicKey(pubkeyHex); break; case 'nsec': if (sourceType !== 'nsec' && sourceType !== 'hex') { throw new Error('Can only convert private keys to nsec format'); } const privkeyHex = sourceData; result = encodePrivateKey(privkeyHex); break; case 'note': if (sourceType === 'nevent') { result = encodeNoteId(sourceData.id); } else if (sourceType === 'note') { result = cleanInput; // Already a note } else if (sourceType === 'hex') { result = encodeNoteId(sourceData); } else { throw new Error('Cannot convert this entity type to note format'); } break; case 'nprofile': const profilePubkey = extractHexFromEntity(sourceType, sourceData); if (!profilePubkey) throw new Error('Cannot extract pubkey from input'); const profileData = { pubkey: profilePubkey, relays: entityData?.relays?.filter(url => isValidRelayUrl(url)) || [] }; result = encodeProfile(profileData); break; case 'nevent': let eventId: string; if (sourceType === 'nevent') { eventId = sourceData.id; } else if (sourceType === 'note') { eventId = sourceData; } else if (sourceType === 'hex') { eventId = sourceData; } else { throw new Error('Cannot convert this entity type to nevent format'); } const eventData = { id: eventId, relays: entityData?.relays?.filter(url => isValidRelayUrl(url)) || [], ...(entityData?.author && { author: entityData.author }), ...(entityData?.kind && { kind: entityData.kind }) }; result = encodeEvent(eventData); break; case 'naddr': if (!entityData?.identifier || !entityData?.kind) { throw new Error('naddr conversion requires identifier and kind'); } const addrPubkey = extractHexFromEntity(sourceType, sourceData); if (!addrPubkey) { if (!entityData?.author) { throw new Error('naddr conversion requires a pubkey (from input or entityData.author)'); } } const addressData = { identifier: entityData.identifier, pubkey: addrPubkey || entityData.author!, kind: entityData.kind, relays: entityData?.relays?.filter(url => isValidRelayUrl(url)) || [] }; result = encodeAddress(addressData); break; default: throw new Error(`Unsupported target type: ${targetType}`); } return { success: true, result, originalType: sourceType, targetType, message: `Successfully converted ${sourceType} to ${targetType}`, data: sourceData }; } catch (error) { return { success: false, message: `Conversion failed: ${error instanceof Error ? error.message : 'Unknown error'}` }; } } /** * Extract hex data from any entity type * For entities that contain multiple hex values (like nevent), this function * prioritizes pubkeys over event IDs for compatibility with npub/nprofile conversions */ function extractHexFromEntity(sourceType: string, sourceData: any): string | null { switch (sourceType) { case 'hex': return sourceData; case 'npub': case 'nsec': case 'note': return sourceData; case 'nprofile': return sourceData.pubkey; case 'nevent': // For nevent, we return the author pubkey if available // This is because extractHexFromEntity is primarily used for pubkey extraction // when converting to npub/nprofile formats // If you need the event ID, access sourceData.id directly return sourceData.author || null; case 'naddr': return sourceData.pubkey; default: return null; } } /** * Get information about any NIP-19 entity without conversion */ export function analyzeNip19Entity(input: string): ConversionResult { try { const cleanInput = input.trim(); // Check if hex if (/^[0-9a-fA-F]{64}$/.test(cleanInput)) { return { success: true, originalType: 'hex', message: 'Valid 64-character hex string', data: cleanInput.toLowerCase() }; } // Try to decode as NIP-19 if (cleanInput.includes('1')) { const decoded = decode(cleanInput as `${string}1${string}`); // Apply security filtering for complex types let safeData = decoded.data; if (['nprofile', 'nevent', 'naddr'].includes(decoded.type)) { if (decoded.type === 'nprofile') { safeData = filterProfile(decoded.data); } else if (decoded.type === 'nevent') { safeData = filterEvent(decoded.data); } else if (decoded.type === 'naddr') { safeData = filterAddress(decoded.data); } } return { success: true, originalType: decoded.type, message: `Valid ${decoded.type} entity`, data: safeData }; } return { success: false, message: 'Input is not a valid NIP-19 entity or hex string' }; } catch (error) { return { success: false, message: `Analysis failed: ${error instanceof Error ? error.message : 'Unknown error'}` }; } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/AustinKelsay/nostr-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server