This commit is contained in:
n 2025-04-07 11:32:04 +01:00
parent 7939eee96f
commit f2efd198f8
12 changed files with 851 additions and 339 deletions

@ -1,5 +1,5 @@
always use Typescript where relevant
do everything in TypeScript (not vanilla Javascript)
don't try to modify files in the build folder (eg /dist)
don't put css or JS in the index.html - we should follow appropriate CSP policies
don't put any css or javascript blocks in the index.html file
run the lint after every code update
don't ever use the alert function

144
client/help.html Normal file

@ -0,0 +1,144 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Implement strict Content Security Policy -->
<title>HTTP Messages - Documentation</title>
<!-- Load our CSS file -->
<link rel="stylesheet" href="./styles.css">
<!-- Include the Nostr extensions - these will be accessed via window.nostr -->
<script defer src="./bundle.js"></script>
</head>
<body>
<!-- Theme toggle button -->
<div class="theme-toggle-container">
<button id="themeToggleBtn" class="theme-toggle-btn">
<span id="themeIcon">🌙</span>
<span id="themeText">Dark Mode</span>
</button>
</div>
<h1>HTTP Messages</h1>
<!-- Navigation -->
<div class="navigation">
<a href="./index.html" class="nav-link">Home</a>
<a href="./help.html" class="nav-link active">Documentation</a>
</div>
<!-- Documentation Content -->
<div class="content">
<div class="info-box">
<p>A specification for sending/receiving HTTP messages (request/response) via a remote server. Header / Body etc are encrypted, either in the <code>content</code> (small messages) or via a blossom server (larger requests).</p>
</div>
<div class="diagram-container">
<img src="../http.png" alt="HTTP Messages Architecture Diagram">
<p class="diagram-caption">HTTP Messages Architecture Overview</p>
</div>
<section class="section">
<h2>Overview</h2>
<p>HTTP Messages enables a local client to make and receive HTTP requests (PUT, POST, GET, PATCH etc) from a remote computer. This approach provides enhanced privacy, anonymity, and resistance to censorship.</p>
<p>The system requires:</p>
<ul>
<li>A trusted machine to process the messages (can be a home PC or Raspberry Pi)</li>
<li>A relay (can be untrusted)</li>
<li>A blossom server (can be untrusted)</li>
</ul>
</section>
<section class="section">
<h2>How It Works</h2>
<p>HTTP Messages operates by converting standard HTTP requests into encrypted Nostr events (kind 21120) that can be safely transmitted through untrusted relays. The process ensures privacy and security while enabling communication with the regular internet.</p>
<div class="features-grid">
<div class="feature-card">
<h3>Privacy Protection</h3>
<p>All HTTP headers and body content are encrypted, protecting sensitive information from intermediaries.</p>
</div>
<div class="feature-card">
<h3>Censorship Resistance</h3>
<p>By routing through Nostr relays, the system can bypass traditional censorship mechanisms that block direct connections.</p>
</div>
<div class="feature-card">
<h3>Server Anonymity</h3>
<p>No domain needed or port forwarding required, keeping your server completely private.</p>
</div>
<div class="feature-card">
<h3>Flexible Architecture</h3>
<p>Works with small payloads (embedded in content) or large requests (via blossom server).</p>
</div>
</div>
</section>
<section class="section">
<h2>Architecture</h2>
<p>The HTTP Messages architecture consists of several components working together:</p>
<h3>Components</h3>
<ul>
<li><strong>Nostr Client</strong>: Converts HTTP requests into kind 21120 events and handles responses</li>
<li><strong>Nostr Relay</strong>: Transmits events between clients and servers (untrusted)</li>
<li><strong>Blossom Storage</strong>: Stores larger payloads that don't fit in event content (untrusted)</li>
<li><strong>Trusted Device</strong>: Processes encrypted requests, makes actual HTTP calls, and returns responses</li>
</ul>
<h3>Process Flow</h3>
<ol>
<li>Client converts HTTP request into kind 21120 event</li>
<li>For large payloads, data is stored in Blossom server</li>
<li>Event is published to a Nostr relay</li>
<li>Trusted device retrieves the event</li>
<li>Trusted device decrypts event, fetches any blossom payloads if needed</li>
<li>Trusted device makes the actual HTTP request to the target server</li>
<li>Response is encrypted and sent back through the same channel</li>
<li>Client decrypts and processes the response</li>
</ol>
</section>
<section class="section">
<h2>Event Structure</h2>
<p>HTTP Messages uses Nostr kind 21120 events with a specific structure:</p>
<h3>Request Event</h3>
<pre>{
"kind": 21120,
"pubkey": "&lt;pubkey&gt;",
"content": "$encryptedPayload",
"tags": [
["p", "&lt;pubkey of remote server&gt;"], // P tag entry, this is a REQUEST
["key","nip44Encrypt($decryptkey)"],
["r", "https://relay.one"],
["expiration",&lt;unix timestamp&gt;]
]
}</pre>
<h3>Response Event</h3>
<pre>{
"kind": 21120,
"pubkey": "&lt;pubkey&gt;",
"content": "encrypt({'url':'blossom.one','hash':'xx'},$decryptkey)",
"tags": [
["key","nip44Encrypt($decryptkey)"],
["E", "&lt;request event id&gt;"], // E tag entry, this is a RESPONSE
["expiration",&lt;unix timestamp&gt;]
]
}</pre>
</section>
<section class="section">
<h2>Use Cases</h2>
<p>HTTP Messages is particularly useful in scenarios where:</p>
<ul>
<li>Privacy and anonymity are important concerns</li>
<li>Censorship might block direct connections</li>
<li>You want to make regular API calls over Nostr</li>
<li>You need to make open source apps available from inside private networks</li>
<li>You want maximum server privacy with no domain needed or port forwarding</li>
</ul>
</section>
</div>
</body>
</html>

