feat: end to end request to request

This commit is contained in:
n 2025-04-07 18:41:26 +01:00
parent bf471337b0
commit 173f9d19fe
8 changed files with 689 additions and 227 deletions

@ -3,4 +3,5 @@ don't try to modify files in the build folder (eg /dist)
don't put any css or javascript blocks in the index.html file
npm run lint after every code update
don't ever use the alert function
assume you are in the client folder
do not ask to "cd client"
all signing to be done using nostr-login

@ -40,7 +40,7 @@
<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">
<input type="text" id="serverPubkey" placeholder="npub, username, or NIP-05 identifier" value="npub1thq3fzcw393c0tpy60sz0dvvjz4tjrgtrudxsa68sldkf78fznksgp34w8" class="server-input">
<button id="searchServerBtn" class="server-search-button">Search</button>
</div>
<div id="serverSearchResult" class="server-search-result" style="display: none;">
@ -50,7 +50,7 @@
<div>
<label for="relay">Response Relay (optional):</label><br>
<input type="text" id="relay" value="wss://relay.damus.io" style="width: 100%; padding: 8px;">
<input type="text" id="relay" value="wss://relay.degmods.com" style="width: 100%; padding: 8px;">
</div>
</div>
@ -75,7 +75,7 @@ User-Agent: Browser/1.0
<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">
<input type="text" id="publishRelay" value="wss://relay.degmods.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;">

@ -36,41 +36,38 @@
<div class="relay-connection">
<div class="relay-input-container">
<label for="relayUrl">Relay URL:</label>
<input type="text" id="relayUrl" value="wss://relay.damus.io" placeholder="wss://relay.example.com">
<input type="text" id="relayUrl" value="wss://relay.degmods.com" placeholder="wss://relay.example.com">
<button id="connectRelayBtn" class="relay-connect-button">Connect</button>
</div>
<div id="relayStatus" class="relay-status">Not connected</div>
</div>
<h2>Subscription Settings</h2>
<div class="subscription-settings">
<p class="info-text">Automatically shows all kind 21120 events that are p-tagged to the server npub.</p>
<button id="startSubscriptionBtn" class="start-subscription-button">Start Subscription</button>
<button id="stopSubscriptionBtn" class="stop-subscription-button" disabled>Stop Subscription</button>
</div>
<h2>Received Events</h2>
<div class="received-events">
<div class="event-controls">
<button id="clearEventsBtn" class="clear-events-button">Clear Events</button>
</div>
<div id="eventsList" class="events-list">
<div class="empty-state">
No events received yet. Connect to a relay and start a subscription.
<div class="events-container">
<div class="events-sidebar">
<div id="eventsList" class="events-list">
<div class="empty-state">
No events received yet. Connect to a relay to start receiving events.
</div>
<!-- Events will be displayed here -->
</div>
</div>
<div class="events-content">
<div id="eventDetails" class="event-details">
<div class="empty-state">
Select an event to view details
</div>
<!-- Selected event details will be shown here -->
</div>
</div>
<!-- Events will be displayed here -->
</div>
</div>
<h2>Event Details</h2>
<div id="eventDetails" class="event-details">
<div class="empty-state">
Select an event to view details
</div>
<!-- Selected event details will be shown here -->
</div>
</div>
<!-- Script will be provided by bundle.js -->

