feat: calculate npub correctly

This commit is contained in:
complex 2025-04-08 21:28:25 +02:00
parent 01e164c119
commit 5300315bf4
5 changed files with 254 additions and 39 deletions

@ -43,7 +43,7 @@ export function isValidHexString(str: string, expectedLength?: number): boolean
* @returns Promise that resolves to a success or error message
*/
export async function publishToRelay(event: NostrEvent, relayUrl: string): Promise<string> {
return new Promise<string>((resolve, reject) => {
return new Promise<string>(async (resolve, reject) => {
try {
// Additional validation specifically for publishing
if (!event || typeof event !== 'object') {
@ -77,17 +77,16 @@ export async function publishToRelay(event: NostrEvent, relayUrl: string): Promi
const relayPool = new nostrTools.SimplePool();
// Set a timeout for the publish operation
const timeout = setTimeout(() => {
relayPool.close([relayUrl]);
reject(new Error(`Timed out connecting to relay: ${relayUrl}`));
}, 10000); // 10 second timeout
}, 10000);
// Create a standard formatted event to ensure it follows the relay protocol
const standardEvent = {
id: event.id,
pubkey: event.pubkey,
created_at: Number(event.created_at),
created_at: Math.floor(Date.now() / 1000), // Always use current timestamp
kind: Number(event.kind),
tags: event.tags,
content: event.content,
@ -103,22 +102,27 @@ export async function publishToRelay(event: NostrEvent, relayUrl: string): Promi
standardEvent.kind = 21120;
}
// Use the standardized event for publishing
const finalEvent = standardEvent;
// Since we updated the timestamp, we need to recalculate the event ID and get a new signature
// First create an unsigned event
const unsignedEvent = {
kind: standardEvent.kind,
created_at: standardEvent.created_at,
tags: standardEvent.tags,
content: standardEvent.content,
pubkey: standardEvent.pubkey
};
// Ensure all hex strings are valid (this is a sanity check beyond our validation)
if (!isValidHexString(finalEvent.id, 64)) {
reject(new Error(`ID is not a valid hex string: ${finalEvent.id}`));
// Get a new signature using the extension
if (!window.nostr) {
reject(new Error('No Nostr extension available to sign the event'));
return;
}
if (!isValidHexString(finalEvent.pubkey, 64)) {
reject(new Error(`Pubkey is not a valid hex string: ${finalEvent.pubkey}`));
return;
}
if (!isValidHexString(finalEvent.sig, 128)) {
reject(new Error(`Sig is not a valid hex string: ${finalEvent.sig}`));
let signedEvent;
try {
signedEvent = await window.nostr.signEvent(unsignedEvent);
} catch (signError) {
reject(new Error(`Failed to sign event with new timestamp: ${String(signError)}`));
return;
}
@ -142,7 +146,7 @@ export async function publishToRelay(event: NostrEvent, relayUrl: string): Promi
ws.onopen = (): void => {
// Send the event directly using the relay protocol format ["EVENT", event]
const messageToSend = JSON.stringify(["EVENT", finalEvent]);
const messageToSend = JSON.stringify(["EVENT", signedEvent]);
ws?.send(messageToSend);
};
@ -195,7 +199,7 @@ export async function publishToRelay(event: NostrEvent, relayUrl: string): Promi
console.log('WebSocket approach failed, trying nostr-tools:', wsError);
// Use the nostr-tools publish method as a fallback
const publishPromises = relayPool.publish([relayUrl], finalEvent);
const publishPromises = relayPool.publish([relayUrl], signedEvent as nostrTools.Event);
// Use Promise.all to wait for all promises to resolve
Promise.all(publishPromises)

@ -1,11 +1,8 @@
// Hard-coded Nostr keys for the server
export const serverKeys = {
// These are test keys - DO NOT USE IN PRODUCTION
privateKeyHex: "d5d49f5d14c8e59fafb5858dd27712674b8f8c53f1d72dae4ee69bd201068c68",
publicKeyHex: "f3c75b9243135a548bc64545dcfd7e8725e20faeacb5c18ac581d7a010036f40",
npub: "npub1r6knexka25dn9w9jnf5kf8xh6gfq7n3p38zfl7nn7cjjjsp4umcqnk0aun",
nsec: "nsec1hsdfy8ch8y0zw8g0np0mzk3chg94y2t5sek7hh9xpv565h5ucqpqgf4t93"
};
import * as nostrTools from 'nostr-tools';
export const privateKey = nostrTools.generateSecretKey();
export const publicKey = nostrTools.getPublicKey(privateKey);
export const serverNPub = nostrTools.nip19.npubEncode(publicKey);
// Relay configuration
export const relayUrls = [

@ -2,16 +2,16 @@
* Main entry point for the Nostr HTTP Request Server
*/
import { serverKeys, relayUrls, serverConfig } from './config.js';
import { privateKey, publicKey, serverNPub, relayUrls, serverConfig } from './config.js';
import { NostrHttpServer } from './server.js';
// Create server configuration
const config = {
port: serverConfig.port,
relayUrls: relayUrls,
privateKey: serverKeys.privateKeyHex,
pubkey: serverKeys.publicKeyHex,
npub: serverKeys.npub
privateKey: privateKey,
pubkey: publicKey,
npub: serverNPub,
};
// Create and start server

@ -4,7 +4,7 @@
*/
import NDK from '@nostr-dev-kit/ndk';
import { NDKUser, NDKEvent, NDKFilter, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk';
import { NDKUser, NDKEvent, NDKFilter, NDKPrivateKeySigner, NDKSubscription } from '@nostr-dev-kit/ndk';
import express from 'express';
import { createServer } from 'http';
import { ServerConfig, HttpRequest, RateLimitConfig, RateLimitState } from './types';
@ -17,6 +17,7 @@ export class NostrHttpServer {
private config: ServerConfig;
private rateLimits: Map<string, RateLimitState>;
private rateLimitConfig: RateLimitConfig;
private subscription: NDKSubscription | null = null;
/**
* Create a new Nostr HTTP Server
@ -38,19 +39,146 @@ export class NostrHttpServer {
});
}
/**
* Check the connection status of all relays
* @returns A map of relay URLs to their connection status
*/
private async checkRelayConnections(): Promise<Map<string, boolean>> {
const connectionStatus = new Map<string, boolean>();
for (const relayUrl of this.config.relayUrls) {
try {
// Try to connect to the relay
await this.ndk.connect();
// If we get here, connection was successful
connectionStatus.set(relayUrl, true);
console.log(`Relay ${relayUrl}: Connected`);
} catch (error) {
connectionStatus.set(relayUrl, false);
console.error(`Error connecting to relay ${relayUrl}:`, error);
}
}
return connectionStatus;
}
/**
* Send a test event to verify subscription is working
*/
private async sendTestEvent(): Promise<void> {
try {
console.log('Sending unencrypted test event to verify subscription...');
// Create a test event
const testEvent = new NDKEvent(this.ndk, {
kind: 21120,
content: 'Test event for subscription verification',
created_at: Math.floor(Date.now() / 1000),
pubkey: this.config.pubkey,
tags: [
['p', this.config.pubkey]
]
});
// Sign and publish the event
await testEvent.sign();
await testEvent.publish();
console.log('Unencrypted test event published successfully');
console.log('Event ID:', testEvent.id);
console.log('This event should be received by your subscription without decryption errors');
} catch (error) {
console.error('Error sending test event:', error);
}
}
/**
* Send an encrypted test event to verify encryption/decryption
*/
private async sendEncryptedTestEvent(): Promise<void> {
try {
console.log('Sending encrypted test event to verify encryption/decryption...');
// Create a test event with encrypted content
const testContent = 'This is an encrypted test event';
// Encrypt the content
const encryptedContent = await this.encryptContent(this.config.pubkey, testContent);
if (!encryptedContent) {
throw new Error('Failed to encrypt test content');
}
// Create the event
const testEvent = new NDKEvent(this.ndk, {
kind: 21120,
content: encryptedContent,
created_at: Math.floor(Date.now() / 1000),
pubkey: this.config.pubkey,
tags: [
['p', this.config.pubkey]
]
});
// Sign and publish the event
await testEvent.sign();
await testEvent.publish();
console.log('Encrypted test event published successfully');
console.log('Event ID:', testEvent.id);
console.log('This event should be received and decrypted by your subscription');
} catch (error) {
console.error('Error sending encrypted test event:', error);
}
}
/**
* Encrypt content using NIP-44
* @param pubkey The public key of the recipient
* @param content The content to encrypt
*/
private async encryptContent(pubkey: string, content: string): Promise<string | null> {
try {
const user = new NDKUser({ pubkey });
return await this.ndk.signer?.encrypt(user, content) || null;
} catch (error) {
console.error('Encryption error:', error);
return null;
}
}
/**
* Start the server
*/
public async start(): Promise<void> {
try {
// Connect to NDK
console.log('Connecting to relays:', this.config.relayUrls);
await this.ndk.connect();
console.log('Connected to Nostr relays');
// Verify connection status
const connectedRelays = this.ndk.explicitRelayUrls;
console.log('Connected to relays:', connectedRelays);
if (connectedRelays.length === 0) {
console.warn('Warning: No relays connected. Check your relay URLs.');
}
// Check individual relay connections
const relayStatus = await this.checkRelayConnections();
const connectedCount = Array.from(relayStatus.values()).filter(status => status).length;
console.log(`Connected to ${connectedCount} out of ${this.config.relayUrls.length} relays`);
// Subscribe to events
await this.subscribeToEvents();
console.log('Subscribed to kind 21120 events');
// Send a test event to verify subscription
await this.sendTestEvent();
// Send an encrypted test event to verify encryption/decryption
await this.sendEncryptedTestEvent();
// Set up Express server for status endpoint
const app = express();
const server = createServer(app);
@ -60,9 +188,51 @@ export class NostrHttpServer {
<h1>Nostr HTTP Request Server</h1>
<p>Server npub: ${this.config.npub}</p>
<p>Status: Running</p>
<p>Connected to ${this.ndk.explicitRelayUrls.length} relays</p>
<p>Subscription: ${this.subscription ? 'Active' : 'Inactive'}</p>
<p><a href="/test">Send Unencrypted Test Event</a></p>
<p><a href="/test-encrypted">Send Encrypted Test Event</a></p>
`);
});
// Add test endpoint
app.get('/test', async (req: express.Request, res: express.Response) => {
try {
await this.sendTestEvent();
res.send(`
<h1>Unencrypted Test Event Sent</h1>
<p>An unencrypted test event has been sent to verify the subscription.</p>
<p>Check the server logs for details.</p>
<p><a href="/">Back to Home</a></p>
`);
} catch (error) {
res.status(500).send(`
<h1>Error</h1>
<p>Failed to send unencrypted test event: ${error instanceof Error ? error.message : String(error)}</p>
<p><a href="/">Back to Home</a></p>
`);
}
});
// Add encrypted test endpoint
app.get('/test-encrypted', async (req: express.Request, res: express.Response) => {
try {
await this.sendEncryptedTestEvent();
res.send(`
<h1>Encrypted Test Event Sent</h1>
<p>An encrypted test event has been sent to verify encryption/decryption.</p>
<p>Check the server logs for details.</p>
<p><a href="/">Back to Home</a></p>
`);
} catch (error) {
res.status(500).send(`
<h1>Error</h1>
<p>Failed to send encrypted test event: ${error instanceof Error ? error.message : String(error)}</p>
<p><a href="/">Back to Home</a></p>
`);
}
});
server.listen(this.config.port, () => {
console.log(`Server running on http://localhost:${this.config.port}`);
});
@ -77,23 +247,49 @@ export class NostrHttpServer {
*/
private async subscribeToEvents(): Promise<void> {
const filter: NDKFilter = {
//kinds: [21120 as any], // Type assertion to bypass NDKKind type check
//'#p': [this.config.pubkey]
kinds: [21120 as any], // Type assertion to bypass NDKKind type check
'#p': [this.config.pubkey] // Filter for events tagged with our pubkey
};
console.log('Subscribing with filter:', JSON.stringify(filter, null, 2));
console.log('Connected to relays:', this.ndk.explicitRelayUrls);
// Create a subscription with the filter and options
const subscription = this.ndk.subscribe(filter, {
closeOnEose: false,
groupable: false
});
// Log subscription status
console.log('Subscription created');
// Set up event handler
subscription.on('event', (event: NDKEvent) => {
console.log('Received event:', event);
console.log('Event details:');
console.log(` ID: ${event.id}`);
console.log(` Kind: ${event.kind}`);
console.log(` Pubkey: ${event.pubkey}`);
console.log(` Created at: ${new Date(event.created_at * 1000).toISOString()}`);
console.log(` Content: ${event.content.substring(0, 100)}${event.content.length > 100 ? '...' : ''}`);
// Log tags
console.log(' Tags:');
event.tags.forEach(tag => {
console.log(` ${tag.join(', ')}`);
});
this.processEvent(event).catch(error => {
console.error('Error processing event:', error);
});
});
// Add EOSE handler to confirm subscription is working
subscription.on('eose', () => {
console.log('EOSE received - subscription is active');
});
// Store subscription for later reference
this.subscription = subscription;
}
/**
@ -115,10 +311,28 @@ export class NostrHttpServer {
return;
}
// Decrypt content
const decryptedContent = await this.decryptContent(event.pubkey, event.content);
if (!decryptedContent) {
throw new Error('Failed to decrypt content');
// Check if this is a test event (unencrypted)
if (event.content === 'Test event for subscription verification') {
console.log('Received test event, no decryption needed');
return;
}
// Try to decrypt content
let decryptedContent: string | null = null;
try {
decryptedContent = await this.decryptContent(event.pubkey, event.content);
if (!decryptedContent) {
throw new Error('Failed to decrypt content');
}
} catch (decryptError) {
console.error('Decryption error:', decryptError);
// If decryption fails, check if this is a test event or other unencrypted content
if (event.content.startsWith('HTTP/')) {
console.log('Content appears to be an unencrypted HTTP request');
decryptedContent = event.content;
} else {
throw new Error('Failed to decrypt content and content is not a recognized unencrypted format');
}
}
// Parse HTTP request

@ -31,7 +31,7 @@ export interface HttpResponse {
export interface ServerConfig {
port: number;
relayUrls: string[];
privateKey: string;
privateKey: Uint8Array<ArrayBufferLike>;
pubkey: string;
npub: string;
}