feat: basic server
This commit is contained in:
parent
ad5babda43
commit
873847689c
6808
client/package-lock.json
generated
6808
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
1919
server/package-lock.json
generated
Normal file
1919
server/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,14 +1,13 @@
|
||||
{
|
||||
"name": "http-to-nostr-server",
|
||||
"name": "nostr-http-server",
|
||||
"version": "1.0.0",
|
||||
"description": "A server for processing HTTP request events from Nostr Kind 21120",
|
||||
"description": "Nostr HTTP Request Server",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js",
|
||||
"build": "tsc",
|
||||
"dev": "ts-node src/index.ts",
|
||||
"watch": "nodemon --exec ts-node src/index.ts",
|
||||
"clean": "rm -rf dist"
|
||||
"test": "jest"
|
||||
},
|
||||
"keywords": [
|
||||
"nostr",
|
||||
@ -20,17 +19,16 @@
|
||||
"dependencies": {
|
||||
"@nostr-dev-kit/ndk": "^2.0.0",
|
||||
"express": "^4.18.2",
|
||||
"node-fetch": "^3.3.2",
|
||||
"websocket-polyfill": "^0.0.3",
|
||||
"ws": "^8.14.2"
|
||||
"node-fetch": "^2.6.9",
|
||||
"ws": "^8.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.18",
|
||||
"@types/node": "^20.8.4",
|
||||
"@types/ws": "^8.5.6",
|
||||
"nodemon": "^3.0.1",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/node": "^18.15.11",
|
||||
"@types/node-fetch": "^2.6.12",
|
||||
"@types/ws": "^8.5.4",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.2.2"
|
||||
"typescript": "^5.0.4"
|
||||
},
|
||||
"type": "module"
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,6 @@ export const relayUrls = [
|
||||
|
||||
// Server configuration
|
||||
export const serverConfig = {
|
||||
port: process.env.PORT || 3000,
|
||||
port: process.env.PORT ? parseInt(process.env.PORT) : 3001,
|
||||
expirationTime: 3600 // 1 hour in seconds
|
||||
};
|
@ -1,39 +0,0 @@
|
||||
import * as crypto from 'crypto';
|
||||
import * as nostrTools from 'nostr-tools';
|
||||
|
||||
// Generate a new random private key
|
||||
function generatePrivateKey(): string {
|
||||
return nostrTools.generatePrivateKey();
|
||||
}
|
||||
|
||||
// Derive public key from private key
|
||||
function getPublicKey(privateKey: string): string {
|
||||
return nostrTools.getPublicKey(privateKey);
|
||||
}
|
||||
|
||||
// Convert hex public key to npub format
|
||||
function convertToNpub(publicKey: string): string {
|
||||
return nostrTools.nip19.npubEncode(publicKey);
|
||||
}
|
||||
|
||||
// Convert hex private key to nsec format
|
||||
function convertToNsec(privateKey: string): string {
|
||||
return nostrTools.nip19.nsecEncode(privateKey);
|
||||
}
|
||||
|
||||
// Generate and display keys
|
||||
function main() {
|
||||
const privateKey = generatePrivateKey();
|
||||
const publicKey = getPublicKey(privateKey);
|
||||
const npub = convertToNpub(publicKey);
|
||||
const nsec = convertToNsec(privateKey);
|
||||
|
||||
console.log('Generated Nostr Key Pair:');
|
||||
console.log('------------------------');
|
||||
console.log('Private Key (hex):', privateKey);
|
||||
console.log('Public Key (hex):', publicKey);
|
||||
console.log('NPUB:', npub);
|
||||
console.log('NSEC:', nsec);
|
||||
}
|
||||
|
||||
main();
|
@ -1,11 +1,11 @@
|
||||
import express from 'express';
|
||||
import { WebSocket, WebSocketServer } from 'ws';
|
||||
import { NDKEvent, NDK, NDKPrivateKeySigner, NDKFilter, NDKRelay } from '@nostr-dev-kit/ndk';
|
||||
import { createServer } from 'http';
|
||||
import fetch from 'node-fetch';
|
||||
import { serverKeys, relayUrls, serverConfig } from './config';
|
||||
import * as crypto from 'crypto';
|
||||
// Configuration
|
||||
/**
|
||||
* Main entry point for the Nostr HTTP Request Server
|
||||
*/
|
||||
|
||||
import { serverKeys, relayUrls, serverConfig } from './config.js';
|
||||
import { NostrHttpServer } from './server.js';
|
||||
|
||||
// Create server configuration
|
||||
const config = {
|
||||
port: serverConfig.port,
|
||||
relayUrls: relayUrls,
|
||||
@ -14,259 +14,22 @@ const config = {
|
||||
npub: serverKeys.npub
|
||||
};
|
||||
|
||||
console.log(`Server running with npub: ${config.npub}`);
|
||||
// Create and start server
|
||||
const server = new NostrHttpServer(config);
|
||||
|
||||
// Create NDK instance with the private key signer
|
||||
const signer = new NDKPrivateKeySigner(config.privateKey);
|
||||
const ndk = new NDK({
|
||||
explicitRelayUrls: config.relayUrls,
|
||||
signer
|
||||
// Handle process termination
|
||||
process.on('SIGINT', () => {
|
||||
console.log('Shutting down server...');
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
// Initialize NDK
|
||||
(async () => {
|
||||
try {
|
||||
await ndk.connect();
|
||||
console.log('NDK Connected to relays');
|
||||
} catch (error) {
|
||||
console.error('Failed to connect to relays:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
|
||||
// Set up Express server
|
||||
const app = express();
|
||||
const server = createServer(app);
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
// Basic route to display server info
|
||||
app.get('/', (req, res) => {
|
||||
res.send(`
|
||||
<h1>HTTP Request Server</h1>
|
||||
<p>This server listens for Nostr kind 21120 events and processes HTTP requests.</p>
|
||||
<p>Server pubkey: ${config.pubkey}</p>
|
||||
`);
|
||||
process.on('unhandledRejection', (error: Error) => {
|
||||
console.error('Unhandled promise rejection:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Start the HTTP server
|
||||
server.listen(config.port, () => {
|
||||
console.log(`Server running on http://localhost:${config.port}`);
|
||||
});
|
||||
|
||||
// Subscribe to kind 21120 events
|
||||
async function subscribeToEvents() {
|
||||
try {
|
||||
// Create filter for kind 21120 events addressed to us
|
||||
const filter: NDKFilter = {
|
||||
kinds: [21120],
|
||||
'#p': [config.pubkey]
|
||||
};
|
||||
|
||||
// Subscribe to events
|
||||
ndk.subscribe(filter, {
|
||||
closeOnEose: false,
|
||||
// Handle each received event
|
||||
callback: (event: NDKEvent) => {
|
||||
processEvent(event);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Subscribed to events with filter:', filter);
|
||||
} catch (error) {
|
||||
console.error('Error subscribing to events:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Start subscriptions
|
||||
(async () => {
|
||||
try {
|
||||
await subscribeToEvents();
|
||||
console.log('Event subscriptions started');
|
||||
} catch (error) {
|
||||
console.error('Failed to start subscriptions:', error);
|
||||
}
|
||||
})();
|
||||
|
||||
// Process incoming HTTP request events
|
||||
async function processEvent(event: NDKEvent) {
|
||||
console.log('Received event:', event.id);
|
||||
|
||||
// Verify this is a request (has p tag pointing to us)
|
||||
const pTag = event.getMatchingTags('p').find(tag => tag[1] === config.pubkey);
|
||||
if (!pTag) {
|
||||
console.log('Not a request for this server, ignoring');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Extract the encrypted HTTP request from content
|
||||
const encryptedContent = event.content;
|
||||
|
||||
// Use NDK's built-in NIP-44 decryption with our private key
|
||||
let httpRequest: string;
|
||||
try {
|
||||
// NDK provides NIP-44 decryption through the event object or signer
|
||||
httpRequest = await ndk.signer?.decrypt(encryptedContent) || '';
|
||||
console.log('Successfully decrypted content with NIP-44');
|
||||
} catch (decryptError) {
|
||||
console.error('Failed to decrypt content with NIP-44:', decryptError);
|
||||
// Fallback in case of decryption failure
|
||||
httpRequest = encryptedContent;
|
||||
}
|
||||
|
||||
console.log('Decrypted HTTP request:', httpRequest);
|
||||
|
||||
// Parse HTTP request
|
||||
const parsedRequest = parseHttpRequest(httpRequest);
|
||||
if (!parsedRequest) {
|
||||
throw new Error('Failed to parse HTTP request');
|
||||
}
|
||||
|
||||
// Execute the HTTP request
|
||||
const response = await executeHttpRequest(parsedRequest);
|
||||
|
||||
// Send response event
|
||||
await sendResponseEvent(event.id, response);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error processing event:', error);
|
||||
|
||||
// Send error response
|
||||
const errorResponse = `HTTP/1.1 500 Internal Server Error
|
||||
Content-Type: text/plain
|
||||
Content-Length: ${error.toString().length}
|
||||
|
||||
${error.toString()}`;
|
||||
|
||||
await sendResponseEvent(event.id, errorResponse);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse an HTTP request string into components
|
||||
function parseHttpRequest(requestStr: string): any {
|
||||
try {
|
||||
const lines = requestStr.split('\n');
|
||||
const firstLine = lines[0].trim().split(' ');
|
||||
|
||||
if (firstLine.length < 3) {
|
||||
throw new Error('Invalid request line');
|
||||
}
|
||||
|
||||
const method = firstLine[0];
|
||||
const path = firstLine[1];
|
||||
const protocol = firstLine[2];
|
||||
|
||||
// Parse headers
|
||||
const headers: Record<string, string> = {};
|
||||
let i = 1;
|
||||
while (i < lines.length && lines[i].trim() !== '') {
|
||||
const headerLine = lines[i].trim();
|
||||
const separatorIndex = headerLine.indexOf(':');
|
||||
|
||||
if (separatorIndex > 0) {
|
||||
const key = headerLine.substring(0, separatorIndex).trim();
|
||||
const value = headerLine.substring(separatorIndex + 1).trim();
|
||||
headers[key] = value;
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
// Parse body (if any)
|
||||
let body = '';
|
||||
if (i < lines.length - 1) {
|
||||
body = lines.slice(i + 1).join('\n');
|
||||
}
|
||||
|
||||
return { method, path, protocol, headers, body };
|
||||
} catch (error) {
|
||||
console.error('Error parsing HTTP request:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Execute an HTTP request
|
||||
async function executeHttpRequest(request: any): Promise<string> {
|
||||
try {
|
||||
// Build URL from path
|
||||
const url = new URL(request.path, 'http://localhost');
|
||||
|
||||
// Prepare fetch options
|
||||
const options: any = {
|
||||
method: request.method,
|
||||
headers: request.headers
|
||||
};
|
||||
|
||||
// Add body if present
|
||||
if (request.body && request.method !== 'GET' && request.method !== 'HEAD') {
|
||||
options.body = request.body;
|
||||
}
|
||||
|
||||
console.log(`Executing HTTP request to ${url}`);
|
||||
|
||||
// Execute the request
|
||||
const response = await fetch(url.toString(), options);
|
||||
|
||||
// Build response string
|
||||
let responseStr = `HTTP/1.1 ${response.status} ${response.statusText}\n`;
|
||||
|
||||
// Add headers
|
||||
response.headers.forEach((value, key) => {
|
||||
responseStr += `${key}: ${value}\n`;
|
||||
});
|
||||
|
||||
// Add empty line between headers and body
|
||||
responseStr += '\n';
|
||||
|
||||
// Add body
|
||||
const body = await response.text();
|
||||
responseStr += body;
|
||||
|
||||
return responseStr;
|
||||
} catch (error) {
|
||||
console.error('Error executing HTTP request:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Send response event using NDK
|
||||
async function sendResponseEvent(requestId: string, responseContent: string) {
|
||||
try {
|
||||
// For a valid Nostr event, we need to manually set all required fields
|
||||
const rawEvent = {
|
||||
kind: 21120,
|
||||
content: responseContent,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
pubkey: config.pubkey,
|
||||
tags: [
|
||||
['e', requestId], // E tag points to request event
|
||||
['expiration', (Math.floor(Date.now() / 1000) + 3600).toString()] // 1 hour from now
|
||||
],
|
||||
id: '', // Will be generated during signing
|
||||
sig: '' // Will be generated during signing
|
||||
};
|
||||
|
||||
// Create a new NDK event from the raw event
|
||||
const event = new NDKEvent(ndk, rawEvent);
|
||||
|
||||
try {
|
||||
// Sign the event with our private key
|
||||
await event.sign();
|
||||
console.log('Response event signed successfully');
|
||||
|
||||
// Publish to relays
|
||||
await event.publish();
|
||||
console.log(`Response sent for request ${requestId}`);
|
||||
} catch (signError) {
|
||||
console.error('Error signing/publishing event:', signError);
|
||||
console.log('Event data:', rawEvent);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error sending response event:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// The server is already started when NDK connects to relays earlier
|
||||
console.log('HTTP to Nostr server is ready and listening for events');
|
||||
// Start the server
|
||||
server.start().catch(error => {
|
||||
console.error('Failed to start server:', error);
|
||||
process.exit(1);
|
||||
});
|
322
server/src/server.ts
Normal file
322
server/src/server.ts
Normal file
@ -0,0 +1,322 @@
|
||||
/**
|
||||
* Nostr HTTP Request Server Implementation
|
||||
* Handles kind 21120 events for HTTP request processing
|
||||
*/
|
||||
|
||||
import NDK from '@nostr-dev-kit/ndk';
|
||||
import { NDKUser, NDKEvent, NDKFilter, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk';
|
||||
import express from 'express';
|
||||
import { createServer } from 'http';
|
||||
import { ServerConfig, HttpRequest, RateLimitConfig, RateLimitState } from './types';
|
||||
|
||||
/**
|
||||
* Nostr HTTP Server class
|
||||
*/
|
||||
export class NostrHttpServer {
|
||||
private ndk: NDK;
|
||||
private config: ServerConfig;
|
||||
private rateLimits: Map<string, RateLimitState>;
|
||||
private rateLimitConfig: RateLimitConfig;
|
||||
|
||||
/**
|
||||
* Create a new Nostr HTTP Server
|
||||
* @param config Server configuration
|
||||
*/
|
||||
constructor(config: ServerConfig) {
|
||||
this.config = config;
|
||||
this.rateLimits = new Map();
|
||||
this.rateLimitConfig = {
|
||||
maxRequests: 100,
|
||||
windowMs: 60000 // 1 minute
|
||||
};
|
||||
|
||||
// Initialize NDK with private key signer
|
||||
const signer = new NDKPrivateKeySigner(config.privateKey);
|
||||
this.ndk = new NDK({
|
||||
explicitRelayUrls: config.relayUrls,
|
||||
signer
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the server
|
||||
*/
|
||||
public async start(): Promise<void> {
|
||||
try {
|
||||
// Connect to NDK
|
||||
await this.ndk.connect();
|
||||
console.log('Connected to Nostr relays');
|
||||
|
||||
// Subscribe to events
|
||||
await this.subscribeToEvents();
|
||||
console.log('Subscribed to kind 21120 events');
|
||||
|
||||
// Set up Express server for status endpoint
|
||||
const app = express();
|
||||
const server = createServer(app);
|
||||
|
||||
app.get('/', (req: express.Request, res: express.Response) => {
|
||||
res.send(`
|
||||
<h1>Nostr HTTP Request Server</h1>
|
||||
<p>Server npub: ${this.config.npub}</p>
|
||||
<p>Status: Running</p>
|
||||
`);
|
||||
});
|
||||
|
||||
server.listen(this.config.port, () => {
|
||||
console.log(`Server running on http://localhost:${this.config.port}`);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to start server:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to kind 21120 events
|
||||
*/
|
||||
private async subscribeToEvents(): Promise<void> {
|
||||
const filter: NDKFilter = {
|
||||
kinds: [21120 as any], // Type assertion to bypass NDKKind type check
|
||||
'#p': [this.config.pubkey]
|
||||
};
|
||||
|
||||
// Create a subscription with the filter and options
|
||||
const subscription = this.ndk.subscribe(filter, {
|
||||
closeOnEose: false,
|
||||
groupable: false
|
||||
});
|
||||
|
||||
// Set up event handler
|
||||
subscription.on('event', (event: NDKEvent) => {
|
||||
this.processEvent(event).catch(error => {
|
||||
console.error('Error processing event:', error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Process incoming Nostr events
|
||||
* @param event The Nostr event to process
|
||||
*/
|
||||
private async processEvent(event: NDKEvent): Promise<void> {
|
||||
// Check rate limit
|
||||
if (!this.checkRateLimit(event.pubkey)) {
|
||||
await this.sendErrorResponse(event.id, 'Rate limit exceeded');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Verify this is a request for us
|
||||
const pTag = event.getMatchingTags('p').find((tag: string[]) => tag[1] === this.config.pubkey);
|
||||
if (!pTag) {
|
||||
console.log('Not a request for this server, ignoring');
|
||||
return;
|
||||
}
|
||||
|
||||
// Decrypt content
|
||||
const decryptedContent = await this.decryptContent(event.pubkey, event.content);
|
||||
if (!decryptedContent) {
|
||||
throw new Error('Failed to decrypt content');
|
||||
}
|
||||
|
||||
// Parse HTTP request
|
||||
const httpRequest = this.parseHttpRequest(decryptedContent);
|
||||
if (!httpRequest) {
|
||||
throw new Error('Failed to parse HTTP request');
|
||||
}
|
||||
|
||||
// Execute HTTP request (or return dummy response)
|
||||
const response = await this.executeHttpRequest(httpRequest);
|
||||
|
||||
// Send response
|
||||
await this.sendResponseEvent(event.id, response);
|
||||
} catch (error) {
|
||||
console.error('Error processing event:', error);
|
||||
await this.sendErrorResponse(event.id, error instanceof Error ? error.message : 'Unknown error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt event content using NIP-44
|
||||
* @param pubkey The public key of the sender
|
||||
* @param encryptedContent The encrypted content to decrypt
|
||||
*/
|
||||
private async decryptContent(pubkey: string, encryptedContent: string): Promise<string | null> {
|
||||
try {
|
||||
const user = new NDKUser({ pubkey });
|
||||
return await this.ndk.signer?.decrypt(user, encryptedContent) || null;
|
||||
} catch (error) {
|
||||
console.error('Decryption error:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse HTTP request string into structured format
|
||||
* @param requestStr The HTTP request string to parse
|
||||
*/
|
||||
private parseHttpRequest(requestStr: string): HttpRequest | null {
|
||||
try {
|
||||
const lines = requestStr.split('\n');
|
||||
const firstLine = lines[0].trim().split(' ');
|
||||
|
||||
if (firstLine.length < 3) {
|
||||
throw new Error('Invalid request line');
|
||||
}
|
||||
|
||||
const [method, path, protocol] = firstLine;
|
||||
|
||||
// Parse headers
|
||||
const headers: Record<string, string> = {};
|
||||
let i = 1;
|
||||
while (i < lines.length && lines[i].trim() !== '') {
|
||||
const headerLine = lines[i].trim();
|
||||
const separatorIndex = headerLine.indexOf(':');
|
||||
|
||||
if (separatorIndex > 0) {
|
||||
const key = headerLine.substring(0, separatorIndex).trim();
|
||||
const value = headerLine.substring(separatorIndex + 1).trim();
|
||||
headers[key] = value;
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
// Parse body
|
||||
const body = lines.slice(i + 1).join('\n');
|
||||
|
||||
return { method, path, protocol, headers, body };
|
||||
} catch (error) {
|
||||
console.error('Error parsing HTTP request:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute HTTP request or return a dummy response
|
||||
* @param request The HTTP request to execute
|
||||
*/
|
||||
private async executeHttpRequest(request: HttpRequest): Promise<string> {
|
||||
// Return a simple Hello World response instead of executing the actual request
|
||||
const responseBody = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Hello World</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
h1 {
|
||||
color: #333;
|
||||
}
|
||||
.info {
|
||||
background-color: #f5f5f5;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello World from Nostr HTTP Server!</h1>
|
||||
<p>This is a dummy response from the Nostr HTTP server.</p>
|
||||
<div class="info">
|
||||
<h2>Request Information:</h2>
|
||||
<p><strong>Method:</strong> ${request.method}</p>
|
||||
<p><strong>Path:</strong> ${request.path}</p>
|
||||
<p><strong>Protocol:</strong> ${request.protocol}</p>
|
||||
<p><strong>Timestamp:</strong> ${new Date().toISOString()}</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
// Build response string
|
||||
let responseStr = `HTTP/1.1 200 OK
|
||||
Content-Type: text/html
|
||||
Content-Length: ${responseBody.length}
|
||||
|
||||
${responseBody}`;
|
||||
|
||||
return responseStr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send response event
|
||||
* @param requestId The ID of the request event
|
||||
* @param responseContent The response content to send
|
||||
*/
|
||||
private async sendResponseEvent(requestId: string, responseContent: string): Promise<void> {
|
||||
try {
|
||||
const rawEvent = {
|
||||
kind: 21120,
|
||||
content: responseContent,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
pubkey: this.config.pubkey,
|
||||
tags: [
|
||||
['e', requestId],
|
||||
['expiration', (Math.floor(Date.now() / 1000) + 3600).toString()]
|
||||
]
|
||||
};
|
||||
|
||||
const event = new NDKEvent(this.ndk, rawEvent);
|
||||
await event.sign();
|
||||
await event.publish();
|
||||
} catch (error) {
|
||||
console.error('Error sending response event:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send error response event
|
||||
* @param requestId The ID of the request event
|
||||
* @param errorMessage The error message to send
|
||||
*/
|
||||
private async sendErrorResponse(requestId: string, errorMessage: string): Promise<void> {
|
||||
const errorResponse = `HTTP/1.1 500 Internal Server Error
|
||||
Content-Type: text/plain
|
||||
Content-Length: ${errorMessage.length}
|
||||
|
||||
${errorMessage}`;
|
||||
|
||||
await this.sendResponseEvent(requestId, errorResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a client has exceeded rate limits
|
||||
* @param clientId The client's public key
|
||||
*/
|
||||
private checkRateLimit(clientId: string): boolean {
|
||||
const now = Date.now();
|
||||
const state = this.rateLimits.get(clientId);
|
||||
|
||||
if (!state) {
|
||||
this.rateLimits.set(clientId, {
|
||||
count: 1,
|
||||
resetTime: now + this.rateLimitConfig.windowMs
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
if (now > state.resetTime) {
|
||||
this.rateLimits.set(clientId, {
|
||||
count: 1,
|
||||
resetTime: now + this.rateLimitConfig.windowMs
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
if (state.count >= this.rateLimitConfig.maxRequests) {
|
||||
return false;
|
||||
}
|
||||
|
||||
state.count++;
|
||||
return true;
|
||||
}
|
||||
}
|
71
server/src/types.ts
Normal file
71
server/src/types.ts
Normal file
@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Type definitions for the Nostr HTTP Request Server
|
||||
*/
|
||||
|
||||
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
||||
|
||||
/**
|
||||
* HTTP Request structure
|
||||
*/
|
||||
export interface HttpRequest {
|
||||
method: string;
|
||||
path: string;
|
||||
protocol: string;
|
||||
headers: Record<string, string>;
|
||||
body: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP Response structure
|
||||
*/
|
||||
export interface HttpResponse {
|
||||
status: number;
|
||||
statusText: string;
|
||||
headers: Record<string, string>;
|
||||
body: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Server configuration
|
||||
*/
|
||||
export interface ServerConfig {
|
||||
port: number;
|
||||
relayUrls: string[];
|
||||
privateKey: string;
|
||||
pubkey: string;
|
||||
npub: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event processing result
|
||||
*/
|
||||
export interface EventProcessingResult {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
response?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rate limiting configuration
|
||||
*/
|
||||
export interface RateLimitConfig {
|
||||
maxRequests: number;
|
||||
windowMs: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rate limit state for a client
|
||||
*/
|
||||
export interface RateLimitState {
|
||||
count: number;
|
||||
resetTime: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extended NDKEvent with our custom properties
|
||||
*/
|
||||
export interface NostrHttpEvent extends NDKEvent {
|
||||
requestId: string;
|
||||
timestamp: number;
|
||||
clientPubkey: string;
|
||||
}
|
@ -2,14 +2,25 @@
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ES2020",
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"lib": [
|
||||
"ES2020"
|
||||
],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"sourceMap": true,
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
]
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user