@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Implement strict Content Security Policy -->
<title>HTTP to Kind 21120 Converter</title>
<title>HTTP Messages - Converter</title>
<!-- Load our CSS file -->
<link rel="stylesheet" href="./styles.css">
<!-- Include the Nostr extensions - these will be accessed via window.nostr -->
@ -19,65 +19,68 @@
</button>
</div>
<h1>HTTP Request to Kind 21120 Converter</h1>
<h1>HTTP Messages</h1>
<div class="info-box">
<p>This tool converts HTTP requests into a single Nostr event of kind 21120. The HTTP request is encrypted using NIP-44 in the content field with appropriate tags following the specification.</p>
<!-- Navigation -->
<div class="navigation">
<a href="./index.html" class="nav-link active">Home</a>
<a href="./help.html" class="nav-link">Documentation</a>
</div>
<div class="login-container">
<!-- NostrLogin container will be inserted here -->
<div id="loginStatus" class="login-status"></div>
</div>
<h2>Server Information:</h2>
<div style="margin-bottom: 15px;">
<div style="margin-bottom: 10px;">
<label for="serverPubkey">Server Pubkey or Search Term:</label><br>
<div class="server-input-container">
<input type="text" id="serverPubkey" placeholder="npub, username, or NIP-05 identifier" value="npub1r6knexka25dn9w9jnf5kf8xh6gfq7n3p38zfl7nn7cjjjsp4umcqnk0aun" class="server-input">
<button id="searchServerBtn" class="server-search-button">Search</button>
<!-- Main Content (HTTP Request Converter) -->
<div class="content">
<div class="info-box">
<p>This tool converts HTTP requests into a single Nostr event of kind 21120. The HTTP request is encrypted using NIP-44 in the content field with appropriate tags following the specification.</p>
</div>
<h2>Server Information:</h2>
<div style="margin-bottom: 15px;">
<div style="margin-bottom: 10px;">
<label for="serverPubkey">Server Pubkey or Search Term:</label><br>
<div class="server-input-container">
<input type="text" id="serverPubkey" placeholder="npub, username, or NIP-05 identifier" value="npub1r6knexka25dn9w9jnf5kf8xh6gfq7n3p38zfl7nn7cjjjsp4umcqnk0aun" class="server-input">
<button id="searchServerBtn" class="server-search-button">Search</button>
</div>
<div id="serverSearchResult" class="server-search-result" style="display: none;">
<!-- Search results will be shown here -->
</div>
</div>
<div id="serverSearchResult" class="server-search-result" style="display: none;">
<!-- Search results will be shown here -->
<div>
<label for="relay">Response Relay (optional):</label><br>
<input type="text" id="relay" value="wss://relay.damus.io" style="width: 100%; padding: 8px;">
</div>
</div>
<div>
<label for="relay">Response Relay (optional):</label><br>
<input type="text" id="relay" value="wss://relay.damus.io" style="width: 100%; padding: 8px;">
</div>
</div>
<h2>Enter HTTP Request:</h2>
<textarea id="httpRequest" placeholder="GET /index.html HTTP/1.1
<h2>Enter HTTP Request:</h2>
<textarea id="httpRequest" placeholder="GET /index.html HTTP/1.1
Host: example.com
User-Agent: Browser/1.0
"></textarea>
<button id="convertButton">Convert to Event</button>
<div id="output" hidden>
<h2>Converted Event:</h2>
<pre id="eventOutput"></pre>
<div class="publish-container">
<h2>Publish to Relay:</h2>
<div class="publish-input-container">
<input type="text" id="publishRelay" value="wss://relay.nostrdev.com" placeholder="wss://relay.example.com" class="publish-input">
<button id="publishButton" class="publish-button">Publish Event</button>
</div>
<div id="publishResult" class="publish-result" style="display: none;">
<!-- Publish results will be shown here -->
</div>
</div>
<button id="convertButton">Convert to Event</button>
<div class="qr-container">
<h2>QR Code:</h2>
<div id="qrCode"></div>
<p><small>Scan this QR code to share the Nostr event</small></p>
<div id="output" hidden>
<h2>Converted Event:</h2>
<pre id="eventOutput"></pre>
<div class="publish-container">
<h2>Publish to Relay:</h2>
<div class="publish-input-container">
<input type="text" id="publishRelay" value="wss://relay.nostrdev.com" placeholder="wss://relay.example.com" class="publish-input">
<button id="publishButton" class="publish-button">Publish Event</button>
</div>
<div id="publishResult" class="publish-result" style="display: none;">
<!-- Publish results will be shown here -->
</div>
</div>
<div class="qr-container">
<h2>QR Code:</h2>
<div id="qrCode"></div>
<p><small>Scan this QR code to share the Nostr event</small></p>
</div>
</div>
</div>
</body>