@ -2,7 +2,17 @@
// This follows strict CSP policies by avoiding inline scripts
// Import from Node.js built-ins & external modules
import * as nostrTools from 'nostr-tools';
// No longer need direct nostr-tools imports for this file
// On page load, always fetch the latest pubkey from window.nostr
if (window.nostr && typeof window.nostr.getPublicKey === 'function') {
window.nostr.getPublicKey().then(pubkey => {
console.log(`Page load: Retrieved pubkey from window.nostr: ${pubkey.slice(0, 8)}...`);
localStorage.setItem('userPublicKey', pubkey);
}).catch(err => {
console.warn("Page load: Failed to get pubkey from window.nostr:", err);
});
}
// Import type definitions
import type { NostrEvent } from './converter';
@ -38,37 +48,44 @@ declare global {
* Initialize nostr-login
*/
function initNostrLogin(): void {
const loginContainer = document.querySelector('.login-container');
const loginStatusDiv = document.getElementById('loginStatus');
if (!loginContainer || !loginStatusDiv) {
return;
}
// Create a container for the NostrLogin button
const nostrLoginContainer = document.createElement('div');
nostrLoginContainer.id = 'nostr-login-container';
loginContainer.appendChild(nostrLoginContainer);
try {
// Initialize NostrLogin with the container
// Initialize NostrLogin without requiring UI elements
if (NostrLogin && NostrLogin.init) {
// Create a temporary container if needed
const tempContainer = document.createElement('div');
tempContainer.style.display = 'none';
document.body.appendChild(tempContainer);
console.log("Initializing NostrLogin...");
NostrLogin.init({
element: nostrLoginContainer,
element: tempContainer,
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>`;
console.log(`Connected to Nostr with pubkey: ${pubkey.slice(0, 8)}...`);
// Store pubkey in localStorage for other parts of the app
localStorage.setItem('userPublicKey', pubkey);
},
onDisconnect: (): void => {
loginStatusDiv.innerHTML = '<span>Disconnected</span>';
console.log('Disconnected from Nostr');
localStorage.removeItem('userPublicKey');
}
});
// Check if we can get an existing pubkey (already connected)
if (window.nostr) {
window.nostr.getPublicKey().then(pubkey => {
console.log(`Already connected with pubkey: ${pubkey.slice(0, 8)}...`);
localStorage.setItem('userPublicKey', pubkey);
}).catch(err => {
console.warn("Not connected to Nostr extension:", err);
});
}
} else {
loginStatusDiv.innerHTML = '<span style="color: #cc0000;">NostrLogin initialization unavailable</span>';
console.warn("NostrLogin initialization unavailable");
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
loginStatusDiv.innerHTML = `<span style="color: #cc0000;">Error initializing Nostr login: ${errorMessage}</span>`;
console.error(`Error initializing Nostr login: ${errorMessage}`);
}
}
@ -401,6 +418,9 @@ function handleCopyEvent(): void {
}
// Initialize the event handlers when the DOM is loaded
// Initialize Nostr login as early as possible, before DOM is ready
initNostrLogin();
document.addEventListener('DOMContentLoaded', function(): void {
// Set up the convert button click handler
const convertButton = document.getElementById('convertButton');
@ -419,8 +439,15 @@ document.addEventListener('DOMContentLoaded', function(): void {
publishButton.addEventListener('click', handlePublishEvent);
}
// Initialize Nostr login
initNostrLogin();
// Try to get pubkey again after DOM is ready
if (window.nostr) {
window.nostr.getPublicKey().then(pubkey => {
console.log(`DOM ready: Retrieved pubkey: ${pubkey.slice(0, 8)}...`);
localStorage.setItem('userPublicKey', pubkey);
}).catch(err => {
console.warn("DOM ready: Failed to get pubkey:", err);
});
}
// Set default HTTP request
setDefaultHttpRequest();

@ -7,7 +7,7 @@ export const defaultServerConfig = {
// Server's npub (hard-coded to match the server's config)
serverNpub: "npub1r6knexka25dn9w9jnf5kf8xh6gfq7n3p38zfl7nn7cjjjsp4umcqnk0aun",
// Default relay for responses
defaultRelay: "wss://relay.damus.io"
defaultRelay: "wss://relay.degmods.com"
};
// Application settings

@ -25,7 +25,8 @@ interface NostrWindowExtension {
getPublicKey: () => Promise<string>;
signEvent: (event: NostrEvent) => Promise<NostrEvent>;
nip44?: {
encrypt: (content: string, pubkey: string) => string;
encrypt(pubkey: string, plaintext: string): Promise<string>;
decrypt(pubkey: string, ciphertext: string): Promise<string>;
};
}
@ -148,20 +149,11 @@ export async function convertToEvent(
if (!/^[0-9a-f]{64}$/i.test(serverPubkeyHex)) {
throw new Error("Invalid server pubkey format. Must be a 64-character hex string.");
}
// Create a payload object with the HTTP request
const payload = {
httpRequest: httpRequest,
timestamp: Date.now(),
serverPubkey: serverPubkeyHex
};
// Stringify the payload to encrypt
const payloadString = JSON.stringify(payload);
// Use Web Crypto API to encrypt the payload with decryptkey
console.log("Encrypting with Web Crypto API");
encryptedContent = await encryptWithWebCrypto(payloadString, decryptkey);
// Encrypt the HTTP request directly without wrapping it in JSON
// This ensures the decrypted content is the raw HTTP request
console.log("Encrypting raw HTTP request with Web Crypto API");
encryptedContent = await encryptWithWebCrypto(httpRequest, decryptkey);
console.log("Successfully encrypted HTTP content with Web Crypto API");
console.log("Successfully encrypted content with Web Crypto API");
} catch (error) {
console.error("Error in encryption:", error);
@ -207,6 +199,43 @@ export async function convertToEvent(
const finalContent = typeof encryptedContent === 'string' ?
encryptedContent : JSON.stringify(encryptedContent);
// Encrypt the decryption key using NIP-44 through nostr-signers
let encryptedKey = decryptkey;
// First ensure we're properly connected to nostr-signers
if (!window.nostr) {
console.warn("window.nostr not available - ensure a NIP-07 extension is installed and connected");
} else if (typeof window.nostr.nip44 !== 'object' || typeof window.nostr.nip44.encrypt !== 'function') {
console.warn("NIP-44 encryption not available - connect to a compatible NIP-07 extension");
// Log additional diagnostic information
console.log("Available nostr methods:", Object.keys(window.nostr).join(", "));
if (NostrLogin && typeof NostrLogin.getPublicKey === 'function') {
try {
// Try to explicitly connect using NostrLogin
const pubkey = await NostrLogin.getPublicKey();
console.log("Retrieved public key via NostrLogin:", pubkey);
// Sometimes this explicit call triggers the extension to connect properly
} catch (e) {
console.error("Failed to connect via NostrLogin:", e);
}
}
} else {
try {
console.log("Encrypting decryption key using window.nostr.nip44.encrypt");
// According to the NIP-07 spec, the first parameter is the pubkey (recipient)
// and the second parameter is the plaintext to encrypt
const encryptionPromise = window.nostr.nip44.encrypt(pTagValue, decryptkey);
// Since this is a Promise, we need to await it
encryptedKey = await encryptionPromise;
console.log("Successfully encrypted the decryption key with NIP-44");
} catch (encryptError) {
console.error("Failed to encrypt key with NIP-44:", encryptError instanceof Error ? encryptError.message : String(encryptError));
console.log("Using unencrypted key as fallback");
}
}
const event: NostrEvent = {
kind: 21120,
pubkey: pubkey,
@ -215,7 +244,7 @@ export async function convertToEvent(
tags: [
// Required tags per README specification
["p", pTagValue], // P tag with hex pubkey (converted from npub if needed)
["key", decryptkey], // Key for decryption
["key", encryptedKey], // Key for decryption, encrypted with NIP-44
["expiration", String(Math.floor(Date.now() / 1000) + appSettings.expirationTime)]
]
};

@ -5,6 +5,55 @@ import * as nostrTools from 'nostr-tools';
import { convertNpubToHex } from './relay';
import type { NostrEvent } from './relay';
// Import the NostrWindowExtension interface from converter
// Helper function for Web Crypto API decryption (mirror of encryptWithWebCrypto in converter.ts)
async function decryptWithWebCrypto(encryptedBase64: string, key: string): Promise<string> {
try {
// Convert base64 to byte array
const encryptedBytes = new Uint8Array(
atob(encryptedBase64)
.split('')
.map(char => char.charCodeAt(0))
);
// 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 decryption key
const keyMaterial = await crypto.subtle.digest(
'SHA-256',
new TextEncoder().encode(key)
);
// 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) {
throw new Error(`WebCrypto decryption failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
// Define interfaces for our application
interface NostrSubscription {
unsub: () => void;
@ -22,6 +71,34 @@ interface ReceivedEvent {
let relayPool: any = null;
let activeSubscription: NostrSubscription | null = null;
let activeRelayUrl: string | null = null;
/**
* Get the logged-in user's public key from localStorage
* This is populated by the nostr-login plugin
*/
function getLoggedInPubkey(): string | null {
const pubkey = localStorage.getItem('userPublicKey');
console.log("Retrieved pubkey from localStorage:", pubkey);
// If no pubkey in localStorage, try to get it from window.nostr directly
if (!pubkey && window.nostr && typeof window.nostr.getPublicKey === 'function') {
console.log("No pubkey in localStorage, trying to get from window.nostr");
// Note: This returns a promise, so we can't use it synchronously
// But we'll trigger the fetch so it might be available next time
window.nostr.getPublicKey()
.then(directPubkey => {
console.log("Retrieved pubkey directly from window.nostr:", directPubkey);
localStorage.setItem('userPublicKey', directPubkey);
return directPubkey;
})
.catch(err => {
console.error("Failed to get pubkey from window.nostr:", err);
return null;
});
}
return pubkey;
}
const receivedEvents = new Map<string, ReceivedEvent>();
// DOM Elements (populated on DOMContentLoaded)
@ -130,25 +207,51 @@ async function subscribeToEvents(options: {
// For now, we're going to fetch ALL kind 21120 events from the relay
// to ensure we're getting data
console.log('Creating subscription for ALL kind 21120 events');
// Get the logged-in user's pubkey if available
const loggedInPubkey = getLoggedInPubkey();
console.log('Creating subscription for kind 21120 events addressed to the user');
// Define the filter type properly
interface NostrFilter {
kinds: number[];
'#p'?: string[];
authors?: string[];
}
// Create filter for kind 21120 events
const filter: any = {
const filter: NostrFilter = {
kinds: [21120], // HTTP Messages event kind
};
// If the user is logged in, filter for events addressed to them
if (loggedInPubkey) {
let pubkeyHex = loggedInPubkey;
// Convert npub to hex if needed
if (loggedInPubkey.startsWith('npub')) {
try {
const hexPubkey = convertNpubToHex(loggedInPubkey);
if (hexPubkey) {
pubkeyHex = hexPubkey;
}
} catch (error) {
console.error("Failed to convert npub to hex:", error);
}
}
// Add p-tag filter for events addressed to the logged-in user
filter['#p'] = [pubkeyHex];
console.log(`Filtering for events addressed to user: ${pubkeyHex}`);
} else {
console.log('No user logged in, showing all kind 21120 events');
}
// Log the filter we're using
console.log('Using filter:', JSON.stringify(filter));
// Define the author filter type properly
interface AuthorFilter {
kinds: number[];
'#p': string[];
authors?: string[];
}
// We can also filter by author if specified
// If a specific pubkey filter is provided in options, use it for p-tag filtering
// This replaces the logged-in user's pubkey filter with the specified one
if (options.pubkeyFilter) {
let pubkey = options.pubkeyFilter;
@ -164,8 +267,10 @@ async function subscribeToEvents(options: {
}
}
// Add optional author filter
(filter as AuthorFilter).authors = [pubkey];
// Set the p-tag filter to the specified pubkey
// This replaces any existing p-tag filter set by the logged-in user
filter['#p'] = [pubkey];
console.log(`Overriding p-tag filter to: ${pubkey}`);
}
// Skip using nostr-tools SimplePool for subscription to avoid issues
@ -302,6 +407,8 @@ function processEvent(event: NostrEvent): void {
return;
}
// Event filtering is now handled at the subscription level
// Store the event
const receivedEvent: ReceivedEvent = {
id: event.id,
@ -330,7 +437,7 @@ function addEventToUI(receivedEvent: ReceivedEvent): void {
return;
}
// Create event item
// Create event item with improved styling
const eventItem = document.createElement('div');
eventItem.className = 'event-item';
eventItem.dataset.id = event.id;
@ -341,15 +448,24 @@ function addEventToUI(receivedEvent: ReceivedEvent): void {
// Determine if it's a request or response
const hasP = event.tags.some(tag => tag[0] === 'p');
const hasE = event.tags.some(tag => tag[0] === 'e');
const eventType = hasP ? 'Request' : (hasE ? 'Response' : 'Unknown');
const eventType = hasP ? 'HTTP Request' : (hasE ? 'HTTP Response' : 'Unknown');
// Find recipient if available
let recipient = '';
const pTag = event.tags.find(tag => tag[0] === 'p');
if (pTag && pTag.length > 1) {
recipient = `| To: ${pTag[1].substring(0, 8)}...`;
}
// Format the event item
eventItem.innerHTML = `
<div class="event-header">
<span class="event-id">ID: ${eventIdForDisplay}...</span>
<span class="event-id">${eventIdForDisplay}...</span>
<span class="event-time">${new Date(event.created_at * 1000).toLocaleTimeString()}</span>
</div>
<div class="event-summary">Type: ${eventType} | From: ${event.pubkey.substring(0, 8)}...</div>
<div class="event-summary">
<span class="event-type">${eventType}</span> | From: ${event.pubkey.substring(0, 8)}... ${recipient}
</div>
<div class="event-actions">
<button class="view-details-btn">View Details</button>
</div>
@ -421,40 +537,57 @@ function showEventDetails(eventId: string): void {
const isResponse = event.tags.some(tag => tag[0] === 'e');
const eventTypeLabel = isRequest ? 'HTTP Request' : (isResponse ? 'HTTP Response' : 'Unknown Type');
// Format content for display (if decrypted, use the decrypted content)
const displayContent = receivedEvent.decrypted ?
// Get the decrypted or original content
// Since we now encrypt just the HTTP request directly, this is just the HTTP content
const httpContent = receivedEvent.decrypted ?
(receivedEvent.decryptedContent || event.content) :
event.content;
// Try to pretty-print JSON if the content appears to be JSON
let formattedContent = displayContent || '';
try {
if (formattedContent.trim().startsWith('{')) {
const jsonObj = JSON.parse(formattedContent);
formattedContent = JSON.stringify(jsonObj, null, 2);
}
} catch {
// Not valid JSON, use as-is
}
// For raw display of encrypted content if needed
const rawContent = event.content;
eventDetails.innerHTML = `
<h3>${eventTypeLabel} (ID: ${eventIdForDisplay}...)</h3>
<div class="event-detail-item">
<strong>ID:</strong> ${fullEventId}
<div class="event-detail-header">
<h3>${eventTypeLabel} (ID: ${eventIdForDisplay}...)</h3>
<div class="event-timestamp">${new Date(event.created_at * 1000).toLocaleString()}</div>
</div>
<div class="event-detail-item">
<strong>From:</strong> ${event.pubkey}
<div class="event-detail-metadata">
<div class="event-detail-item">
<strong>ID:</strong> <span class="metadata-value">${fullEventId}</span>
</div>
<div class="event-detail-item">
<strong>From:</strong> <span class="metadata-value">${event.pubkey}</span>
</div>
<div class="event-detail-item">
<strong>Created:</strong> <span class="metadata-value">${new Date(event.created_at * 1000).toLocaleString()}</span>
</div>
<div class="event-detail-item">
<strong>Tags:</strong>
<ul class="event-tags">${tagsHtml}</ul>
</div>
</div>
<div class="event-detail-item">
<strong>Created:</strong> ${new Date(event.created_at * 1000).toLocaleString()}
</div>
<div class="event-detail-item">
<strong>Tags:</strong>
<ul>${tagsHtml}</ul>
</div>
<div class="event-detail-item">
<strong>Content${receivedEvent.decrypted ? ' (Decrypted)' : ''}:</strong>
<pre class="event-content">${formattedContent}</pre>
<div class="event-detail-content">
<div class="content-row">
<div class="event-metadata-column">
<div class="content-header">
<strong>Event Metadata</strong>
</div>
<div class="metadata-content">
<div>ID: ${fullEventId}</div>
<div>Pubkey: ${event.pubkey}</div>
<div>Created: ${new Date(event.created_at * 1000).toLocaleString()}</div>
${receivedEvent.decrypted ? '<div class="decrypted-badge">Decrypted</div>' : ''}
</div>
</div>
<div class="http-content-column">
<div class="content-header">
<strong>HTTP ${eventTypeLabel}</strong>
</div>
<pre class="http-content">${httpContent}</pre>
</div>
</div>
</div>
`;
}
@ -467,32 +600,95 @@ async function decryptEvent(eventId: string): Promise<void> {
}
try {
// First try to parse as base64-encoded JSON (for unencrypted content)
const event = receivedEvent.event;
let decryptedContent: string;
try {
// Try to parse as base64-encoded JSON
const decodedContent = atob(receivedEvent.event.content);
const parsedContent = JSON.parse(decodedContent);
decryptedContent = JSON.stringify(parsedContent, null, 2);
} catch {
// If that fails, try NIP-44 decryption with user's private key
// Look for a "key" tag in the event
const keyTag = event.tags.find(tag => tag[0] === 'key');
if (!keyTag || keyTag.length < 2) {
decryptedContent = `[No key tag found for decryption]\n${event.content}`;
} else {
try {
// Get user's private key from localStorage (would be set during login)
const userPrivateKey = localStorage.getItem('userPrivateKey');
// Extract the encrypted key from the tag
const encryptedKey = keyTag[1];
if (userPrivateKey) {
// In a real implementation, we would use the nostr-tools or similar library
// to decrypt the content using NIP-44 and the user's private key
// For now, we'll just use the original content with a note
decryptedContent = `[Auto-decryption would happen here with NIP-44]\n${receivedEvent.event.content}`;
} else {
// No private key available
decryptedContent = `[No private key available for decryption]\n${receivedEvent.event.content}`;
// Check if window.nostr and nip44.decrypt are available
if (!window.nostr) {
throw new Error("window.nostr is not available - ensure a NIP-07 extension is installed");
}
if (!window.nostr.nip44 || !window.nostr.nip44.decrypt) {
console.warn("NIP-44 decryption not available - trying to connect to a compatible extension");
// Log diagnostic information
console.log("Available nostr methods:", Object.keys(window.nostr).join(", "));
// Check if we can trigger a connection via NostrLogin
if (typeof window.nostr.getPublicKey === 'function') {
try {
await window.nostr.getPublicKey();
// Check again after getting public key
if (typeof window.nostr.nip44 === 'object' &&
typeof window.nostr.nip44.decrypt === 'function') {
console.log("NIP-44 is now available after connecting");
} else {
throw new Error("NIP-44 decryption is still not available after connecting");
}
} catch (e) {
throw new Error(`Failed to connect to extension: ${e}`);
}
} else {
throw new Error("window.nostr.nip44.decrypt is not available");
}
}
// Use window.nostr to decrypt the key tag with NIP-44
console.log("Using window.nostr.nip44.decrypt for key decryption");
// Note that according to the NIP-07 spec, the first parameter is the pubkey (sender)
// and the second parameter is the ciphertext
// Declare decryptedKey outside the try block so it's available in the outer scope
let decryptedKey: string;
try {
// Log the actual values being passed to help debug
console.log(`Attempting to decrypt with pubkey: ${event.pubkey.substring(0, 8)}... and encrypted key: ${encryptedKey.substring(0, 10)}...`);
decryptedKey = await window.nostr.nip44.decrypt(
event.pubkey, // The pubkey that encrypted the key
encryptedKey // The encrypted key from the "key" tag
);
console.log("Decryption successful, decrypted key:", decryptedKey.substring(0, 5) + "...");
} catch (decryptKeyError) {
console.error("Error in direct decryption call:", decryptKeyError);
throw decryptKeyError; // Re-throw to be caught by the outer catch block
}
console.log("Key decryption successful");
// Now use the decrypted key to decrypt the content using Web Crypto API
try {
// Decrypt the content using Web Crypto API and the decrypted key
const decryptedEventContent = await decryptWithWebCrypto(
event.content,
decryptedKey
);
// The decrypted content is the direct HTTP request/response
// No need to try parsing as JSON anymore
console.log("Successfully decrypted HTTP content");
decryptedContent = decryptedEventContent;
} catch (contentDecryptError) {
console.error("Content decryption failed:", contentDecryptError instanceof Error ? contentDecryptError.message : String(contentDecryptError));
decryptedContent = `Key decryption successful, but content decryption failed: ${contentDecryptError instanceof Error ? contentDecryptError.message : String(contentDecryptError)}
Encrypted content: ${event.content.substring(0, 50)}...
Decrypted key: ${decryptedKey}`;
}
} catch (decryptError) {
// If decryption fails, use the original content
console.error("Could not decrypt content:", decryptError);
decryptedContent = `[Decryption failed]\n${receivedEvent.event.content}`;
console.error("Failed to decrypt with window.nostr.nip44:", decryptError instanceof Error ? decryptError.message : String(decryptError));
decryptedContent = `[NIP-44 decryption failed: ${decryptError instanceof Error ? decryptError.message : String(decryptError)}]\n${event.content}`;
}
}
@ -502,7 +698,7 @@ async function decryptEvent(eventId: string): Promise<void> {
receivedEvents.set(eventId, receivedEvent);
// Update UI if this event is currently being viewed
const selectedEventId = eventDetails?.querySelector('h3')?.textContent?.match(/Event (.+)\.\.\./)?.[1];
const selectedEventId = eventDetails?.querySelector('h3')?.textContent?.match(/(.+)\.\.\./)?.[1];
if (selectedEventId && `${selectedEventId}...` === `${eventId.substring(0, 8)}...`) {
showEventDetails(eventId);
}
@ -520,18 +716,7 @@ document.addEventListener('DOMContentLoaded', () => {
eventDetails = document.getElementById('eventDetails');
const connectRelayBtn = document.getElementById('connectRelayBtn');
const startSubscriptionBtn = document.getElementById('startSubscriptionBtn');
const stopSubscriptionBtn = document.getElementById('stopSubscriptionBtn');
const clearEventsBtn = document.getElementById('clearEventsBtn');
const subscriptionPubkeyInput = document.getElementById('subscriptionPubkey') as HTMLInputElement;
/**
* Get the logged-in user's public key from localStorage
* This is populated by the nostr-login plugin
*/
function getLoggedInPubkey(): string | null {
return localStorage.getItem('userPublicKey');
}
// Connect to relay and automatically start subscription for logged-in user
if (connectRelayBtn) {
@ -540,6 +725,10 @@ document.addEventListener('DOMContentLoaded', () => {
return;
}
// Check if we're logged in before connecting
const userPubkey = getLoggedInPubkey();
console.log("User pubkey when connecting to relay:", userPubkey);
const relayUrl = relayUrlInput.value.trim();
if (!relayUrl || !relayUrl.startsWith('wss://')) {
updateRelayStatus('Invalid relay URL. Must start with wss://', 'error');
@ -549,19 +738,15 @@ document.addEventListener('DOMContentLoaded', () => {
const success = await connectToRelay(relayUrl);
if (success) {
// Get the logged-in user's pubkey from localStorage
let pubkeyFilter = subscriptionPubkeyInput?.value.trim();
// Get the logged-in user's pubkey from localStorage as the default
const userPubkey = getLoggedInPubkey();
const pubkeyFilter = userPubkey || '';
// If no pubkey is specified in the input field, use the logged-in user's pubkey
if (!pubkeyFilter) {
const userPubkey = getLoggedInPubkey();
if (userPubkey) {
pubkeyFilter = userPubkey;
// Update the input field to show which pubkey we're using
if (subscriptionPubkeyInput) {
subscriptionPubkeyInput.value = userPubkey;
}
}
// Log the pubkey we're using for subscription
if (userPubkey) {
console.log(`Using logged-in user's pubkey for subscription: ${userPubkey.substring(0, 8)}...`);
} else {
console.log('No user pubkey found, subscribing to all kind 21120 events');
}
// Automatically subscribe to kind 21120 events
@ -578,72 +763,15 @@ document.addEventListener('DOMContentLoaded', () => {
} catch (subError) {
console.error("Subscription error:", subError);
updateRelayStatus(`Subscription error: ${subError instanceof Error ? subError.message : String(subError)}`, 'error');
// Return subscription button to enabled state
if (startSubscriptionBtn && stopSubscriptionBtn) {
stopSubscriptionBtn.setAttribute('disabled', 'disabled');
startSubscriptionBtn.removeAttribute('disabled');
}
// No need to update subscription buttons since they're removed
}
// Update UI to reflect active subscription
if (startSubscriptionBtn && stopSubscriptionBtn) {
startSubscriptionBtn.setAttribute('disabled', 'disabled');
stopSubscriptionBtn.removeAttribute('disabled');
}
// Successful subscription message
console.log(`Subscribed to kind 21120 events for ${pubkeyFilter || 'all users'}`);
}
});
}
// Start subscription
if (startSubscriptionBtn && stopSubscriptionBtn) {
startSubscriptionBtn.addEventListener('click', async () => {
if (!relayPool || !activeRelayUrl) {
console.error('Please connect to a relay first');
return;
}
// Update status to indicate subscription is in progress
updateRelayStatus('Subscribing...', 'connecting');
try {
// Get pubkey if specified
const subscriptionPubkeyInput = document.getElementById('subscriptionPubkey') as HTMLInputElement;
const pubkeyFilter = subscriptionPubkeyInput?.value.trim();
await subscribeToEvents({
pubkeyFilter
});
// Update UI to reflect active subscription
if (startSubscriptionBtn && stopSubscriptionBtn) {
startSubscriptionBtn.setAttribute('disabled', 'disabled');
stopSubscriptionBtn.removeAttribute('disabled');
}
} catch (subError) {
console.error("Subscription error:", subError);
updateRelayStatus(`Subscription error: ${subError instanceof Error ? subError.message : String(subError)}`, 'error');
// Leave buttons in their current state if there's an error
if (startSubscriptionBtn) {
startSubscriptionBtn.removeAttribute('disabled');
}
}
});
}
// Stop subscription
if (stopSubscriptionBtn && startSubscriptionBtn) {
stopSubscriptionBtn.addEventListener('click', () => {
if (activeSubscription) {
activeSubscription.unsub();
activeSubscription = null;
}
if (stopSubscriptionBtn && startSubscriptionBtn) {
stopSubscriptionBtn.setAttribute('disabled', 'disabled');
startSubscriptionBtn.removeAttribute('disabled');
// Add subscribed indication to the UI
updateRelayStatus('Connected and subscribed ✓', 'connected');
}
});
}
@ -654,7 +782,7 @@ document.addEventListener('DOMContentLoaded', () => {
receivedEvents.clear();
if (eventsList) {
eventsList.innerHTML = '<div class="empty-state">No events received yet. Connect to a relay and start a subscription.</div>';
eventsList.innerHTML = '<div class="empty-state">No events received yet. Connect to a relay to start receiving events.</div>';
}
if (eventDetails) {

@ -747,32 +747,126 @@ footer {
color: var(--text-tertiary);
cursor: not-allowed;
}
.events-list {
max-height: 300px;
overflow-y: auto;
border: 1px solid var(--border-color);
border-radius: 4px;
background-color: var(--bg-secondary);
/* Events container for side-by-side layout */
.events-container {
display: flex;
gap: 20px;
margin-top: 15px;
}
.events-sidebar {
flex: 0 0 300px;
}
.events-content {
flex: 1;
}
.events-list {
max-height: 600px;
overflow-y: auto;
border: 1px solid var(--border-color);
border-radius: 8px;
background-color: var(--bg-secondary);
background-color: var(--bg-secondary);
}
.event-item {
padding: 10px;
padding: 15px;
border-bottom: 1px solid var(--border-color);
cursor: pointer;
transition: all 0.2s ease;
}
.event-item:last-child {
border-bottom: none;
}
.event-item:hover {
background-color: var(--bg-tertiary);
}
.event-item.selected {
background-color: var(--bg-tertiary);
border-left: 4px solid var(--accent-color);
}
.event-header {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
font-size: 14px;
}
.event-id {
font-weight: bold;
color: var(--accent-color);
}
.event-time {
color: var(--text-tertiary);
}
.event-summary {
font-size: 13px;
margin-bottom: 10px;
color: var(--text-secondary);
line-height: 1.4;
}
.event-actions {
text-align: right;
}
.view-details-btn {
font-size: 12px;
padding: 4px 8px;
background-color: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 4px;
color: var(--text-secondary);
cursor: pointer;
}
.view-details-btn:hover {
background-color: var(--accent-color);
color: white;
}
.event-details {
margin-top: 20px;
padding: 15px;
padding: 20px;
background-color: var(--bg-secondary);
border-radius: 8px;
border: 1px solid var(--border-color);
min-height: 600px;
}
.event-detail-item {
margin-bottom: 15px;
line-height: 1.5;
}
.event-detail-item strong {
color: var(--accent-color);
}
.event-detail-item ul {
padding-left: 20px;
margin-top: 8px;
}
.event-content {
margin-top: 10px;
padding: 15px;
border-radius: 4px;
background-color: var(--bg-tertiary);
max-height: 300px;
overflow-y: auto;
font-family: 'Courier New', monospace;
font-size: 13px;
line-height: 1.4;
white-space: pre-wrap;
}
.empty-state {
padding: 20px;
text-align: center;
@ -803,6 +897,152 @@ footer {
.copy-button:hover {
background-color: var(--button-hover);
/* Enhanced Event Details Styling */
.event-detail-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid var(--border-color);
}
.event-detail-header h3 {
margin: 0;
color: var(--accent-color);
}
.event-timestamp {
color: var(--text-tertiary);
font-size: 14px;
}
.event-detail-metadata {
margin-bottom: 20px;
padding: 15px;
background-color: var(--bg-tertiary);
border-radius: 4px;
}
.metadata-value {
font-family: 'Courier New', monospace;
font-size: 13px;
word-break: break-all;
}
.event-tags {
max-height: 150px;
overflow-y: auto;
}
.event-detail-content {
margin-top: 20px;
}
.content-header {
margin-bottom: 10px;
color: var(--accent-color);
}
.decrypted-badge {
background-color: var(--button-success);
color: white;
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
margin-left: 8px;
}
/* Content row for side-by-side display */
.content-row {
display: flex;
gap: 20px;
flex-wrap: wrap;
}
.event-metadata-column {
flex: 0 0 250px;
background-color: var(--bg-tertiary);
border-radius: 6px;
padding: 15px;
}
.http-content-column {
flex: 1;
min-width: 300px;
}
.metadata-content {
margin-top: 10px;
line-height: 1.6;
font-family: 'Courier New', monospace;
font-size: 13px;
word-break: break-all;
}
.http-content {
margin-top: 10px;
padding: 15px;
border-radius: 4px;
background-color: var(--bg-tertiary);
min-height: 200px;
max-height: 400px;
overflow-y: auto;
font-family: 'Courier New', monospace;
font-size: 13px;
line-height: 1.4;
white-space: pre-wrap;
border-left: 4px solid var(--accent-color);
}
.raw-content-column {
flex: 1 0 100%;
margin-top: 20px;
}
.raw-content {
margin-top: 10px;
padding: 15px;
border-radius: 4px;
background-color: var(--bg-tertiary);
max-height: 200px;
overflow-y: auto;
font-family: 'Courier New', monospace;
font-size: 12px;
line-height: 1.4;
white-space: pre-wrap;
color: var(--text-tertiary);
}
.toggle-raw-btn {
float: right;
font-size: 12px;
padding: 2px 8px;
background-color: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 4px;
cursor: pointer;
}
.toggle-raw-btn:hover {
background-color: var(--accent-color);
color: white;
}
.event-type {
display: inline-block;
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
background-color: var(--bg-tertiary);
color: var(--text-primary);
}
.event-item:hover .event-type {
background-color: var(--accent-color);
color: white;
}
}
.copy-button:active {
@ -812,4 +1052,44 @@ footer {
.copy-button.copied {
background-color: var(--button-success);
}
/* Dark mode is already handled by CSS variables */
/* Dark mode is already handled by CSS variables */
/* Responsive adjustments for the events container */
@media (max-width: 768px) {
.events-container {
flex-direction: column;
}
.events-sidebar {
flex: auto;
width: 100%;
}
.events-list {
max-height: 300px;
}
.event-details {
min-height: 400px;
}
}
/* Clear events button */
.event-controls {
margin-bottom: 10px;
text-align: right;
}
.clear-events-button {
padding: 8px 15px;
background-color: #dc3545;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.clear-events-button:hover {
background-color: #c82333;
}