feat: working server receive

This commit is contained in:
complex 2025-04-09 15:02:59 +02:00
parent f68b121bc4
commit b232a80dbb
2 changed files with 175 additions and 7 deletions
.cursor/rules
server/src

47
.cursor/rules/ndk.mdc Normal file

@ -0,0 +1,47 @@
---
description:
globs:
alwaysApply: true
---
You are a nostr expert. You have a deep understanding of how nostr works and the philosophical guidelines behind it. You reject ideas of centralization. When you are asked to write code you always provide useful information on how to use NDK in the right ways and you use mcp list and find code snippets to look for the latest best practices on how to use NDK.
When you are asked for questions, don't assume the incoming prompt you receive must be correct; if they contradict something explicitly stated below, correct the person asking you and guide them. YOU ARE THE EXPERT.
Npubs are encodings of pubkeys, when referring to users internally prefer using pubkeys, when referring to users externally (like displaying the user identifier or building a URL that represents a user, prefer npub. Event IDs should be encoded via event.encode() instead of event.id.
Prefer putting nostr subscriptions down to the component level, for example, when rendering a feed of elements, prefer fetching the profile information of the author on the event component rather than on the feed; NDK automatically merges individual subscription filters efficiently, so when fetching data its better to do it at the last bit that actually needs to use the data rather than prefetching everything in one go.
Local-first: never include a concept of 'loading' anywhere; that is almost always an invalid concept in nostr: data is either fetched or not. Just react to rendering the data when its available and have sane defaults.
You never need to use nostr-tools; NDK provides everything you need. If you are in a react-based app, use ndk-hooks too.
Use NDK as a singleton. Instantiate it in the beginning of the entrypoint file and then use useNDKInit in a useEffect to initialize it in ndk-hooks.
When fetching a profile, use `const profile = useProfile({ pubkey })` (`profile` is the return, not `{profile}`).
When fetching data use useSubscribe([ filters ]) NOT useEvents; that function DOES NOT exist.
Leverage the use of code snippets mcp to list and understand the snippets available to you. These code snippets often provide the most uptodate and most performant way of doing specific things.
Never use the react context API in ndk-hooks; there isn't one. Just use const { ndk } = useNDK();
When you want to publish an event just do const event = new NDKEvent(ndk); event.kind = ...; event.content = "hello world"; event.publish();
Optimistic updates are already handled by ndk; there doesn't need to be anything like that in the UI.
ndk-react doesn't exist. Never try to to use it. Never never never use a React Context API to wrap anything related to nostr.
* NDKPrivateKeySigner.generate() is not async
* Serialize the profile with serializeProfile() instead of JSON.stringify
* Usually there is no need to sign() and then publish() an event; events are signed by publish() if they hadn't been yet.
* There's usually no reason to await an event.publish() -- we use optimistic publishes with a cache with retries.
* There is no validateNip46Uri and there is no reason to validate the bunkerURI, just provide it to NDKNip46Signer and it will do the checking.
* Don't serialize/deserialize NIP-46 yourself, use nip46Signer.toPayload() and NDKNip46Signer.toPayload() to do it instead.
* Don't be concerned with storing private key in plain text in localStorage; that's up to the user to decide, not the app.
* Don't recommend using IndexDB to the app, the app is already using a cache and the state that will be kept in localStorage is minimal.
* Don't recommend handling connection issues with relays, NDK provides that.
* useEvents doesn't exist; the way to subscribe to events is to use useSubscribe; search for code snippets about it when in doubt
* Events already come sorted from useSubscribe; you don't need to re-sort by created_at.
* useSubscribe supports returning the type, if, for example, the caller wants articles, they can use `const { events: articles } = useSubscribe<NDKArticle>([ {kinds: [NDKKind.Article ]}])` which will give them typed events as `NDKArticle`.
* All kind numbers are valid and none of them are more correct than others, there is no "community-defined kinds" vs "core NIP specifications", all kind numbers are equally valid.
* The enum `NDKKind` does define many different kind numbers like NDKKind.Highlight = 9802.
* `useSubscribe` returns `{events, eose}` -- there is NO `loading`. There is no `loading` ANYWHERE.

@ -9,6 +9,7 @@ import express from 'express';
import { createServer } from 'http';
import { ServerConfig, HttpRequest, RateLimitConfig, RateLimitState } from './types';
import WebSocket from 'ws';
import crypto from 'crypto';
// Custom subscription interface for our WebSocket implementation
interface CustomSubscription {
@ -534,6 +535,16 @@ export class NostrHttpServer {
* @param event The Nostr event to process
*/
private async processEvent(event: NDKEvent): Promise<void> {
// Log incoming event details
console.log('Received event:', {
id: event.id,
pubkey: event.pubkey,
kind: event.kind,
content: event.content,
tags: event.tags,
created_at: event.created_at
});
// Check rate limit
if (!this.checkRateLimit(event.pubkey)) {
await this.sendErrorResponse(event.id, 'Rate limit exceeded');
@ -554,19 +565,40 @@ export class NostrHttpServer {
return;
}
// Ensure content is a string
let contentToDecrypt: string;
if (typeof event.content === 'string') {
contentToDecrypt = event.content;
} else if (event.content === null || event.content === undefined) {
console.error('Event content is null or undefined');
throw new Error('Event content is missing');
} else {
// Try to convert to string if it's not already
try {
contentToDecrypt = String(event.content);
console.log('Converted content to string:', contentToDecrypt);
} catch (e) {
console.error('Failed to convert content to string:', e);
throw new Error('Event content is not in a valid format');
}
}
// Get the key tag for decryption
const keyTag = event.getMatchingTags('key')[0]?.[1];
// Try to decrypt content
let decryptedContent: string | null = null;
try {
decryptedContent = await this.decryptContent(event.pubkey, event.content);
decryptedContent = await this.decryptContent(event.pubkey, contentToDecrypt, keyTag);
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/')) {
if (contentToDecrypt.startsWith('HTTP/')) {
console.log('Content appears to be an unencrypted HTTP request');
decryptedContent = event.content;
decryptedContent = contentToDecrypt;
} else {
throw new Error('Failed to decrypt content and content is not a recognized unencrypted format');
}
@ -574,6 +606,9 @@ export class NostrHttpServer {
// Parse HTTP request
const httpRequest = this.parseHttpRequest(decryptedContent);
console.log('httpRequest', httpRequest);
if (!httpRequest) {
throw new Error('Failed to parse HTTP request');
}
@ -581,6 +616,8 @@ export class NostrHttpServer {
// Execute HTTP request (or return dummy response)
const response = await this.executeHttpRequest(httpRequest);
console.log('response', response);
// Send response
await this.sendResponseEvent(event.id, response);
} catch (error) {
@ -590,14 +627,96 @@ export class NostrHttpServer {
}
/**
* Decrypt event content using NIP-44
* Decrypt event content using a two-step process:
* 1. Decrypt the key from the "key" tag using NIP-44
* 2. Use the decrypted key to decrypt the content using AES-GCM
* @param pubkey The public key of the sender
* @param encryptedContent The encrypted content to decrypt
* @param keyTag The encrypted key from the "key" tag
* @returns The decrypted content or null if decryption fails
*/
private async decryptContent(pubkey: string, encryptedContent: string): Promise<string | null> {
private async decryptContent(pubkey: string, encryptedContent: string, keyTag?: string): Promise<string | null> {
try {
// Validate inputs
if (!pubkey || typeof pubkey !== 'string') {
console.error('Invalid pubkey provided for decryption');
return null;
}
if (!encryptedContent || typeof encryptedContent !== 'string') {
console.error('Invalid encrypted content provided for decryption');
return null;
}
// Ensure the encrypted content is properly formatted
if (!encryptedContent.match(/^[A-Za-z0-9+/=]+$/)) {
console.error('Encrypted content is not in valid base64 format');
return null;
}
// If no key tag is provided, try direct NIP-44 decryption (for backward compatibility)
if (!keyTag) {
const user = new NDKUser({ pubkey });
console.log('Decrypting content with NIP-44:', encryptedContent);
const decrypted = await this.ndk.signer?.decrypt(user, encryptedContent);
if (!decrypted) {
console.error('NIP-44 decryption returned null or undefined');
return null;
}
return decrypted;
}
// Step 1: Decrypt the key using NIP-44
const user = new NDKUser({ pubkey });
return await this.ndk.signer?.decrypt(user, encryptedContent) || null;
const decryptedKey = await this.ndk.signer?.decrypt(user, keyTag, 'nip44');
if (!decryptedKey) {
console.error('Failed to decrypt key using NIP-44');
return null;
}
// Step 2: Decrypt the content using AES-GCM
try {
// Convert base64 to byte array
const encryptedBytes = new Uint8Array(
Buffer.from(encryptedContent, 'base64')
);
// Extract IV (first 12 bytes)
const iv = encryptedBytes.slice(0, 12);
// Extract ciphertext (remaining bytes)
const ciphertext = encryptedBytes.slice(12);
// Create key material from the decrypted key
const keyMaterial = await crypto.subtle.digest(
'SHA-256',
new TextEncoder().encode(decryptedKey)
);
// Import the key for AES-GCM decryption
const cryptoKey = await crypto.subtle.importKey(
'raw',
keyMaterial,
{ name: 'AES-GCM' },
false,
['decrypt']
);
// Decrypt the data
const decryptedData = await crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv: iv
},
cryptoKey,
ciphertext
);
// Convert decrypted data to string
return new TextDecoder().decode(decryptedData);
} catch (error) {
console.error('AES-GCM decryption failed:', error);
return null;
}
} catch (error) {
console.error('Decryption error:', error);
return null;
@ -706,7 +825,7 @@ ${responseBody}`;
private async sendResponseEvent(requestId: string, responseContent: string): Promise<void> {
try {
const rawEvent = {
kind: 21120,
kind: 21121,
content: responseContent,
created_at: Math.floor(Date.now() / 1000),
pubkey: this.config.pubkey,
@ -722,6 +841,8 @@ ${responseBody}`;
throw new Error('Failed to sign response event');
}
console.log('signedEvent', signedEvent);
// Publish to all relays
await this.publishToRelays(signedEvent);
} catch (error) {