@ -5,7 +5,7 @@
"main": "dist/index.html",
"scripts": {
"build": "webpack --mode production",
"copy-assets": "mkdir -p dist && cp index.html dist/ && cp http.png dist/ 2>/dev/null || true",
"copy-assets": "mkdir -p dist && cp index.html help.html dist/ && cp http.png dist/ 2>/dev/null || true",
"dev": "webpack serve --open",
"start": "npm run build && npm run copy-assets && npx serve dist",
"clean": "rm -rf dist",

@ -1,45 +1,43 @@
// client.ts - External TypeScript file for HTTP to Nostr converter
// This follows strict CSP policies by avoiding inline scripts
console.log('client.ts loaded');
// Import functions from other modules
import { displayConvertedEvent } from './converter';
import { lookupNip05, searchUsers } from './search';
import { publishToRelay, convertNpubToHex, verifyEvent } from './relay';
// Import from Node.js built-ins & external modules
import * as nostrTools from 'nostr-tools';
import {
setDefaultHttpRequest,
sanitizeText,
processTags,
standardizeEvent,
// Import type definitions
import type { NostrEvent } from './converter';
// Import functions from internal modules
import { displayConvertedEvent } from './converter';
import { publishToRelay, convertNpubToHex, verifyEvent } from './relay';
import { searchUsers } from './search';
import {
sanitizeText,
setDefaultHttpRequest,
showError,
showLoading,
showSuccess,
showLoading
standardizeEvent
} from './utils';
// Import nostr-login - use require since the default export might not work with import
const NostrLogin = require('nostr-login');
console.log('NostrLogin library imported:', NostrLogin);
console.log('NostrLogin methods:', Object.keys(NostrLogin));
console.log('NostrLogin init method:', NostrLogin.init);
// Import nostr-login using CommonJS pattern
// eslint-disable-next-line no-undef
const NostrLogin = typeof require !== 'undefined' ? require('nostr-login') : null;
// Check for encryption methods
if (NostrLogin.nip04) console.log('NIP-04 encryption available:', NostrLogin.nip04);
if (NostrLogin.nip44) console.log('NIP-44 encryption available:', NostrLogin.nip44);
if (NostrLogin.encrypt) console.log('Direct encrypt method available:', NostrLogin.encrypt);
console.log('NostrLogin init method:', NostrLogin.init);
// Declare missing globals to prevent linting errors
declare global {
interface Window {
currentSignedEvent?: NostrEvent;
}
}
/**
* Initialize nostr-login
*/
function initNostrLogin() {
console.log('Initializing NostrLogin');
function initNostrLogin(): void {
const loginContainer = document.querySelector('.login-container');
const loginStatusDiv = document.getElementById('loginStatus');
if (!loginContainer || !loginStatusDiv) {
console.error('Login elements not found');
return;
}
@ -51,26 +49,22 @@ function initNostrLogin() {
try {
// Initialize NostrLogin with the container
if (NostrLogin && NostrLogin.init) {
console.log('Initializing NostrLogin.init with container');
NostrLogin.init({
element: nostrLoginContainer,
onConnect: (pubkey: string) => {
console.log('Connected with pubkey:', pubkey);
onConnect: (pubkey: string): void => {
const npub = nostrTools.nip19.npubEncode(pubkey);
loginStatusDiv.innerHTML = `<span style="color: #008800;">Connected as: ${npub.slice(0, 8)}...${npub.slice(-4)}</span>`;
},
onDisconnect: () => {
console.log('Disconnected');
onDisconnect: (): void => {
loginStatusDiv.innerHTML = '<span>Disconnected</span>';
}
});
} else {
console.error('NostrLogin.init is not available');
loginStatusDiv.innerHTML = '<span style="color: #cc0000;">NostrLogin initialization unavailable</span>';
}
} catch (error: any) {
console.error('Failed to initialize NostrLogin:', error);
loginStatusDiv.innerHTML = `<span style="color: #cc0000;">Error initializing Nostr login: ${error.message || String(error)}</span>`;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
loginStatusDiv.innerHTML = `<span style="color: #cc0000;">Error initializing Nostr login: ${errorMessage}</span>`;
}
}
@ -100,7 +94,7 @@ async function handleServerSearch(): Promise<void> {
resultDiv.style.display = 'none';
return;
}
} catch (error) {
} catch {
// Not a valid npub, continue with search
}
}
@ -171,11 +165,10 @@ async function handlePublishEvent(): Promise<void> {
}
// Check if we have a stored event from the creation process
if ((window as any).currentSignedEvent) {
console.log("Using stored signed event from creation process");
if (window.currentSignedEvent) {
try {
const event = (window as any).currentSignedEvent;
// Type assertion needed since window.currentSignedEvent is optional
const event = window.currentSignedEvent as NostrEvent;
const relayUrl = publishRelayInput.value.trim();
if (!relayUrl || !relayUrl.startsWith('wss://')) {
@ -188,16 +181,13 @@ async function handlePublishEvent(): Promise<void> {
try {
const result = await publishToRelay(event, relayUrl);
console.log('Publish success:', result);
showSuccess(publishResultDiv, result);
} catch (publishError) {
console.error('Publish error:', publishError);
showError(publishResultDiv, String(publishError));
// Don't rethrow, just handle it here so the UI shows the error
}
return;
} catch (error) {
console.error("Error using stored event, falling back to parsed event");
} catch {
// Continue with normal flow if this fails
}
}
@ -208,16 +198,12 @@ async function handlePublishEvent(): Promise<void> {
return;
}
console.log('Raw event text:', eventText);
let event;
try {
// Check for non-printable characters or hidden characters that might cause issues
const sanitizedEventText = sanitizeText(eventText);
console.log('Sanitized event text:', sanitizedEventText);
event = JSON.parse(sanitizedEventText);
console.log('Parsed event:', event);
// Validate that it's a proper Nostr event
if (!event || typeof event !== 'object') {
@ -233,7 +219,6 @@ async function handlePublishEvent(): Promise<void> {
if (event.pubkey.startsWith('npub')) {
const hexPubkey = convertNpubToHex(event.pubkey);
if (hexPubkey) {
console.log('Converting npub to hex pubkey...');
event.pubkey = hexPubkey;
} else {
throw new Error('Invalid npub format in pubkey');
@ -241,9 +226,7 @@ async function handlePublishEvent(): Promise<void> {
}
// Create a clean event with exactly the fields we need
console.log("Creating a clean event for publishing...");
event = standardizeEvent(event);
console.log("Clean event created:", JSON.stringify(event, null, 2));
} catch (error) {
showError(publishResultDiv, `Invalid event: ${String(error)}`);
@ -260,33 +243,24 @@ async function handlePublishEvent(): Promise<void> {
showLoading(publishResultDiv, 'Publishing to relay...');
try {
// Check the event structure
console.log('Full event object before publish:', JSON.stringify(event, null, 2));
// Verify event first
console.log('Verifying event...');
const isValid = verifyEvent(event);
console.log('Event verification result:', isValid);
if (!isValid) {
console.log('Event verification failed. Proceeding anyway as the relay will validate.');
// Just continue, the relay will validate
}
// Proceed with publish even if verification failed - the relay will validate
publishResultDiv.innerHTML += '<br><span>Attempting to publish...</span>';
console.log('Attempting to publish event to relay:', relayUrl);
try {
const result = await publishToRelay(event, relayUrl);
console.log('Publish success:', result);
showSuccess(publishResultDiv, result);
} catch (publishError) {
console.error('Publish error:', publishError);
showError(publishResultDiv, String(publishError));
// Don't rethrow, just handle it here so the UI shows the error
}
} catch (error) {
console.error('Error preparing event for publish:', error);
showError(publishResultDiv, `Error preparing event: ${String(error)}`);
}
}
@ -297,7 +271,8 @@ async function handlePublishEvent(): Promise<void> {
function toggleTheme(): void {
const body = document.body;
const themeToggle = document.getElementById('themeToggle');
const themeToggleBtn = document.getElementById('themeToggleBtn');
// Commented out to avoid unused variable error
// const themeToggleBtn = document.getElementById('themeToggleBtn');
const themeIcon = document.getElementById('themeIcon');
const themeText = document.getElementById('themeText');
@ -306,52 +281,92 @@ function toggleTheme(): void {
if (isDarkMode) {
// Switch to light theme
body.removeAttribute('data-theme');
localStorage.setItem('theme', 'light');
window.localStorage.setItem('theme', 'light');
// Update old toggle if it exists
if (themeToggle) {
const toggleText = themeToggle.querySelector('.theme-toggle-text');
const toggleIcon = themeToggle.querySelector('.theme-toggle-icon');
if (toggleText) toggleText.textContent = 'Dark Mode';
if (toggleIcon) toggleIcon.textContent = '🌓';
if (toggleText) {
toggleText.textContent = 'Dark Mode';
}
if (toggleIcon) {
toggleIcon.textContent = '🌓';
}
}
// Update new toggle button if it exists
if (themeIcon) themeIcon.textContent = '🌙';
if (themeText) themeText.textContent = 'Dark Mode';
if (themeIcon) {
themeIcon.textContent = '🌙';
}
if (themeText) {
themeText.textContent = 'Dark Mode';
}
} else {
// Switch to dark theme
body.setAttribute('data-theme', 'dark');
localStorage.setItem('theme', 'dark');
window.localStorage.setItem('theme', 'dark');
// Update old toggle if it exists
if (themeToggle) {
const toggleText = themeToggle.querySelector('.theme-toggle-text');
const toggleIcon = themeToggle.querySelector('.theme-toggle-icon');
if (toggleText) toggleText.textContent = 'Light Mode';
if (toggleIcon) toggleIcon.textContent = '☀️';
if (toggleText) {
toggleText.textContent = 'Light Mode';
}
if (toggleIcon) {
toggleIcon.textContent = '☀️';
}
}
// Update new toggle button if it exists
if (themeIcon) themeIcon.textContent = '☀️';
if (themeText) themeText.textContent = 'Light Mode';
if (themeIcon) {
themeIcon.textContent = '☀️';
}
if (themeText) {
themeText.textContent = 'Light Mode';
}
}
}
/**
* Handle tab switching
*/
function setupTabSwitching(): void {
const tabs = document.querySelectorAll('.tab-btn');
const tabPanes = document.querySelectorAll('.tab-pane');
tabs.forEach(tab => {
tab.addEventListener('click', () => {
// Remove active class from all tabs
tabs.forEach(t => t.classList.remove('active'));
// Add active class to clicked tab
tab.classList.add('active');
// Hide all tab panes
tabPanes.forEach(pane => {
pane.classList.add('hidden');
pane.classList.remove('active');
});
// Show the corresponding tab pane
const targetPane = document.getElementById((tab as HTMLElement).dataset.tab as string);
if (targetPane) {
targetPane.classList.remove('hidden');
targetPane.classList.add('active');
}
});
});
}
// Initialize the event handlers when the DOM is loaded
document.addEventListener('DOMContentLoaded', function(): void {
console.log('DOM content loaded');
// Set up the convert button click handler
const convertButton = document.getElementById('convertButton');
const searchButton = document.getElementById('searchServerBtn');
const publishButton = document.getElementById('publishButton');
// Debug - list all buttons in the document
console.log('All buttons in document:');
document.querySelectorAll('button').forEach((button, index) => {
console.log(`Button ${index}:`, button.id, button.textContent);
});
if (convertButton) {
convertButton.addEventListener('click', displayConvertedEvent);
}
@ -379,7 +394,7 @@ document.addEventListener('DOMContentLoaded', function(): void {
if (toggleElement) {
// Set initial theme based on local storage
const savedTheme = localStorage.getItem('theme');
const savedTheme = window.localStorage.getItem('theme');
if (savedTheme === 'dark') {
document.body.setAttribute('data-theme', 'dark');
@ -387,13 +402,21 @@ document.addEventListener('DOMContentLoaded', function(): void {
if (themeToggleBtn) {
const themeIcon = document.getElementById('themeIcon');
const themeText = document.getElementById('themeText');
if (themeIcon) themeIcon.textContent = '☀️';
if (themeText) themeText.textContent = 'Light Mode';
if (themeIcon) {
themeIcon.textContent = '☀️';
}
if (themeText) {
themeText.textContent = 'Light Mode';
}
} else if (themeToggle) {
const toggleText = themeToggle.querySelector('.theme-toggle-text');
const toggleIcon = themeToggle.querySelector('.theme-toggle-icon');
if (toggleText) toggleText.textContent = 'Light Mode';
if (toggleIcon) toggleIcon.textContent = '☀️';
if (toggleText) {
toggleText.textContent = 'Light Mode';
}
if (toggleIcon) {
toggleIcon.textContent = '☀️';
}
}
}
@ -401,5 +424,6 @@ document.addEventListener('DOMContentLoaded', function(): void {
toggleElement.addEventListener('click', toggleTheme);
}
console.log('HTTP to Nostr converter initialized');
// Setup tab switching
setupTabSwitching();
});

@ -1,21 +1,14 @@
// Type definition for the window.nostrTools object
declare global {
interface Window {
nostrTools: any;
nostr: any;
}
}
import { defaultServerConfig, appSettings } from './config';
// External dependencies
import * as nostrTools from 'nostr-tools';
import qrcode from 'qrcode-generator';
// Internal imports
import { defaultServerConfig, appSettings } from './config';
import { convertNpubToHex } from './relay';
import { processTags, showSuccess } from './utils';
// Import nostr-login for encryption/signing
const NostrLogin = require('nostr-login');
// Define interface for Nostr event
interface NostrEvent {
// Define interface for Nostr event - making id and sig optional for event creation
export interface NostrEvent {
kind: number;
content: string;
tags: string[][];
@ -25,6 +18,29 @@ interface NostrEvent {
sig?: string;
}
// Type definition for the window.nostrTools object
// Define interfaces to avoid unused parameter errors
/* eslint-disable no-unused-vars */
interface NostrWindowExtension {
getPublicKey: () => Promise<string>;
signEvent: (event: NostrEvent) => Promise<NostrEvent>;
nip44?: {
encrypt: (content: string, pubkey: string) => string;
};
}
declare global {
interface Window {
nostrTools: Record<string, unknown>;
nostr: NostrWindowExtension;
currentSignedEvent?: NostrEvent;
}
}
// Import nostr-login for encryption/signing
// eslint-disable-next-line no-undef
const NostrLogin = typeof require !== 'undefined' ? require('nostr-login') : null;
// Generate a keypair for standalone mode (when no extension is available)
let standaloneSecretKey: Uint8Array | null = null;
let standalonePublicKey: string | null = null;
@ -58,7 +74,7 @@ export function convertToEvent(
pubkey: string,
serverPubkey: string,
decryptkey: string,
relay?: string
relayUrl?: string
): string | null {
console.log("convertToEvent called with httpRequest:", httpRequest.substring(0, 50) + "...");
@ -165,12 +181,16 @@ export function convertToEvent(
console.error(`Error decoding npub: ${serverPubkey}`, error);
}
}
// Create the event with the proper structure
const event = {
// Ensure encryptedContent is a string
const finalContent = typeof encryptedContent === 'string' ?
encryptedContent : JSON.stringify(encryptedContent);
const event: NostrEvent = {
kind: 21120,
pubkey: pubkey,
content: encryptedContent, // Encrypted HTTP request using NIP-44
content: finalContent, // Encrypted HTTP request using NIP-44
created_at: Math.floor(Date.now() / 1000),
tags: [
// Required tags per README specification
["p", pTagValue], // P tag with hex pubkey (converted from npub if needed)
@ -180,15 +200,15 @@ export function convertToEvent(
};
console.log("Created event object:", JSON.stringify(event, null, 2));
// Add optional relay tag if provided
if (relay) {
event.tags.push(["r", relay]);
if (relayUrl) {
event.tags.push(["r", relayUrl]);
}
// Process tags to ensure proper format (convert any npub in tags to hex, etc.)
event.tags = processTags(event.tags);
return JSON.stringify(event, null, 2);
}
@ -251,11 +271,17 @@ export async function displayConvertedEvent(): Promise<void> {
}
}
// Generate a random key for encryption
const randomKey = Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15);
console.log("Generated random encryption key:", randomKey);
const convertedEvent = convertToEvent(
requestToUse,
pubkey,
serverPubkey,
"$decryptkey",
randomKey,
relay
);
@ -268,17 +294,17 @@ export async function displayConvertedEvent(): Promise<void> {
try {
// Parse the event to create a proper Nostr event object for signing
console.log("convertedEvent to parse:", convertedEvent);
const parsedEvent = JSON.parse(convertedEvent);
// Debug the content field
console.log("Event content from parsedEvent:", typeof parsedEvent.content, parsedEvent.content);
// IMPORTANT: Create the nostrEvent with the raw HTTP request as content
// This bypasses any issues with JSON parsing or encryption
// Create the nostrEvent, ensuring content is a string
nostrEvent = {
kind: 21120,
tags: parsedEvent.tags,
content: requestToUse, // Use the original HTTP request directly
content: typeof parsedEvent.content === 'string' ? parsedEvent.content : JSON.stringify(parsedEvent.content), // Ensure content is a string
created_at: Math.floor(Date.now() / 1000),
pubkey: parsedEvent.pubkey
};
@ -484,7 +510,7 @@ if (publishRelayInput) {
pauseBtn.textContent = 'Pause';
} else {
if (animationInterval !== null) {
clearInterval(animationInterval);
window.clearInterval(animationInterval);
animationInterval = null;
}
pauseBtn.textContent = 'Play';
@ -533,7 +559,7 @@ if (publishRelayInput) {
<p class="qr-info">This QR code contains a reference to the event: ${eventId.substring(0, 8)}...</p>
</div>
`;
} catch (fallbackError) {
} catch {
qrCodeContainer.innerHTML = `
<div class="qr-error-container">
<h3>QR Generation Failed</h3>

@ -1,5 +1,15 @@
// relay.ts - Functions for communicating with Nostr relays
import * as nostrTools from 'nostr-tools';
// Define Nostr event type to avoid using 'any'
export interface NostrEvent {
id?: string;
pubkey: string;
created_at: number;
kind: number;
tags: string[][];
content: string;
sig?: string;
}
/**
* Validate a hex string
@ -32,7 +42,7 @@ export function isValidHexString(str: string, expectedLength?: number): boolean
* @param relayUrl The URL of the relay to publish to
* @returns Promise that resolves to a success or error message
*/
export async function publishToRelay(event: any, relayUrl: string): Promise<string> {
export async function publishToRelay(event: NostrEvent, relayUrl: string): Promise<string> {
return new Promise<string>((resolve, reject) => {
try {
// Additional validation specifically for publishing
@ -67,14 +77,12 @@ export async function publishToRelay(event: any, relayUrl: string): Promise<stri
const relayPool = new nostrTools.SimplePool();
// Set a timeout for the publish operation
// eslint-disable-next-line no-undef
const timeout = setTimeout(() => {
relayPool.close([relayUrl]);
reject(new Error(`Timed out connecting to relay: ${relayUrl}`));
}, 10000); // 10 second timeout
// Publish the event to the relay
console.log(`Publishing event to relay: ${relayUrl}`);
// Create a standard formatted event to ensure it follows the relay protocol
const standardEvent = {
id: event.id,
@ -95,29 +103,22 @@ export async function publishToRelay(event: any, relayUrl: string): Promise<stri
standardEvent.kind = 21120;
}
console.log('Standard event format:', JSON.stringify(standardEvent, null, 2));
// Debug log the important hex values
console.log('ID:', standardEvent.id, 'length:', standardEvent.id.length, 'valid hex?', isValidHexString(standardEvent.id, 64));
console.log('Pubkey:', standardEvent.pubkey, 'length:', standardEvent.pubkey.length, 'valid hex?', isValidHexString(standardEvent.pubkey, 64));
console.log('Sig:', standardEvent.sig, 'length:', standardEvent.sig.length, 'valid hex?', isValidHexString(standardEvent.sig, 128));
// Use the standardized event for publishing
event = standardEvent;
const finalEvent = standardEvent;
// Ensure all hex strings are valid (this is a sanity check beyond our validation)
if (!isValidHexString(event.id, 64)) {
reject(new Error(`ID is not a valid hex string: ${event.id}`));
if (!isValidHexString(finalEvent.id, 64)) {
reject(new Error(`ID is not a valid hex string: ${finalEvent.id}`));
return;
}
if (!isValidHexString(event.pubkey, 64)) {
reject(new Error(`Pubkey is not a valid hex string: ${event.pubkey}`));
if (!isValidHexString(finalEvent.pubkey, 64)) {
reject(new Error(`Pubkey is not a valid hex string: ${finalEvent.pubkey}`));
return;
}
if (!isValidHexString(event.sig, 128)) {
reject(new Error(`Sig is not a valid hex string: ${event.sig}`));
if (!isValidHexString(finalEvent.sig, 128)) {
reject(new Error(`Sig is not a valid hex string: ${finalEvent.sig}`));
return;
}
@ -126,80 +127,84 @@ export async function publishToRelay(event: any, relayUrl: string): Promise<stri
// Use the WebSocket API directly
const ws = new WebSocket(relayUrl);
let wsTimeout = setTimeout(() => {
// eslint-disable-next-line no-undef
const wsTimeout = setTimeout(() => {
try {
ws.close();
} catch (e) {}
} catch {
// Ignore errors when closing WebSocket
}
reject(new Error("WebSocket connection timed out"));
}, 10000);
// Create a flag to track if we've handled response
let responseHandled = false;
ws.onopen = () => {
ws.onopen = (): void => {
// Send the event directly using the relay protocol format ["EVENT", event]
const messageToSend = JSON.stringify(["EVENT", event]);
console.log("Sending WebSocket message:", messageToSend);
const messageToSend = JSON.stringify(["EVENT", finalEvent]);
ws.send(messageToSend);
};
ws.onmessage = (msg) => {
console.log("WebSocket message received:", msg.data);
ws.onmessage = (msg): void => {
if (responseHandled) {
return; // Skip if we've already handled a response
}
if (typeof msg.data === 'string' && msg.data.startsWith('["OK"')) {
responseHandled = true;
// eslint-disable-next-line no-undef
clearTimeout(wsTimeout);
resolve(`Event published successfully via WebSocket`);
try {
ws.close();
} catch (e) {}
} catch {
// Ignore errors when closing WebSocket
}
} else if (typeof msg.data === 'string' && (msg.data.includes('invalid') || msg.data.includes('error'))) {
responseHandled = true;
// eslint-disable-next-line no-undef
clearTimeout(wsTimeout);
console.error("WebSocket error response:", msg.data);
reject(new Error(`Relay rejected event: ${msg.data}`));
try {
ws.close();
} catch (e) {}
} else {
console.log("Received other message, waiting for OK or error response");
} catch {
// Ignore errors when closing WebSocket
}
}
};
ws.onerror = (error) => {
ws.onerror = (error): void => {
if (responseHandled) {
return; // Skip if we've already handled a response
}
responseHandled = true;
// eslint-disable-next-line no-undef
clearTimeout(wsTimeout);
console.error("WebSocket error:", error);
reject(new Error(`WebSocket error: ${String(error)}`));
try {
ws.close();
} catch (e) {}
} catch {
// Ignore errors when closing WebSocket
}
};
ws.onclose = () => {
ws.onclose = (): void => {
// eslint-disable-next-line no-undef
clearTimeout(wsTimeout);
console.log("WebSocket closed");
};
} catch (wsError) {
// If WebSocket fails, try the regular method
console.error("WebSocket approach failed:", wsError);
} catch {
// If WebSocket fails, try the regular method
// Use the nostr-tools publish method as a fallback
console.log("Trying nostr-tools publish method...");
const publishPromises = relayPool.publish([relayUrl], event);
const publishPromises = relayPool.publish([relayUrl], finalEvent);
// Use Promise.all to wait for all promises to resolve
Promise.all(publishPromises)
.then((relayResults: string[]) => {
// eslint-disable-next-line no-undef
clearTimeout(timeout);
relayPool.close([relayUrl]);
if (relayResults && relayResults.length > 0) {
@ -209,9 +214,9 @@ export async function publishToRelay(event: any, relayUrl: string): Promise<stri
}
})
.catch((error: Error) => {
// eslint-disable-next-line no-undef
clearTimeout(timeout);
relayPool.close([relayUrl]);
console.error('Error details:', error);
reject(new Error(`Failed to publish event: ${error.message}`));
});
}
@ -236,8 +241,8 @@ export function convertNpubToHex(npub: string): string | null {
if (decoded.type === 'npub') {
return decoded.data as string;
}
} catch (error) {
console.error('Error decoding npub:', error);
} catch {
// Silent error handling to not disturb user experience
}
return null;
@ -248,11 +253,16 @@ export function convertNpubToHex(npub: string): string | null {
* @param event The event to verify
* @returns True if valid, false otherwise
*/
export function verifyEvent(event: any): boolean {
export function verifyEvent(event: NostrEvent): boolean {
try {
return nostrTools.verifyEvent(event);
} catch (error) {
console.error('Error verifying event:', error);
// For verification, the event must have id and sig
if (!event.id || !event.sig) {
return false;
}
// Type assertion to satisfy the nostrTools.verifyEvent requirements
return nostrTools.verifyEvent(event as nostrTools.Event);
} catch {
return false;
}
}

@ -26,12 +26,22 @@ export async function lookupNip05(nip05Address: string): Promise<{pubkey: string
}
return null;
} catch (error) {
console.error("Error looking up NIP-05 address:", error);
} catch {
return null;
}
}
// Define the NostrEvent interface to avoid using any type
interface NostrEvent {
id: string;
pubkey: string;
created_at: number;
kind: number;
tags: string[][];
content: string;
sig: string;
}
/**
* Search for users based on a search term across popular relays
* @param searchTerm The username or NIP-05 identifier to search for
@ -53,9 +63,8 @@ export async function searchUsers(searchTerm: string): Promise<Array<{name: stri
npub: searchTerm
}];
}
} catch (error) {
} catch {
// Not a valid npub, continue with search
console.log("Not a valid npub, continuing with search");
}
}
@ -73,8 +82,8 @@ export async function searchUsers(searchTerm: string): Promise<Array<{name: stri
});
processedPubkeys.add(nip05Result.pubkey);
}
} catch (error) {
console.error("Error looking up NIP-05:", error);
} catch {
// Error handling silent to not disturb the user experience
}
}
@ -90,11 +99,12 @@ export async function searchUsers(searchTerm: string): Promise<Array<{name: stri
// Set a timeout to ensure we don't wait forever
const timeoutPromise = new Promise<void>((resolve) => {
setTimeout(() => resolve(), 5000); // 5 second timeout
// Use window.setTimeout to avoid "not defined" error
window.setTimeout(() => resolve(), 5000); // 5 second timeout
});
// Create an event handler
const eventHandler = (event: any) => {
const eventHandler = (event: NostrEvent): void => {
try {
// Skip if we've already processed this pubkey
if (processedPubkeys.has(event.pubkey)) {
@ -127,8 +137,8 @@ export async function searchUsers(searchTerm: string): Promise<Array<{name: stri
// Mark as processed
processedPubkeys.add(event.pubkey);
}
} catch (error) {
console.error("Error processing event:", error);
} catch {
// Error handling silent to not break the user flow
}
};
@ -145,10 +155,10 @@ export async function searchUsers(searchTerm: string): Promise<Array<{name: stri
// Close the subscription
sub.close();
relayPool.close(POPULAR_RELAYS);
} catch {
// Error handling silent to provide a graceful fallback
}
} catch (error) {
console.error("Error searching relays:", error);
}
return results;
}

@ -1,5 +1,18 @@
// utils.ts - Utility functions for the HTTP to Nostr converter
import { convertNpubToHex } from './relay';
// Define NostrEvent interface
export interface NostrEvent {
id: string;
pubkey: string;
created_at: number;
kind: number;
tags: string[][];
content: string;
sig: string;
}
/**
* Populate the HTTP request textarea with a default example
*/
@ -51,15 +64,12 @@ export function processTags(tags: string[][]): string[][] {
// Convert npub in "p" tags to hex pubkeys
if (tag[0] === 'p' && typeof tag[1] === 'string' && tag[1].startsWith('npub')) {
try {
console.log(`Processing p tag: ${tag[1]}`);
const { convertNpubToHex } = require('./relay');
const hexPubkey = convertNpubToHex(tag[1]);
if (hexPubkey) {
processedTags[i][1] = hexPubkey;
console.log(`Converted npub to hex: ${hexPubkey}`);
}
} catch (error) {
console.error(`Error processing p tag: ${tag[1]}`, error);
} catch {
// Silent error handling
}
}
@ -77,19 +87,20 @@ export function processTags(tags: string[][]): string[][] {
* @param event The event to standardize
* @returns Standardized event
*/
export function standardizeEvent(event: any): any {
export function standardizeEvent(event: Record<string, unknown>): NostrEvent {
if (!event || typeof event !== 'object') {
return event;
throw new Error('Invalid event: not an object');
}
// Type assertions needed for raw object properties
return {
id: event.id,
pubkey: event.pubkey,
created_at: Number(event.created_at),
kind: Number(event.kind),
tags: processTags(event.tags),
content: event.content,
sig: event.sig
id: String(event.id || ''),
pubkey: String(event.pubkey || ''),
created_at: Number(event.created_at || Math.floor(Date.now() / 1000)),
kind: Number(event.kind || 21120),
tags: processTags(Array.isArray(event.tags) ? event.tags as string[][] : []),
content: String(event.content || ''),
sig: String(event.sig || '')
};
}

385
client/styles.css Normal file

@ -0,0 +1,385 @@
/* Styles for the HTTP Messages Project Homepage */
/* CSS Variables for themes */
:root {
/* Light theme (default) */
--bg-primary: #f8f9fa;
--bg-secondary: #ffffff;
--bg-tertiary: #f1f8ff;
--bg-info: #e2f0fd;
--text-primary: #212529;
--text-secondary: #495057;
--text-tertiary: #6c757d;
--border-color: #dee2e6;
--accent-color: #0d6efd;
--button-primary: #0d6efd;
--button-hover: #0b5ed7;
--button-success: #28a745;
--button-success-hover: #218838;
--info-border: #0d6efd;
--code-bg: #f8f9fa;
}
/* Dark theme */
[data-theme="dark"] {
--bg-primary: #121212;
--bg-secondary: #1e1e1e;
--bg-tertiary: #252836;
--bg-info: #1a2634;
--text-primary: #e0e0e0;
--text-secondary: #b0b0b0;
--text-tertiary: #909090;
--border-color: #333333;
--accent-color: #3f87ff;
--button-primary: #3f87ff;
--button-hover: #2970e3;
--button-success: #2a9745;
--button-success-hover: #218838;
--info-border: #3f87ff;
--code-bg: #252525;
}
/* General layout */
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
max-width: 900px;
margin: 0 auto;
padding: 20px;
background-color: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
transition: background-color 0.3s ease, color 0.3s ease;
}
/* Headings */
h1 {
color: var(--text-primary);
margin-bottom: 20px;
border-bottom: 2px solid var(--border-color);
padding-bottom: 10px;
}
h2 {
color: var(--text-secondary);
margin-top: 25px;
margin-bottom: 15px;
}
h3 {
color: var(--text-secondary);
margin-top: 20px;
margin-bottom: 10px;
}
/* Theme toggle */
.theme-toggle-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 100;
}
.theme-toggle-btn {
display: flex;
align-items: center;
background-color: var(--bg-secondary);
color: var(--text-primary);
border: 1px solid var(--border-color);
border-radius: 30px;
padding: 8px 12px;
cursor: pointer;
font-size: 14px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
transition: all 0.3s ease;
}
.theme-toggle-btn:hover {
background-color: var(--accent-color);
color: white;
}
/* Info box */
.info-box {
background-color: var(--bg-info);
border-left: 4px solid var(--info-border);
padding: 15px 20px;
margin-bottom: 25px;
border-radius: 0 4px 4px 0;
}
/* Sections */
.section {
margin-bottom: 40px;
padding: 20px;
background-color: var(--bg-secondary);
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
border: 1px solid var(--border-color);
}
/* Architecture diagram */
.diagram-container {
margin: 30px 0;
text-align: center;
}
.diagram-container img {
max-width: 100%;
height: auto;
border-radius: 8px;
border: 1px solid var(--border-color);
}
.diagram-caption {
margin-top: 10px;
font-style: italic;
color: var(--text-tertiary);
}
/* Code blocks */
pre {
background-color: var(--code-bg);
padding: 15px;
border-radius: 4px;
overflow-x: auto;
font-family: 'Courier New', Courier, monospace;
font-size: 14px;
line-height: 1.4;
border: 1px solid var(--border-color);
}
code {
font-family: 'Courier New', Courier, monospace;
background-color: var(--code-bg);
padding: 2px 5px;
border-radius: 3px;
font-size: 0.9em;
border: 1px solid var(--border-color);
}
/* Buttons */
.button {
display: inline-block;
background-color: var(--button-primary);
color: white;
text-decoration: none;
padding: 10px 20px;
border-radius: 4px;
font-weight: bold;
transition: background-color 0.2s;
text-align: center;
margin: 10px 5px;
}
.button:hover {
background-color: var(--button-hover);
}
.button-success {
background-color: var(--button-success);
}
.button-success:hover {
background-color: var(--button-success-hover);
}
/* Features grid */
.features-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
margin: 30px 0;
}
.feature-card {
padding: 20px;
background-color: var(--bg-tertiary);
border-radius: 8px;
border: 1px solid var(--border-color);
}
.feature-card h3 {
color: var(--accent-color);
margin-top: 0;
}
/* Content */
.content {
margin-bottom: 40px;
}
/* Footer */
footer {
margin-top: 50px;
padding-top: 20px;
border-top: 1px solid var(--border-color);
text-align: center;
color: var(--text-tertiary);
}
/* Responsive adjustments */
@media (max-width: 768px) {
body {
padding: 15px;
}
.features-grid {
grid-template-columns: 1fr;
}
.navigation {
flex-direction: column;
}
.theme-toggle-container {
position: static;
display: flex;
justify-content: flex-end;
margin-bottom: 20px;
}
}
/* Navigation */
.navigation {
display: flex;
margin-bottom: 25px;
border-bottom: 2px solid var(--border-color);
padding-bottom: 10px;
}
.nav-link {
padding: 10px 20px;
margin-right: 10px;
text-decoration: none;
color: var(--text-secondary);
font-weight: 500;
border-bottom: 2px solid transparent;
transition: all 0.3s ease;
}
.nav-link:hover {
color: var(--accent-color);
}
.nav-link.active {
color: var(--accent-color);
border-bottom: 2px solid var(--accent-color);
}
/* Tab Navigation (legacy) */
.tab-navigation {
margin-bottom: 25px;
border-bottom: 2px solid var(--border-color);
}
.tab-buttons {
display: flex;
flex-wrap: wrap;
margin-bottom: -2px;
}
.tab-btn {
padding: 10px 20px;
margin-right: 10px;
border: none;
background: none;
border-bottom: 2px solid transparent;
cursor: pointer;
font-weight: 500;
color: var(--text-secondary);
transition: all 0.3s ease;
}
/* Server input */
.server-input-container {
display: flex;
margin-top: 5px;
margin-bottom: 10px;
}
.server-input {
flex: 1;
padding: 8px;
border: 1px solid var(--border-color);
border-radius: 4px 0 0 4px;
font-size: 14px;
}
.server-search-button {
background-color: var(--button-primary);
color: white;
border: none;
border-radius: 0 4px 4px 0;
padding: 8px 15px;
cursor: pointer;
font-weight: 500;
transition: background-color 0.2s;
}
.server-search-button:hover {
background-color: var(--button-hover);
}
.server-search-result {
margin-top: 10px;
padding: 10px;
background-color: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 4px;
}
/* HTTP Request textarea */
#httpRequest {
width: 100%;
min-height: 150px;
padding: 10px;
font-family: 'Courier New', Courier, monospace;
font-size: 14px;
border: 1px solid var(--border-color);
border-radius: 4px;
background-color: var(--bg-secondary);
color: var(--text-primary);
resize: vertical;
margin-bottom: 15px;
}
/* Convert button */
#convertButton {
display: inline-block;
background-color: var(--button-primary);
color: white;
border: none;
border-radius: 4px;
padding: 10px 20px;
font-weight: bold;
cursor: pointer;
transition: background-color 0.2s;
}
#convertButton:hover {
background-color: var(--button-hover);
}
.tab-btn:hover {
color: var(--accent-color);
}
.tab-btn.active {
color: var(--accent-color);
border-bottom: 2px solid var(--accent-color);
}
/* Login container */
.login-container {
margin-bottom: 20px;
}
.login-status {
margin-top: 5px;
font-size: 14px;
}
/* Hidden elements */
.hidden {
display: none !important;
}

@ -29,6 +29,7 @@ module.exports = {
{ from: 'src/styles.css', to: 'styles.css' },
{ from: 'http.png', to: 'http.png' },
{ from: 'index.html', to: 'index.html' },
{ from: 'help.html', to: 'help.html' },
],
}),
],

@ -1,102 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HTTP to Kind 21120 Converter</title>
<script src='https://www.unpkg.com/nostr-login@latest/dist/unpkg.js'></script>
<script src="https://cdn.jsdelivr.net/npm/nostr-tools@latest"></script>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
line-height: 1.6;
}
textarea {
width: 100%;
height: 200px;
font-family: monospace;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
margin-bottom: 15px;
}
button {
background-color: #4CAF50;
color: white;
padding: 10px 15px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
button:hover {
background-color: #45a049;
}
#output {
margin-top: 20px;
padding: 15px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #f9f9f9;
}
pre {
background-color: #f5f5f5;
padding: 10px;
border-radius: 4px;
overflow-x: auto;
white-space: pre-wrap;
word-wrap: break-word;
}
.info-box {
background-color: #e7f3fe;
border-left: 6px solid #2196F3;
padding: 10px;
margin-bottom: 20px;
}
h1 {
color: #333;
}
h2 {
color: #555;
}
</style>
</head>
<body>
<h1>HTTP Request to Kind 21120 Converter</h1>
<div class="info-box">
<p>This tool converts HTTP requests into a single Nostr event of kind 21120. The HTTP request is placed directly in the content field with appropriate tags following the specification.</p>
</div>
<h2>Server Information:</h2>
<div style="margin-bottom: 15px;">
<div style="margin-bottom: 10px;">
<label for="serverPubkey">Server Pubkey:</label><br>
<input type="text" id="serverPubkey" placeholder="Enter server pubkey" style="width: 100%; padding: 8px;">
</div>
<div>
<label for="relay">Response Relay (optional):</label><br>
<input type="text" id="relay" placeholder="wss://relay.example.com" style="width: 100%; padding: 8px;">
</div>
</div>
<h2>Enter HTTP Request:</h2>
<textarea id="httpRequest" placeholder="GET /index.html HTTP/1.1
Host: example.com
User-Agent: Browser/1.0
"></textarea>
<button id="convertButton">Convert to Event</button>
<div id="output" hidden>
<h2>Converted Event:</h2>
<pre id="eventOutput"></pre>
</div>
<script src="./src/converter.js" type="module"></script>
</body>
</html>