Files
next.orly.dev/app/web/src/websocket-auth.js
mleku f19dc4e5c8
Some checks failed
Go / build (push) Has been cancelled
Implement compose tab for event creation and management
- Added a new compose tab for users to create, sign, and publish Nostr events.
- Introduced functions for JSON reformatting, event signing, and publishing with WebSocket authentication.
- Implemented clipboard functionality to copy event JSON directly from the UI.
- Enhanced UI with buttons for reformatting, signing, and publishing events, improving user experience.
- Created a new websocket-auth.js module for handling WebSocket authentication with Nostr relays.
- Bumped version to v0.14.5.
2025-10-11 10:30:38 +01:00

252 lines
8.7 KiB
JavaScript

/**
* WebSocket Authentication Module for Nostr Relays
* Implements NIP-42 authentication with proper challenge handling
*/
export class NostrWebSocketAuth {
constructor(relayUrl, userSigner, userPubkey) {
this.relayUrl = relayUrl;
this.userSigner = userSigner;
this.userPubkey = userPubkey;
this.ws = null;
this.challenge = null;
this.isAuthenticated = false;
this.authPromise = null;
}
/**
* Connect to relay and handle authentication
*/
async connect() {
return new Promise((resolve, reject) => {
this.ws = new WebSocket(this.relayUrl);
this.ws.onopen = () => {
console.log('WebSocket connected to relay:', this.relayUrl);
resolve();
};
this.ws.onmessage = async (message) => {
try {
const data = JSON.parse(message.data);
await this.handleMessage(data);
} catch (error) {
console.error('Error parsing relay message:', error);
}
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
reject(new Error('Failed to connect to relay'));
};
this.ws.onclose = () => {
console.log('WebSocket connection closed');
this.isAuthenticated = false;
this.challenge = null;
};
// Timeout for connection
setTimeout(() => {
if (this.ws.readyState !== WebSocket.OPEN) {
reject(new Error('Connection timeout'));
}
}, 10000);
});
}
/**
* Handle incoming messages from relay
*/
async handleMessage(data) {
const [messageType, ...params] = data;
switch (messageType) {
case 'AUTH':
// Relay sent authentication challenge
this.challenge = params[0];
console.log('Received AUTH challenge:', this.challenge);
await this.authenticate();
break;
case 'OK':
const [eventId, success, reason] = params;
if (eventId && success) {
console.log('Authentication successful for event:', eventId);
this.isAuthenticated = true;
if (this.authPromise) {
this.authPromise.resolve();
this.authPromise = null;
}
} else if (eventId && !success) {
console.error('Authentication failed:', reason);
if (this.authPromise) {
this.authPromise.reject(new Error(reason || 'Authentication failed'));
this.authPromise = null;
}
}
break;
case 'NOTICE':
console.log('Relay notice:', params[0]);
break;
default:
console.log('Unhandled message type:', messageType, params);
}
}
/**
* Authenticate with the relay using NIP-42
*/
async authenticate() {
if (!this.challenge) {
throw new Error('No challenge received from relay');
}
if (!this.userSigner) {
throw new Error('No signer available for authentication');
}
try {
// Create NIP-42 authentication event
const authEvent = {
kind: 22242, // ClientAuthentication kind
created_at: Math.floor(Date.now() / 1000),
tags: [
['relay', this.relayUrl],
['challenge', this.challenge]
],
content: '',
pubkey: this.userPubkey
};
// Sign the authentication event
const signedAuthEvent = await this.userSigner.signEvent(authEvent);
// Send AUTH message to relay
const authMessage = ["AUTH", signedAuthEvent];
this.ws.send(JSON.stringify(authMessage));
console.log('Sent authentication event to relay');
// Wait for authentication response
return new Promise((resolve, reject) => {
this.authPromise = { resolve, reject };
// Timeout for authentication
setTimeout(() => {
if (this.authPromise) {
this.authPromise.reject(new Error('Authentication timeout'));
this.authPromise = null;
}
}, 10000);
});
} catch (error) {
console.error('Authentication error:', error);
throw error;
}
}
/**
* Publish an event to the relay
*/
async publishEvent(event) {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
throw new Error('WebSocket not connected');
}
return new Promise((resolve, reject) => {
// Send EVENT message
const eventMessage = ["EVENT", event];
this.ws.send(JSON.stringify(eventMessage));
// Set up message handler for this specific event
const originalOnMessage = this.ws.onmessage;
const timeout = setTimeout(() => {
this.ws.onmessage = originalOnMessage;
reject(new Error('Publish timeout'));
}, 15000);
this.ws.onmessage = async (message) => {
try {
const data = JSON.parse(message.data);
const [messageType, eventId, success, reason] = data;
if (messageType === 'OK' && eventId === event.id) {
clearTimeout(timeout);
this.ws.onmessage = originalOnMessage;
if (success) {
console.log('Event published successfully:', eventId);
resolve({ success: true, eventId, reason });
} else {
console.error('Event publish failed:', reason);
// Check if authentication is required
if (reason && reason.includes('auth-required')) {
console.log('Authentication required, attempting to authenticate...');
try {
await this.authenticate();
// Re-send the event after authentication
const retryMessage = ["EVENT", event];
this.ws.send(JSON.stringify(retryMessage));
// Don't resolve yet, wait for the retry response
return;
} catch (authError) {
reject(new Error(`Authentication failed: ${authError.message}`));
return;
}
}
reject(new Error(`Publish failed: ${reason}`));
}
} else {
// Handle other messages normally
await this.handleMessage(data);
}
} catch (error) {
clearTimeout(timeout);
this.ws.onmessage = originalOnMessage;
reject(error);
}
};
});
}
/**
* Close the WebSocket connection
*/
close() {
if (this.ws) {
this.ws.close();
this.ws = null;
}
this.isAuthenticated = false;
this.challenge = null;
}
/**
* Check if currently authenticated
*/
getAuthenticated() {
return this.isAuthenticated;
}
}
/**
* Convenience function to publish an event with authentication
*/
export async function publishEventWithAuth(relayUrl, event, userSigner, userPubkey) {
const auth = new NostrWebSocketAuth(relayUrl, userSigner, userPubkey);
try {
await auth.connect();
const result = await auth.publishEvent(event);
return result;
} finally {
auth.close();
}
}