Files
c-relay/api/index.html
2025-09-27 14:08:45 -04:00

2680 lines
106 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>C-Relay Admin API</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Courier New', monospace;
background-color: white;
color: black;
line-height: 1.4;
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
h1 {
border-bottom: 2px solid black;
padding-bottom: 10px;
margin-bottom: 30px;
font-weight: normal;
font-size: 24px;
}
h2 {
margin: 30px 0 15px 0;
font-weight: normal;
border-left: 4px solid black;
padding-left: 10px;
font-size: 16px;
}
.section {
border: 1px solid black;
padding: 20px;
margin-bottom: 20px;
}
.input-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
font-size: 14px;
}
input,
textarea,
select,
button {
width: 100%;
padding: 8px;
border: 1px solid black;
font-family: 'Courier New', monospace;
font-size: 14px;
background-color: white;
color: black;
}
button {
background-color: black;
color: white;
cursor: pointer;
margin: 5px 0;
font-weight: bold;
}
button:hover {
background-color: #333;
}
button:disabled {
background-color: #ccc;
color: #666;
cursor: not-allowed;
}
.status {
padding: 10px;
margin: 10px 0;
border: 1px solid black;
font-weight: bold;
}
.status.connected {
background-color: black;
color: white;
}
.status.disconnected {
background-color: white;
color: black;
}
.status.authenticated {
background-color: black;
color: white;
}
.status.error {
background-color: white;
color: black;
border: 2px solid black;
}
.config-table {
border: 1px solid black;
width: 100%;
border-collapse: collapse;
margin: 10px 0;
}
.config-table th,
.config-table td {
border: 1px solid black;
padding: 8px;
text-align: left;
}
.config-table th {
background-color: black;
color: white;
font-weight: bold;
}
.json-display {
background-color: white;
border: 1px solid black;
padding: 10px;
font-family: 'Courier New', monospace;
font-size: 12px;
white-space: pre-wrap;
max-height: 300px;
overflow-y: auto;
margin: 10px 0;
}
.log-panel {
height: 200px;
overflow-y: auto;
border: 1px solid black;
padding: 10px;
font-size: 12px;
background-color: white;
}
.log-entry {
margin-bottom: 5px;
border-bottom: 1px solid #ccc;
padding-bottom: 5px;
}
.log-timestamp {
font-weight: bold;
}
.inline-buttons {
display: flex;
gap: 10px;
}
.inline-buttons button {
flex: 1;
}
.user-info {
padding: 10px;
border: 1px solid black;
margin: 10px 0;
background-color: white;
}
.user-pubkey {
font-family: 'Courier New', monospace;
font-size: 12px;
word-break: break-all;
margin: 5px 0;
}
.hidden {
display: none;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
border-bottom: 1px solid black;
padding-bottom: 10px;
}
.auth-rules-controls {
margin-bottom: 15px;
}
.section-header .status {
margin: 0;
padding: 5px 10px;
min-width: auto;
font-size: 12px;
}
.section-header .status:before {
content: '';
}
/* Auth Rule Input Sections Styling */
.auth-rule-section {
border: 1px solid black;
padding: 15px;
margin: 15px 0;
background-color: white;
}
.auth-rule-section h3 {
margin: 0 0 10px 0;
font-size: 14px;
font-weight: bold;
border-left: 4px solid black;
padding-left: 8px;
}
.auth-rule-section p {
margin: 0 0 15px 0;
font-size: 13px;
color: #666;
}
.rule-status {
margin-top: 10px;
padding: 8px;
border: 1px solid #ccc;
font-size: 12px;
min-height: 20px;
background-color: #f9f9f9;
}
.rule-status.success {
border-color: #4CAF50;
background-color: #E8F5E8;
color: #2E7D32;
}
.rule-status.error {
border-color: #F44336;
background-color: #FFEBEE;
color: #C62828;
}
.rule-status.warning {
border-color: #FF9800;
background-color: #FFF3E0;
color: #E65100;
}
.warning-box {
border: 2px solid #FF9800;
background-color: #FFF3E0;
padding: 10px;
margin: 10px 0;
font-size: 13px;
color: #E65100;
}
.warning-box strong {
color: #D84315;
}
#login-section {
text-align: center;
padding: 20px;
}
@media (max-width: 768px) {
body {
padding: 10px;
}
.inline-buttons {
flex-direction: column;
}
h1 {
font-size: 20px;
}
h2 {
font-size: 14px;
}
}
</style>
</head>
<body>
<h1>C-RELAY ADMIN API</h1>
<!-- Testing Section - Always Visible -->
<div class="section">
<h2>DEBUG - TEST FETCH WITHOUT LOGIN</h2>
<div class="input-group">
<label for="relay-url">Relay URL:</label>
<input type="text" id="relay-url" value="ws://localhost:8888" placeholder="ws://localhost:8888">
</div>
<div class="status disconnected" id="relay-status">READY TO FETCH</div>
<div class="inline-buttons">
<button type="button" id="fetch-config-btn">FETCH CONFIGURATION (NO LOGIN)</button>
</div>
<div class="status disconnected" id="config-status">NO CONFIGURATION LOADED</div>
<div id="config-display" class="hidden">
<div id="config-view-mode">
<table class="config-table" id="config-table">
<thead>
<tr>
<th>Parameter</th>
<th>Value</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="config-table-body">
</tbody>
</table>
<div class="inline-buttons">
<button type="button" id="edit-config-btn">EDIT CONFIGURATION</button>
<button type="button" id="copy-config-btn">COPY CONFIGURATION</button>
</div>
</div>
<div id="config-edit-mode" class="hidden">
<h3>Edit Configuration</h3>
<div id="config-form" class="section">
<!-- Dynamic form will be generated here -->
</div>
<div class="inline-buttons">
<button type="button" id="save-config-btn">SAVE & PUBLISH</button>
<button type="button" id="cancel-edit-btn">CANCEL</button>
</div>
</div>
<div id="config-raw-display">
<h3>Raw Event JSON:</h3>
<div class="json-display" id="raw-config-json"></div>
</div>
</div>
</div>
<!-- TESTS Section -->
<div class="section">
<h2>ADMIN API TESTS</h2>
<p>Test the admin API functionality with real-time event logging. Login required for authenticated tests.</p>
<!-- Event Log Display -->
<div class="input-group">
<label for="test-event-log">Event Log (Sent/Received):</label>
<div class="log-panel" id="test-event-log" style="height: 300px;">
<div class="log-entry">
<span class="log-timestamp">SYSTEM:</span> Test interface ready. Click buttons below to test admin API functions.
</div>
</div>
<button type="button" id="clear-test-log-btn">CLEAR TEST LOG</button>
</div>
<!-- Test Buttons -->
<div class="input-group">
<label>Admin API Tests:</label>
<div class="inline-buttons">
<button type="button" id="test-get-auth-rules-btn">GET AUTH RULES</button>
<button type="button" id="test-clear-auth-rules-btn">CLEAR AUTH RULES</button>
</div>
<div class="inline-buttons">
<button type="button" id="test-add-blacklist-btn">ADD BLACKLIST</button>
<button type="button" id="test-add-whitelist-btn">ADD WHITELIST</button>
</div>
<div class="inline-buttons">
<button type="button" id="test-post-event-btn">POST EVENT</button>
</div>
</div>
<!-- Test Input Fields -->
<div id="test-input-section" style="display: none;">
<div class="input-group">
<label for="test-pubkey-input">Test Pubkey (for blacklist/whitelist):</label>
<div style="display: flex; gap: 10px; align-items: flex-start;">
<input type="text" id="test-pubkey-input" placeholder="Enter pubkey or nsec1... for testing" style="flex: 1;">
<button type="button" id="generate-test-key-btn" style="width: auto; padding: 8px 16px; white-space: nowrap;">GENERATE KEY</button>
</div>
<small>This pubkey will be used for blacklist/whitelist tests</small>
</div>
</div>
<!-- Test Status -->
<div class="status disconnected" id="test-status">READY TO TEST</div>
</div>
<!-- Auth Rules Management -->
<div class="section" id="authRulesSection" style="display: none;">
<div class="section-header">
<h2>AUTH RULES MANAGEMENT</h2>
<div class="status" id="authRulesStatus"></div>
</div>
<div class="auth-rules-controls">
<div class="inline-buttons">
<button id="viewAuthRulesBtn" class="btn">VIEW RULES</button>
<button id="refreshAuthRulesBtn" class="btn">REFRESH</button>
</div>
</div>
<!-- Auth Rules Table -->
<div id="authRulesTableContainer" style="display: none;">
<table class="config-table" id="authRulesTable">
<thead>
<tr>
<th>Rule Type</th>
<th>Pattern Type</th>
<th>Pattern Value</th>
<th>Action</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="authRulesTableBody">
</tbody>
</table>
</div>
<!-- Streamlined Auth Rule Input Sections -->
<div id="authRuleInputSections" style="display: block;">
<!-- Blacklist Section -->
<div class="auth-rule-section">
<h3>BLACKLIST PUBKEY</h3>
<p>Block a specific user from all operations</p>
<div class="input-group">
<label for="blacklistPubkey">Pubkey (nsec or hex):</label>
<input type="text" id="blacklistPubkey" placeholder="nsec1... or 64-character hex pubkey">
<small id="blacklistHelp">Enter nsec (will auto-convert) or 64-character hex pubkey</small>
</div>
<button type="button" id="addBlacklistBtn" onclick="addBlacklistRule()">ADD TO BLACKLIST</button>
<div id="blacklistStatus" class="rule-status"></div>
</div>
<!-- Whitelist Section -->
<div class="auth-rule-section">
<h3>WHITELIST PUBKEY</h3>
<p>Allow only specific users (converts relay to whitelist-only mode)</p>
<div class="input-group">
<label for="whitelistPubkey">Pubkey (nsec or hex):</label>
<input type="text" id="whitelistPubkey" placeholder="nsec1... or 64-character hex pubkey">
<small id="whitelistHelp">Enter nsec (will auto-convert) or 64-character hex pubkey</small>
</div>
<div id="whitelistWarning" class="warning-box" style="display: none;">
<strong>⚠️ WARNING:</strong> Adding whitelist rules changes relay behavior to whitelist-only mode.
Only whitelisted users will be able to interact with the relay.
</div>
<button type="button" id="addWhitelistBtn" onclick="addWhitelistRule()">ADD TO WHITELIST</button>
<div id="whitelistStatus" class="rule-status"></div>
</div>
<!-- Hash Blacklist Section -->
<div class="auth-rule-section">
<h3>BLACKLIST CONTENT HASH</h3>
<p>Block specific content by SHA-256 hash</p>
<div class="input-group">
<label for="hashBlacklist">Content Hash (SHA-256):</label>
<input type="text" id="hashBlacklist" placeholder="64-character hex SHA-256 hash">
<small id="hashHelp">Enter 64-character hex SHA-256 hash of content to block</small>
</div>
<button type="button" id="addHashBlacklistBtn" onclick="addHashBlacklistRule()">BLOCK CONTENT HASH</button>
<div id="hashStatus" class="rule-status"></div>
</div>
</div>
<!-- Auth Rules Status Display -->
<div id="authRulesStatusDisplay" style="display: none;">
<h3>Auth System Status</h3>
<div class="status" id="authSystemStatus">CHECKING...</div>
<div class="json-display" id="authRulesCount">
Rules: Loading...
</div>
</div>
</div>
<!-- Login Section -->
<div id="login-section">
<div class="section">
<h2>NOSTR AUTHENTICATION</h2>
<p>Please login with your Nostr identity to access the admin interface.</p>
<!-- nostr-lite login UI will be injected here -->
</div>
</div>
<!-- Main Interface (hidden until logged in) -->
<div id="main-interface" class="hidden">
<!-- User Info Section -->
<div class="section">
<h2>LOGGED IN USER</h2>
<div class="user-info">
<div><strong>Name:</strong> <span id="user-name">Loading...</span></div>
<div><strong>Public Key:</strong>
<div class="user-pubkey" id="user-pubkey">Loading...</div>
</div>
<div><strong>About:</strong> <span id="user-about">Loading...</span></div>
</div>
<button type="button" id="logout-btn">LOGOUT</button>
</div>
<!-- Command Section -->
<div class="section">
<h2>ADMIN COMMANDS</h2>
<div class="input-group">
<label for="command-type">Command Type:</label>
<select id="command-type">
<option value="">Select command type...</option>
<option value="update_config">Update Configuration</option>
<option value="restart_relay">Restart Relay</option>
<option value="get_status">Get Status</option>
</select>
</div>
<div class="input-group">
<label for="command-payload">Command Payload (JSON):</label>
<textarea id="command-payload" rows="4" placeholder='{"param": "value"}'></textarea>
</div>
<div class="input-group">
<label>Event Preview:</label>
<div class="json-display" id="event-preview">No event constructed</div>
</div>
<button type="button" id="send-command-btn">SEND COMMAND</button>
</div>
<!-- Log Section -->
<div class="section">
<h2>EVENT LOG</h2>
<div class="log-panel" id="log-panel">
<div class="log-entry">
<span class="log-timestamp">SYSTEM:</span> Interface loaded. Login and connect to relay to begin.
</div>
</div>
<button type="button" id="clear-log-btn">CLEAR LOG</button>
</div>
</div>
<!-- Load the official nostr-tools bundle first -->
<!-- <script src="./nostr.bundle.js"></script> -->
<script src="https://laantungir.net/nostr-login-lite/nostr.bundle.js"></script>
<!-- Load NOSTR_LOGIN_LITE main library -->
<script src="https://laantungir.net/nostr-login-lite/nostr-lite.js"></script>
<!-- <script src="./nostr-lite.js"></script> -->
<script>
// Global error handler to prevent page refreshes
window.addEventListener('error', function (e) {
console.error('Global error caught:', e.error);
console.error('Error message:', e.message);
console.error('Error filename:', e.filename);
console.error('Error line:', e.lineno);
e.preventDefault(); // Prevent default browser error handling
return true; // Prevent page refresh
});
window.addEventListener('unhandledrejection', function (e) {
console.error('Unhandled promise rejection:', e.reason);
e.preventDefault(); // Prevent default browser error handling
return true; // Prevent page refresh
});
// Global state
let nlLite = null;
let userPubkey = null;
let isLoggedIn = false;
let currentConfig = null;
// Global subscription state
let relayPool = null;
let subscriptionId = null;
// DOM elements
const loginSection = document.getElementById('login-section');
const mainInterface = document.getElementById('main-interface');
const userName = document.getElementById('user-name');
const userPubkeyDisplay = document.getElementById('user-pubkey');
const userAbout = document.getElementById('user-about');
const logoutBtn = document.getElementById('logout-btn');
const relayUrl = document.getElementById('relay-url');
const relayStatus = document.getElementById('relay-status');
const fetchConfigBtn = document.getElementById('fetch-config-btn');
const configStatus = document.getElementById('config-status');
const configDisplay = document.getElementById('config-display');
const configViewMode = document.getElementById('config-view-mode');
const configEditMode = document.getElementById('config-edit-mode');
const configTableBody = document.getElementById('config-table-body');
const configForm = document.getElementById('config-form');
const rawConfigJson = document.getElementById('raw-config-json');
const copyConfigBtn = document.getElementById('copy-config-btn');
const editConfigBtn = document.getElementById('edit-config-btn');
const saveConfigBtn = document.getElementById('save-config-btn');
const cancelEditBtn = document.getElementById('cancel-edit-btn');
const commandType = document.getElementById('command-type');
const commandPayload = document.getElementById('command-payload');
const eventPreview = document.getElementById('event-preview');
const sendCommandBtn = document.getElementById('send-command-btn');
const logPanel = document.getElementById('log-panel');
const clearLogBtn = document.getElementById('clear-log-btn');
// Utility functions
function log(message, type = 'INFO') {
const timestamp = new Date().toISOString().split('T')[1].split('.')[0];
const logMessage = `${timestamp} [${type}]: ${message}`;
// Always log to browser console so we don't lose logs on refresh
console.log(logMessage);
// Also log to UI if elements exist
if (logPanel) {
const logEntry = document.createElement('div');
logEntry.className = 'log-entry';
logEntry.innerHTML = `<span class="log-timestamp">${timestamp} [${type}]:</span> ${message}`;
logPanel.appendChild(logEntry);
logPanel.scrollTop = logPanel.scrollHeight;
}
}
// Check for existing authentication state with multiple API methods and retry logic
async function checkExistingAuthWithRetries() {
console.log('Starting authentication state detection with retry logic...');
const maxAttempts = 10;
const delay = 500; // ms between attempts
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
console.log(`Authentication detection attempt ${attempt}/${maxAttempts}`);
try {
// Method 1: Try window.NOSTR_LOGIN_LITE.getAuthState()
if (window.NOSTR_LOGIN_LITE && typeof window.NOSTR_LOGIN_LITE.getAuthState === 'function') {
console.log('Trying window.NOSTR_LOGIN_LITE.getAuthState()...');
const authState = window.NOSTR_LOGIN_LITE.getAuthState();
if (authState && authState.pubkey) {
console.log('✅ Auth state found via NOSTR_LOGIN_LITE.getAuthState():', authState.pubkey);
await restoreAuthenticationState(authState.pubkey);
return true;
}
}
// Method 2: Try nlLite.getPublicKey()
if (nlLite && typeof nlLite.getPublicKey === 'function') {
console.log('Trying nlLite.getPublicKey()...');
const pubkey = await nlLite.getPublicKey();
if (pubkey && pubkey.length === 64) {
console.log('✅ Pubkey found via nlLite.getPublicKey():', pubkey);
await restoreAuthenticationState(pubkey);
return true;
}
}
// Method 3: Try window.nostr.getPublicKey() (NIP-07)
if (window.nostr && typeof window.nostr.getPublicKey === 'function') {
console.log('Trying window.nostr.getPublicKey()...');
const pubkey = await window.nostr.getPublicKey();
if (pubkey && pubkey.length === 64) {
console.log('✅ Pubkey found via window.nostr.getPublicKey():', pubkey);
await restoreAuthenticationState(pubkey);
return true;
}
}
// Method 4: Check localStorage directly for NOSTR_LOGIN_LITE data
const localStorageData = localStorage.getItem('NOSTR_LOGIN_LITE_DATA');
if (localStorageData) {
try {
const parsedData = JSON.parse(localStorageData);
if (parsedData.pubkey) {
console.log('✅ Pubkey found in localStorage:', parsedData.pubkey);
await restoreAuthenticationState(parsedData.pubkey);
return true;
}
} catch (parseError) {
console.log('Failed to parse localStorage data:', parseError.message);
}
}
console.log(`❌ Attempt ${attempt}: No authentication found via any method`);
// Wait before next attempt (except for last attempt)
if (attempt < maxAttempts) {
await new Promise(resolve => setTimeout(resolve, delay));
}
} catch (error) {
console.log(`❌ Attempt ${attempt} failed:`, error.message);
if (attempt < maxAttempts) {
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
console.log('🔍 Authentication detection completed - no existing auth found after all attempts');
return false;
}
// Helper function to restore authentication state
async function restoreAuthenticationState(pubkey) {
console.log('🔄 Restoring authentication state for pubkey:', pubkey);
userPubkey = pubkey;
isLoggedIn = true;
// Show main interface
showMainInterface();
loadUserProfile();
// Automatically fetch configuration after restoring login
setTimeout(() => {
fetchConfiguration().catch(error => {
console.log('Auto-fetch configuration failed after restore: ' + error.message);
});
}, 1000);
console.log('✅ Authentication state restored successfully');
}
// Legacy function for backward compatibility
async function checkExistingAuth() {
return await checkExistingAuthWithRetries();
}
// Initialize NOSTR_LOGIN_LITE
async function initializeApp() {
try {
await window.NOSTR_LOGIN_LITE.init({
theme: 'default',
methods: {
extension: true,
local: true,
seedphrase: true,
readonly: true,
connect: true,
remote: true,
otp: true
},
floatingTab: {
enabled: true,
hPosition: 1, // 0.0-1.0 or '95%' from left
vPosition: 0, // 0.0-1.0 or '50%' from top
appearance: {
style: 'square', // 'pill', 'square', 'circle', 'minimal'
// icon: '[LOGIN]', // Now uses text-based icons like [LOGIN], [KEY], [NET]
text: 'Login'
},
behavior: {
hideWhenAuthenticated: false,
showUserInfo: true,
autoSlide: true
},
animation: {
slideDirection: 'auto' // 'auto', 'left', 'right', 'up', 'down'
}
}
});
nlLite = window.NOSTR_LOGIN_LITE;
console.log('Nostr login system initialized');
// Check for existing authentication state after initialization
const wasAlreadyLoggedIn = await checkExistingAuth();
if (wasAlreadyLoggedIn) {
console.log('User was already logged in, main interface restored');
} else {
console.log('No existing authentication found, showing login interface');
}
// Listen for authentication events
window.addEventListener('nlMethodSelected', handleAuthEvent);
} catch (error) {
console.log('Failed to initialize Nostr login: ' + error.message);
}
}
// Handle authentication events
function handleAuthEvent(event) {
const { pubkey, method, error } = event.detail;
if (method && pubkey) {
userPubkey = pubkey;
isLoggedIn = true;
console.log(`Login successful! Method: ${method}`);
console.log(`Public key: ${pubkey}`);
showMainInterface();
loadUserProfile();
// Automatically fetch configuration after login
setTimeout(() => {
fetchConfiguration().catch(error => {
console.log('Auto-fetch configuration failed: ' + error.message);
});
}, 1000);
console.log('Login successful. Auto-fetching configuration...');
} else if (error) {
console.log(`Authentication error: ${error}`);
}
}
// Show main interface after login
function showMainInterface() {
loginSection.classList.add('hidden');
mainInterface.classList.remove('hidden');
userPubkeyDisplay.textContent = userPubkey;
}
// Load user profile using nostr-tools pool
async function loadUserProfile() {
if (!userPubkey) return;
console.log('Loading user profile...');
userName.textContent = 'Loading...';
userAbout.textContent = 'Loading...';
try {
// Create a SimplePool instance for profile loading
const profilePool = new window.NostrTools.SimplePool();
const relays = ['wss://relay.laantungir.net'];
// Get profile event (kind 0) for the user
const events = await profilePool.querySync(relays, {
kinds: [0],
authors: [userPubkey],
limit: 1
});
if (events.length > 0) {
const profile = JSON.parse(events[0].content);
displayProfile(profile);
} else {
userName.textContent = 'Anonymous User';
userAbout.textContent = 'No profile found';
}
// Close the profile pool
profilePool.close(relays);
} catch (error) {
console.log('Profile loading failed: ' + error.message);
userName.textContent = 'Error loading profile';
userAbout.textContent = error.message;
}
}
// Display profile data
function displayProfile(profile) {
const name = profile.name || profile.display_name || profile.displayName || 'Anonymous User';
const about = profile.about || 'No description provided';
userName.textContent = name;
userAbout.textContent = about;
console.log(`Profile loaded for: ${name}`);
}
// Logout function
async function logout() {
console.log('Logging out...');
try {
// Clean up configuration pool
if (relayPool) {
console.log('Closing configuration pool...');
const url = relayUrl.value.trim();
if (url) {
relayPool.close([url]);
}
relayPool = null;
subscriptionId = null;
}
await nlLite.logout();
userPubkey = null;
isLoggedIn = false;
currentConfig = null;
// Reset UI
mainInterface.classList.add('hidden');
loginSection.classList.remove('hidden');
updateConfigStatus(false);
relayStatus.textContent = 'READY TO FETCH';
relayStatus.className = 'status disconnected';
console.log('Logged out successfully');
} catch (error) {
console.log('Logout failed: ' + error.message);
}
}
function updateConfigStatus(loaded) {
if (loaded) {
configStatus.textContent = 'CONFIGURATION LOADED';
configStatus.className = 'status connected';
configDisplay.classList.remove('hidden');
} else {
configStatus.textContent = 'NO CONFIGURATION LOADED';
configStatus.className = 'status disconnected';
configDisplay.classList.add('hidden');
}
}
// Generate random subscription ID
function generateSubId() {
return Math.random().toString(36).substring(2, 15);
}
// Configuration subscription using nostr-tools SimplePool
async function subscribeToConfiguration() {
try {
console.log('=== STARTING SIMPLEPOOL CONFIGURATION SUBSCRIPTION ===');
if (!isLoggedIn) {
console.log('WARNING: Not logged in, but proceeding with subscription test');
}
const url = relayUrl.value.trim();
if (!url) {
console.error('Please enter a relay URL');
return false;
}
console.log(`Connecting to relay via SimplePool: ${url}`);
relayStatus.textContent = 'CONNECTING...';
relayStatus.className = 'status connected';
// Clean up existing pool
if (relayPool) {
console.log('Closing existing pool connection');
relayPool.close([url]);
relayPool = null;
subscriptionId = null;
}
// Create new SimplePool instance
relayPool = new window.NostrTools.SimplePool();
subscriptionId = generateSubId();
console.log(`Generated subscription ID: ${subscriptionId}`);
relayStatus.textContent = 'CONNECTED - SUBSCRIBING...';
relayStatus.className = 'status connected';
// Subscribe to kind 23457 events (admin response events)
const subscription = relayPool.subscribeMany([url], [{
kinds: [23457],
authors: [getRelayPubkey()], // Only listen to responses from the relay
"#p": [userPubkey], // Only responses directed to this user
limit: 50
}], {
onevent(event) {
console.log('=== ADMIN RESPONSE EVENT RECEIVED VIA SIMPLEPOOL ===');
console.log('Event data:', event);
console.log('Event kind:', event.kind);
console.log('Event tags:', event.tags);
console.log('Event pubkey:', event.pubkey);
console.log('=== END ADMIN RESPONSE ===');
// Log all received messages for testing
if (typeof logTestEvent === 'function') {
logTestEvent('RECV', `Admin response event: ${JSON.stringify(event)}`, 'EVENT');
}
// Process admin response event
processAdminResponse(event);
relayStatus.textContent = 'SUBSCRIBED - LIVE UPDATES';
relayStatus.className = 'status connected';
},
oneose() {
console.log('EOSE received - End of stored events');
console.log('Current config after EOSE:', currentConfig);
if (!currentConfig) {
console.log('No configuration events were received');
configStatus.textContent = 'NO CONFIGURATION EVENTS FOUND';
configStatus.className = 'status error';
relayStatus.textContent = 'SUBSCRIBED - NO EVENTS FOUND';
relayStatus.className = 'status error';
} else {
relayStatus.textContent = 'SUBSCRIBED - LIVE UPDATES';
relayStatus.className = 'status connected';
}
},
onclose(reason) {
console.log('Subscription closed:', reason);
relayStatus.textContent = 'SUBSCRIPTION CLOSED';
relayStatus.className = 'status error';
updateConfigStatus(false);
}
});
// Store subscription for cleanup
relayPool.currentSubscription = subscription;
console.log('SimplePool subscription established');
return true;
} catch (error) {
console.error('Configuration subscription failed:', error.message);
console.error('Configuration subscription failed:', error);
console.error('Error stack:', error.stack);
relayStatus.textContent = 'SUBSCRIPTION FAILED';
relayStatus.className = 'status error';
return false;
}
}
// Process admin response events (kind 23457)
async function processAdminResponse(event) {
try {
console.log('=== PROCESSING ADMIN RESPONSE ===');
console.log('Response event:', event);
// Verify this is a kind 23457 admin response event
if (event.kind !== 23457) {
console.log('Ignoring non-admin response event, kind:', event.kind);
return;
}
// Verify the event is from the relay
const expectedRelayPubkey = getRelayPubkey();
if (event.pubkey !== expectedRelayPubkey) {
console.log('Ignoring response from unknown pubkey:', event.pubkey);
return;
}
// Decrypt the NIP-44 encrypted content
const decryptedContent = await decryptFromRelay(event.content);
if (!decryptedContent) {
throw new Error('Failed to decrypt admin response content');
}
console.log('Decrypted admin response:', decryptedContent);
// Parse the decrypted JSON response
const responseData = JSON.parse(decryptedContent);
console.log('Parsed response data:', responseData);
// Log the response for testing
if (typeof logTestEvent === 'function') {
logTestEvent('RECV', `Decrypted response: ${JSON.stringify(responseData)}`, 'RESPONSE');
}
// Handle different types of admin responses
handleAdminResponseData(responseData);
} catch (error) {
console.error('Error processing admin response:', error);
if (typeof logTestEvent === 'function') {
logTestEvent('ERROR', `Failed to process admin response: ${error.message}`, 'ERROR');
}
}
}
// Handle different types of admin response data
function handleAdminResponseData(responseData) {
try {
console.log('=== HANDLING ADMIN RESPONSE DATA ===');
console.log('Response data:', responseData);
// Handle auth query responses
if (responseData.query_type) {
handleAuthQueryResponse(responseData);
return;
}
// Handle system command responses
if (responseData.command) {
handleSystemCommandResponse(responseData);
return;
}
// Handle auth rule modification responses
if (responseData.operation) {
handleAuthRuleResponse(responseData);
return;
}
// Generic response handling
if (typeof logTestEvent === 'function') {
logTestEvent('RECV', `Generic admin response: ${JSON.stringify(responseData)}`, 'RESPONSE');
}
} catch (error) {
console.error('Error handling admin response data:', error);
if (typeof logTestEvent === 'function') {
logTestEvent('ERROR', `Failed to handle response data: ${error.message}`, 'ERROR');
}
}
}
// Handle auth query responses
function handleAuthQueryResponse(responseData) {
console.log('=== AUTH QUERY RESPONSE ===');
console.log('Query type:', responseData.query_type);
console.log('Total results:', responseData.total_results);
console.log('Data:', responseData.data);
if (typeof logTestEvent === 'function') {
logTestEvent('RECV', `Auth query response: ${responseData.query_type}, ${responseData.total_results} results`, 'AUTH_QUERY');
if (responseData.data && responseData.data.length > 0) {
responseData.data.forEach((rule, index) => {
logTestEvent('RECV', `Rule ${index + 1}: ${rule.rule_type} - ${rule.rule_target}`, 'AUTH_RULE');
});
} else {
logTestEvent('RECV', 'No auth rules found', 'AUTH_QUERY');
}
}
}
// Handle system command responses
function handleSystemCommandResponse(responseData) {
console.log('=== SYSTEM COMMAND RESPONSE ===');
console.log('Command:', responseData.command);
console.log('Status:', responseData.status);
if (typeof logTestEvent === 'function') {
logTestEvent('RECV', `System command response: ${responseData.command} - ${responseData.status}`, 'SYSTEM_CMD');
}
}
// Handle auth rule modification responses
function handleAuthRuleResponse(responseData) {
console.log('=== AUTH RULE MODIFICATION RESPONSE ===');
console.log('Operation:', responseData.operation);
console.log('Status:', responseData.status);
if (typeof logTestEvent === 'function') {
logTestEvent('RECV', `Auth rule response: ${responseData.operation} - ${responseData.status}`, 'AUTH_RULE');
if (responseData.processed_rules) {
responseData.processed_rules.forEach((rule, index) => {
logTestEvent('RECV', `Processed rule ${index + 1}: ${rule.rule_type} - ${rule.rule_target}`, 'AUTH_RULE');
});
}
}
}
// Helper function to decrypt content from relay using NIP-44
async function decryptFromRelay(encryptedContent) {
try {
console.log('Decrypting content from relay...');
// Get the relay public key for decryption
const relayPubkey = getRelayPubkey();
// Use NIP-07 extension's NIP-44 decrypt method
if (!window.nostr || !window.nostr.nip44) {
throw new Error('NIP-44 decryption not available via NIP-07 extension');
}
const decryptedContent = await window.nostr.nip44.decrypt(relayPubkey, encryptedContent);
if (!decryptedContent) {
throw new Error('NIP-44 decryption returned empty result');
}
console.log('Successfully decrypted content from relay');
return decryptedContent;
} catch (error) {
console.error('NIP-44 decryption failed:', error);
throw error;
}
}
// Legacy function name for compatibility - now calls subscription
async function fetchConfiguration() {
return await subscribeToConfiguration();
}
function displayConfiguration(event) {
try {
console.log('=== DISPLAYING CONFIGURATION EVENT ===');
console.log('Event received for display:', event);
currentConfig = event;
// Clear existing table
configTableBody.innerHTML = '';
// Display basic event info
const basicInfo = [
['Event ID', event.id],
['Public Key', event.pubkey],
['Created At', new Date(event.created_at * 1000).toISOString()],
['Kind', event.kind],
['Content', event.content]
];
console.log(`Adding ${basicInfo.length} basic info rows`);
basicInfo.forEach(([key, value]) => {
const row = document.createElement('tr');
row.innerHTML = `<td>${key}</td><td>${value}</td><td>-</td>`;
configTableBody.appendChild(row);
});
// Display tags
console.log(`Processing ${event.tags.length} tags`);
event.tags.forEach(tag => {
if (tag.length >= 2) {
const row = document.createElement('tr');
row.innerHTML = `<td>${tag[0]}</td><td>${tag[1]}</td><td>-</td>`;
configTableBody.appendChild(row);
}
});
// Display raw JSON
rawConfigJson.textContent = JSON.stringify(event, null, 2);
console.log('Configuration display completed successfully');
updateConfigStatus(true);
} catch (error) {
console.error('Error in displayConfiguration:', error.message);
console.error('Display configuration error:', error);
}
}
// Configuration editing functions
function generateConfigForm(event) {
if (!event || !event.tags) {
console.log('No configuration event to edit');
return;
}
configForm.innerHTML = '';
// Define field types and validation for different config parameters
const fieldTypes = {
'auth_enabled': 'boolean',
'nip42_auth_required_events': 'boolean',
'nip42_auth_required_subscriptions': 'boolean',
'nip40_expiration_enabled': 'boolean',
'nip40_expiration_strict': 'boolean',
'nip40_expiration_filter': 'boolean',
'relay_port': 'number',
'max_connections': 'number',
'pow_min_difficulty': 'number',
'nip42_challenge_expiration': 'number',
'nip40_expiration_grace_period': 'number',
'max_subscriptions_per_client': 'number',
'max_total_subscriptions': 'number',
'max_filters_per_subscription': 'number',
'max_event_tags': 'number',
'max_content_length': 'number',
'max_message_length': 'number',
'default_limit': 'number',
'max_limit': 'number'
};
const descriptions = {
'relay_pubkey': 'Relay Public Key (Read-only)',
'auth_enabled': 'Enable Authentication',
'nip42_auth_required_events': 'Require Auth for Events',
'nip42_auth_required_subscriptions': 'Require Auth for Subscriptions',
'nip42_auth_required_kinds': 'Auth Required Event Kinds',
'nip42_challenge_expiration': 'Auth Challenge Expiration (seconds)',
'relay_port': 'Relay Port',
'max_connections': 'Maximum Connections',
'relay_description': 'Relay Description',
'relay_contact': 'Relay Contact',
'relay_software': 'Relay Software URL',
'relay_version': 'Relay Version',
'pow_min_difficulty': 'Minimum PoW Difficulty',
'pow_mode': 'PoW Mode',
'nip40_expiration_enabled': 'Enable Event Expiration',
'nip40_expiration_strict': 'Strict Expiration Mode',
'nip40_expiration_filter': 'Filter Expired Events',
'nip40_expiration_grace_period': 'Expiration Grace Period (seconds)',
'max_subscriptions_per_client': 'Max Subscriptions per Client',
'max_total_subscriptions': 'Max Total Subscriptions',
'max_filters_per_subscription': 'Max Filters per Subscription',
'max_event_tags': 'Max Event Tags',
'max_content_length': 'Max Content Length',
'max_message_length': 'Max Message Length',
'default_limit': 'Default Query Limit',
'max_limit': 'Maximum Query Limit'
};
// Process configuration tags (no d tag filtering for ephemeral events)
const configData = {};
event.tags.forEach(tag => {
if (tag.length >= 2) {
configData[tag[0]] = tag[1];
}
});
// Create form fields for each configuration parameter
Object.entries(configData).forEach(([key, value]) => {
const fieldType = fieldTypes[key] || 'text';
const description = descriptions[key] || key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
const fieldGroup = document.createElement('div');
fieldGroup.className = 'input-group';
const label = document.createElement('label');
label.textContent = description;
label.setAttribute('for', `config-${key}`);
let input;
if (fieldType === 'boolean') {
input = document.createElement('select');
input.innerHTML = `
<option value="true" ${value === 'true' ? 'selected' : ''}>true</option>
<option value="false" ${value === 'false' ? 'selected' : ''}>false</option>
`;
} else if (fieldType === 'number') {
input = document.createElement('input');
input.type = 'number';
input.value = value;
input.min = '0';
} else {
input = document.createElement('input');
input.type = 'text';
input.value = value;
}
input.id = `config-${key}`;
input.name = key;
// Make relay_pubkey read-only
if (key === 'relay_pubkey' || key === 'd') {
input.disabled = true;
}
fieldGroup.appendChild(label);
fieldGroup.appendChild(input);
configForm.appendChild(fieldGroup);
});
console.log('Configuration form generated');
}
function enterEditMode() {
if (!currentConfig) {
console.log('No configuration loaded to edit');
return;
}
generateConfigForm(currentConfig);
configViewMode.classList.add('hidden');
configEditMode.classList.remove('hidden');
console.log('Entered edit mode');
}
function exitEditMode() {
configViewMode.classList.remove('hidden');
configEditMode.classList.add('hidden');
configForm.innerHTML = '';
console.log('Exited edit mode');
}
async function saveConfiguration() {
if (!isLoggedIn || !userPubkey) {
console.log('Must be logged in to save configuration');
return;
}
if (!currentConfig) {
console.log('No current configuration to update');
return;
}
try {
console.log('Building new configuration event...');
// Collect form data
const formData = new FormData();
const formInputs = configForm.querySelectorAll('input, select');
const newTags = [];
// Add updated configuration tags (no d tag needed for ephemeral events)
formInputs.forEach(input => {
if (!input.disabled && input.name) {
newTags.push([input.name, input.value]);
}
});
// Create new kind 23455 event (ephemeral configuration event)
const newEvent = {
kind: 23455,
pubkey: userPubkey,
created_at: Math.floor(Date.now() / 1000),
tags: newTags,
content: JSON.stringify({
action: 'update_config',
config_data: Object.fromEntries(newTags.filter(tag => tag[0] !== 'd'))
})
};
console.log('Signing event with window.nostr.signEvent()...');
// Sign the event using the standard NIP-07 interface
const signedEvent = await window.nostr.signEvent(newEvent);
if (!signedEvent || !signedEvent.sig) {
throw new Error('Event signing failed - no signature returned');
}
console.log('Event signed successfully');
console.log('Signed event:', signedEvent);
// Publish the event to the relay
await publishConfigurationEvent(signedEvent);
} catch (error) {
console.log('Configuration save failed: ' + error.message);
console.error('Save configuration error:', error);
}
}
async function publishConfigurationEvent(signedEvent) {
try {
const url = relayUrl.value.trim();
if (!url) {
throw new Error('No relay URL specified');
}
console.log('Publishing configuration event via SimplePool...');
// Use existing pool or create temporary one for publishing
const publishPool = relayPool || new window.NostrTools.SimplePool();
// Publish the event
const publishPromises = publishPool.publish([url], signedEvent);
// Wait for at least one relay to accept the event
const results = await Promise.any(publishPromises);
console.log('Configuration published successfully!');
console.log('The updated configuration should appear automatically via subscription');
// Exit edit mode
exitEditMode();
// Clean up temporary pool if we created one
if (!relayPool) {
publishPool.close([url]);
}
return true;
} catch (error) {
console.log('Configuration publish failed: ' + error.message);
console.error('Publish error:', error);
throw error;
}
}
function updateEventPreview() {
const type = commandType.value;
const payload = commandPayload.value.trim();
if (!type || !userPubkey) {
eventPreview.textContent = 'No event constructed';
return;
}
const event = {
kind: 1,
pubkey: userPubkey,
created_at: Math.floor(Date.now() / 1000),
tags: [
['p', 'RELAY_PUBLIC_KEY'],
['command', type]
],
content: payload || '{}',
id: 'EVENT_ID_PLACEHOLDER',
sig: 'SIGNATURE_PLACEHOLDER'
};
eventPreview.textContent = JSON.stringify(event, null, 2);
}
// Event handlers
logoutBtn.addEventListener('click', logout);
fetchConfigBtn.addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
fetchConfiguration().catch(error => {
console.log('Manual fetch configuration failed: ' + error.message);
});
});
copyConfigBtn.addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
if (currentConfig) {
navigator.clipboard.writeText(JSON.stringify(currentConfig, null, 2))
.then(() => console.log('Configuration copied to clipboard'))
.catch(err => console.log('Failed to copy: ' + err.message));
}
});
editConfigBtn.addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
enterEditMode();
});
saveConfigBtn.addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
saveConfiguration().catch(error => {
console.log('Save configuration failed: ' + error.message);
});
});
cancelEditBtn.addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
exitEditMode();
});
commandType.addEventListener('change', updateEventPreview);
commandPayload.addEventListener('input', updateEventPreview);
sendCommandBtn.addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
const type = commandType.value;
if (!type) {
console.log('Please select a command type');
return;
}
console.log(`Command sending not yet implemented: ${type}`);
});
clearLogBtn.addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
logPanel.innerHTML = '<div class="log-entry"><span class="log-timestamp">SYSTEM:</span> Log cleared.</div>';
});
// ================================
// AUTH RULES MANAGEMENT FUNCTIONS
// ================================
// Global auth rules state
let currentAuthRules = [];
let editingAuthRule = null;
// DOM elements for auth rules
const authRulesSection = document.getElementById('authRulesSection');
const authRulesStatus = document.getElementById('authRulesStatus');
const viewAuthRulesBtn = document.getElementById('viewAuthRulesBtn');
const addAuthRuleBtn = document.getElementById('addAuthRuleBtn');
const refreshAuthRulesBtn = document.getElementById('refreshAuthRulesBtn');
const authRulesTableContainer = document.getElementById('authRulesTableContainer');
const authRulesTableBody = document.getElementById('authRulesTableBody');
const authRuleFormContainer = document.getElementById('authRuleFormContainer');
const authRuleForm = document.getElementById('authRuleForm');
const authRuleFormTitle = document.getElementById('authRuleFormTitle');
const saveAuthRuleBtn = document.getElementById('saveAuthRuleBtn');
const cancelAuthRuleBtn = document.getElementById('cancelAuthRuleBtn');
const authRulesStatusDisplay = document.getElementById('authRulesStatusDisplay');
const authSystemStatus = document.getElementById('authSystemStatus');
const authRulesCount = document.getElementById('authRulesCount');
// Show auth rules section after login
function showAuthRulesSection() {
if (authRulesSection) {
authRulesSection.style.display = 'block';
updateAuthRulesStatus('ready');
log('Auth rules section is now available', 'INFO');
}
}
// Hide auth rules section on logout
function hideAuthRulesSection() {
if (authRulesSection) {
authRulesSection.style.display = 'none';
authRulesTableContainer.style.display = 'none';
authRuleFormContainer.style.display = 'none';
authRulesStatusDisplay.style.display = 'none';
currentAuthRules = [];
editingAuthRule = null;
log('Auth rules section hidden', 'INFO');
}
}
// Update auth rules status indicator
function updateAuthRulesStatus(status) {
if (!authRulesStatus) return;
switch (status) {
case 'ready':
authRulesStatus.textContent = 'READY';
authRulesStatus.className = 'status disconnected';
break;
case 'loading':
authRulesStatus.textContent = 'LOADING';
authRulesStatus.className = 'status connected';
break;
case 'loaded':
authRulesStatus.textContent = 'LOADED';
authRulesStatus.className = 'status connected';
break;
case 'error':
authRulesStatus.textContent = 'ERROR';
authRulesStatus.className = 'status error';
break;
}
}
// Load auth rules from relay (placeholder - will be implemented with WebSocket)
async function loadAuthRules() {
try {
log('Loading auth rules...', 'INFO');
updateAuthRulesStatus('loading');
// TODO: Implement actual auth rules loading via WebSocket/HTTP
// For now, show empty state
currentAuthRules = [];
displayAuthRules(currentAuthRules);
updateAuthRulesStatus('loaded');
log('Auth rules loaded (placeholder implementation)', 'INFO');
} catch (error) {
log(`Failed to load auth rules: ${error.message}`, 'ERROR');
updateAuthRulesStatus('error');
}
}
// Display auth rules in the table
function displayAuthRules(rules) {
if (!authRulesTableBody) return;
authRulesTableBody.innerHTML = '';
if (rules.length === 0) {
const row = document.createElement('tr');
row.innerHTML = `<td colspan="6" style="text-align: center; font-style: italic;">No auth rules configured</td>`;
authRulesTableBody.appendChild(row);
return;
}
rules.forEach((rule, index) => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${rule.rule_type}</td>
<td>${rule.pattern_type || rule.operation || '-'}</td>
<td style="font-family: 'Courier New', monospace; font-size: 12px; word-break: break-all; max-width: 200px;">${rule.pattern_value || rule.rule_target || '-'}</td>
<td>${rule.action || 'allow'}</td>
<td>${rule.enabled !== false ? 'Active' : 'Inactive'}</td>
<td>
<div class="inline-buttons">
<button onclick="editAuthRule(${index})" style="margin: 2px; padding: 4px 8px; font-size: 12px;">EDIT</button>
<button onclick="deleteAuthRule(${index})" style="margin: 2px; padding: 4px 8px; font-size: 12px;">DELETE</button>
</div>
</td>
`;
authRulesTableBody.appendChild(row);
});
// Update status display
if (authRulesCount) {
const activeRules = rules.filter(r => r.enabled !== false).length;
authRulesCount.textContent = `Total Rules: ${rules.length} (${activeRules} active)`;
}
}
// Show auth rules table
function showAuthRulesTable() {
if (authRulesTableContainer) {
authRulesTableContainer.style.display = 'block';
authRulesStatusDisplay.style.display = 'block';
loadAuthRules();
log('Auth rules table displayed', 'INFO');
}
}
// Show add auth rule form
function showAddAuthRuleForm() {
if (authRuleFormContainer && authRuleFormTitle) {
editingAuthRule = null;
authRuleFormTitle.textContent = 'Add Auth Rule';
authRuleForm.reset();
authRuleFormContainer.style.display = 'block';
log('Opened add auth rule form', 'INFO');
}
}
// Show edit auth rule form
function editAuthRule(index) {
if (index < 0 || index >= currentAuthRules.length) return;
const rule = currentAuthRules[index];
editingAuthRule = { ...rule, index: index };
if (authRuleFormTitle && authRuleForm) {
authRuleFormTitle.textContent = 'Edit Auth Rule';
// Populate form fields
document.getElementById('authRuleType').value = rule.rule_type || '';
document.getElementById('authPatternType').value = rule.pattern_type || rule.operation || '';
document.getElementById('authPatternValue').value = rule.pattern_value || rule.rule_target || '';
document.getElementById('authRuleAction').value = rule.action || 'allow';
document.getElementById('authRuleDescription').value = rule.description || '';
authRuleFormContainer.style.display = 'block';
log(`Editing auth rule: ${rule.rule_type} - ${rule.pattern_value || rule.rule_target}`, 'INFO');
}
}
// Delete auth rule
async function deleteAuthRule(index) {
if (index < 0 || index >= currentAuthRules.length) return;
const rule = currentAuthRules[index];
const confirmMsg = `Delete auth rule: ${rule.rule_type} - ${rule.pattern_value || rule.rule_target}?`;
if (!confirm(confirmMsg)) return;
try {
log(`Deleting auth rule: ${rule.rule_type} - ${rule.pattern_value || rule.rule_target}`, 'INFO');
// TODO: Implement actual rule deletion via WebSocket kind 23456 event
// For now, just remove from local array
currentAuthRules.splice(index, 1);
displayAuthRules(currentAuthRules);
log('Auth rule deleted (placeholder implementation)', 'INFO');
} catch (error) {
log(`Failed to delete auth rule: ${error.message}`, 'ERROR');
}
}
// Hide auth rule form
function hideAuthRuleForm() {
if (authRuleFormContainer) {
authRuleFormContainer.style.display = 'none';
editingAuthRule = null;
log('Auth rule form hidden', 'INFO');
}
}
// Validate auth rule form
function validateAuthRuleForm() {
const ruleType = document.getElementById('authRuleType').value;
const patternType = document.getElementById('authPatternType').value;
const patternValue = document.getElementById('authPatternValue').value.trim();
const action = document.getElementById('authRuleAction').value;
if (!ruleType) {
alert('Please select a rule type');
return false;
}
if (!patternType) {
alert('Please select a pattern type');
return false;
}
if (!patternValue) {
alert('Please enter a pattern value');
return false;
}
if (!action) {
alert('Please select an action');
return false;
}
// Validate pubkey format for pubkey rules
if ((ruleType === 'pubkey_whitelist' || ruleType === 'pubkey_blacklist') &&
patternValue.length !== 64) {
alert('Pubkey must be exactly 64 hex characters');
return false;
}
// Validate hex format for pubkey rules
if ((ruleType === 'pubkey_whitelist' || ruleType === 'pubkey_blacklist')) {
const hexPattern = /^[0-9a-fA-F]+$/;
if (!hexPattern.test(patternValue)) {
alert('Pubkey must contain only hex characters (0-9, a-f, A-F)');
return false;
}
}
return true;
}
// Save auth rule (add or update)
async function saveAuthRule(event) {
event.preventDefault();
if (!validateAuthRuleForm()) return;
try {
const ruleData = {
rule_type: document.getElementById('authRuleType').value,
pattern_type: document.getElementById('authPatternType').value,
pattern_value: document.getElementById('authPatternValue').value.trim(),
action: document.getElementById('authRuleAction').value,
description: document.getElementById('authRuleDescription').value.trim() || null,
enabled: true
};
if (editingAuthRule) {
log(`Updating auth rule: ${ruleData.rule_type} - ${ruleData.pattern_value}`, 'INFO');
// TODO: Implement actual rule update via WebSocket kind 23456 event
// For now, just update local array
currentAuthRules[editingAuthRule.index] = { ...ruleData, id: editingAuthRule.id || Date.now() };
log('Auth rule updated (placeholder implementation)', 'INFO');
} else {
log(`Adding new auth rule: ${ruleData.rule_type} - ${ruleData.pattern_value}`, 'INFO');
// TODO: Implement actual rule creation via WebSocket kind 23456 event
// For now, just add to local array
currentAuthRules.push({ ...ruleData, id: Date.now() });
log('Auth rule added (placeholder implementation)', 'INFO');
}
displayAuthRules(currentAuthRules);
hideAuthRuleForm();
} catch (error) {
log(`Failed to save auth rule: ${error.message}`, 'ERROR');
}
}
// Update existing logout and showMainInterface functions to handle auth rules
const originalLogout = logout;
logout = async function() {
hideAuthRulesSection();
await originalLogout();
};
const originalShowMainInterface = showMainInterface;
showMainInterface = function() {
originalShowMainInterface();
showAuthRulesSection();
};
// Auth rules event handlers
if (viewAuthRulesBtn) {
viewAuthRulesBtn.addEventListener('click', function(e) {
e.preventDefault();
showAuthRulesTable();
});
}
if (addAuthRuleBtn) {
addAuthRuleBtn.addEventListener('click', function(e) {
e.preventDefault();
showAddAuthRuleForm();
});
}
if (refreshAuthRulesBtn) {
refreshAuthRulesBtn.addEventListener('click', function(e) {
e.preventDefault();
loadAuthRules();
});
}
if (authRuleForm) {
authRuleForm.addEventListener('submit', saveAuthRule);
}
if (cancelAuthRuleBtn) {
cancelAuthRuleBtn.addEventListener('click', function(e) {
e.preventDefault();
hideAuthRuleForm();
});
}
// ================================
// STREAMLINED AUTH RULE FUNCTIONS
// ================================
// Utility function to convert nsec to hex pubkey
function nsecToHex(input) {
if (!input || input.trim().length === 0) {
return null;
}
const trimmed = input.trim();
// If it's already 64-char hex, return as-is
if (/^[0-9a-fA-F]{64}$/.test(trimmed)) {
return trimmed;
}
// If it starts with nsec1, try to decode
if (trimmed.startsWith('nsec1')) {
try {
if (window.NostrTools && window.NostrTools.nip19 && window.NostrTools.nip19.decode) {
const decoded = window.NostrTools.nip19.decode(trimmed);
if (decoded.type === 'nsec') {
// Convert bytes to hex
const hexPubkey = Array.from(decoded.data)
.map(b => b.toString(16).padStart(2, '0'))
.join('');
return hexPubkey;
}
}
} catch (error) {
console.error('Failed to decode nsec:', error);
return null;
}
}
return null; // Invalid format
}
// Add blacklist rule
function addBlacklistRule() {
const input = document.getElementById('blacklistPubkey');
const statusDiv = document.getElementById('blacklistStatus');
if (!input || !statusDiv) return;
const inputValue = input.value.trim();
if (!inputValue) {
statusDiv.className = 'rule-status error';
statusDiv.textContent = 'Please enter a pubkey or nsec';
return;
}
// Convert nsec to hex if needed
const hexPubkey = nsecToHex(inputValue);
if (!hexPubkey) {
statusDiv.className = 'rule-status error';
statusDiv.textContent = 'Invalid pubkey format. Please enter nsec1... or 64-character hex';
return;
}
// Validate hex length
if (hexPubkey.length !== 64) {
statusDiv.className = 'rule-status error';
statusDiv.textContent = 'Invalid pubkey length. Must be exactly 64 characters';
return;
}
statusDiv.className = 'rule-status';
statusDiv.textContent = 'Adding to blacklist...';
// Create auth rule data
const ruleData = {
rule_type: 'pubkey_blacklist',
pattern_type: 'Global',
pattern_value: hexPubkey,
action: 'deny'
};
// Add to WebSocket queue for processing
addAuthRuleViaWebSocket(ruleData)
.then(() => {
statusDiv.className = 'rule-status success';
statusDiv.textContent = `Pubkey ${hexPubkey.substring(0, 16)}... added to blacklist`;
input.value = '';
// Refresh auth rules display if visible
if (authRulesTableContainer && authRulesTableContainer.style.display !== 'none') {
loadAuthRules();
}
})
.catch(error => {
statusDiv.className = 'rule-status error';
statusDiv.textContent = `Failed to add rule: ${error.message}`;
});
}
// Add whitelist rule
function addWhitelistRule() {
const input = document.getElementById('whitelistPubkey');
const statusDiv = document.getElementById('whitelistStatus');
const warningDiv = document.getElementById('whitelistWarning');
if (!input || !statusDiv) return;
const inputValue = input.value.trim();
if (!inputValue) {
statusDiv.className = 'rule-status error';
statusDiv.textContent = 'Please enter a pubkey or nsec';
return;
}
// Convert nsec to hex if needed
const hexPubkey = nsecToHex(inputValue);
if (!hexPubkey) {
statusDiv.className = 'rule-status error';
statusDiv.textContent = 'Invalid pubkey format. Please enter nsec1... or 64-character hex';
return;
}
// Validate hex length
if (hexPubkey.length !== 64) {
statusDiv.className = 'rule-status error';
statusDiv.textContent = 'Invalid pubkey length. Must be exactly 64 characters';
return;
}
statusDiv.className = 'rule-status';
statusDiv.textContent = 'Adding to whitelist...';
// Create auth rule data
const ruleData = {
rule_type: 'pubkey_whitelist',
pattern_type: 'Global',
pattern_value: hexPubkey,
action: 'allow'
};
// Add to WebSocket queue for processing
addAuthRuleViaWebSocket(ruleData)
.then(() => {
statusDiv.className = 'rule-status success';
statusDiv.textContent = `Pubkey ${hexPubkey.substring(0, 16)}... added to whitelist`;
input.value = '';
// Refresh auth rules display if visible
if (authRulesTableContainer && authRulesTableContainer.style.display !== 'none') {
loadAuthRules();
}
})
.catch(error => {
statusDiv.className = 'rule-status error';
statusDiv.textContent = `Failed to add rule: ${error.message}`;
});
}
// Add hash blacklist rule
function addHashBlacklistRule() {
const input = document.getElementById('hashBlacklist');
const statusDiv = document.getElementById('hashStatus');
if (!input || !statusDiv) return;
const inputValue = input.value.trim();
if (!inputValue) {
statusDiv.className = 'rule-status error';
statusDiv.textContent = 'Please enter a SHA-256 hash';
return;
}
// Validate hash format (64-char hex)
if (!/^[0-9a-fA-F]{64}$/.test(inputValue)) {
statusDiv.className = 'rule-status error';
statusDiv.textContent = 'Invalid hash format. Must be 64-character hex SHA-256 hash';
return;
}
statusDiv.className = 'rule-status';
statusDiv.textContent = 'Adding content hash to blacklist...';
// Create auth rule data
const ruleData = {
rule_type: 'hash_blacklist',
pattern_type: 'Global',
pattern_value: inputValue.toLowerCase(), // Normalize to lowercase
action: 'deny'
};
// Add to WebSocket queue for processing
addAuthRuleViaWebSocket(ruleData)
.then(() => {
statusDiv.className = 'rule-status success';
statusDiv.textContent = `Content hash ${inputValue.substring(0, 16)}... added to blacklist`;
input.value = '';
// Refresh auth rules display if visible
if (authRulesTableContainer && authRulesTableContainer.style.display !== 'none') {
loadAuthRules();
}
})
.catch(error => {
statusDiv.className = 'rule-status error';
statusDiv.textContent = `Failed to add rule: ${error.message}`;
});
}
// Add auth rule via SimplePool (kind 23456 event)
async function addAuthRuleViaWebSocket(ruleData) {
if (!isLoggedIn || !userPubkey) {
throw new Error('Must be logged in to add auth rules');
}
if (!relayPool) {
throw new Error('SimplePool connection not available');
}
try {
log(`Adding auth rule: ${ruleData.rule_type} - ${ruleData.pattern_value.substring(0, 16)}...`, 'INFO');
// Map client-side rule types to database schema values
let dbRuleType, dbPatternType, dbAction;
switch (ruleData.rule_type) {
case 'pubkey_blacklist':
dbRuleType = 'blacklist';
dbPatternType = 'pubkey';
dbAction = 'deny';
break;
case 'pubkey_whitelist':
dbRuleType = 'whitelist';
dbPatternType = 'pubkey';
dbAction = 'allow';
break;
case 'hash_blacklist':
dbRuleType = 'blacklist';
dbPatternType = 'pubkey'; // Schema supports: pubkey, kind, ip, global - using pubkey for hash for now
dbAction = 'deny';
break;
default:
throw new Error(`Unknown rule type: ${ruleData.rule_type}`);
}
// Map pattern type to database schema values
if (ruleData.pattern_type === 'Global') {
dbPatternType = 'global';
} else if (ruleData.pattern_type === 'pubkey') {
dbPatternType = 'pubkey';
}
// Create kind 23456 auth rule event (ephemeral auth management)
const authEvent = {
kind: 23456,
pubkey: userPubkey,
created_at: Math.floor(Date.now() / 1000),
tags: [
[dbRuleType, dbPatternType, ruleData.pattern_value]
],
content: JSON.stringify({
action: 'add',
rule_type: dbRuleType,
pattern_type: dbPatternType,
pattern_value: ruleData.pattern_value,
rule_action: dbAction
})
};
// DEBUG: Log the complete event structure being sent
console.log('=== AUTH RULE EVENT DEBUG ===');
console.log('Original Rule Data:', ruleData);
console.log('Mapped DB Values:', { dbRuleType, dbPatternType, dbAction });
console.log('Auth Event (before signing):', JSON.stringify(authEvent, null, 2));
console.log('Auth Event Tags:', authEvent.tags);
console.log('Auth Event Content:', authEvent.content);
console.log('=== END AUTH RULE EVENT DEBUG ===');
// Sign the event using the standard NIP-07 interface
const signedEvent = await window.nostr.signEvent(authEvent);
if (!signedEvent || !signedEvent.sig) {
throw new Error('Event signing failed');
}
// Publish via SimplePool
const url = relayUrl.value.trim();
const publishPromises = relayPool.publish([url], signedEvent);
// Wait for at least one relay to accept the event
await Promise.any(publishPromises);
log('Auth rule added successfully', 'INFO');
} catch (error) {
log(`Failed to add auth rule: ${error.message}`, 'ERROR');
throw error;
}
}
// ================================
// TEST FUNCTIONS FOR ADMIN API
// ================================
// Test event logging function
function logTestEvent(direction, message, type = 'INFO') {
const testLog = document.getElementById('test-event-log');
if (!testLog) return;
const timestamp = new Date().toISOString().split('T')[1].split('.')[0];
const logEntry = document.createElement('div');
logEntry.className = 'log-entry';
const directionColor = direction === 'SENT' ? '#007bff' : '#28a745';
logEntry.innerHTML = `
<span class="log-timestamp">${timestamp}</span>
<span style="color: ${directionColor}; font-weight: bold;">[${direction}]</span>
<span style="color: #666;">[${type}]</span>
${message}
`;
testLog.appendChild(logEntry);
testLog.scrollTop = testLog.scrollHeight;
}
// Test: Get Auth Rules
async function testGetAuthRules() {
if (!isLoggedIn || !userPubkey) {
logTestEvent('ERROR', 'Must be logged in to test admin API', 'ERROR');
return;
}
if (!relayPool) {
logTestEvent('ERROR', 'SimplePool connection not available', 'ERROR');
return;
}
try {
logTestEvent('INFO', 'Testing Get Auth Rules command...', 'TEST');
// Create command array for getting auth rules
const command_array = '["auth_query", "all"]';
// Encrypt the command content using NIP-44
const encrypted_content = await encryptForRelay(command_array);
if (!encrypted_content) {
throw new Error('Failed to encrypt auth query command');
}
// Create kind 23456 admin event
const authEvent = {
kind: 23456,
pubkey: userPubkey,
created_at: Math.floor(Date.now() / 1000),
tags: [
["p", getRelayPubkey()]
],
content: encrypted_content
};
// Sign the event
const signedEvent = await window.nostr.signEvent(authEvent);
if (!signedEvent || !signedEvent.sig) {
throw new Error('Event signing failed');
}
logTestEvent('SENT', `Get Auth Rules event: ${JSON.stringify(signedEvent)}`, 'EVENT');
// Publish via SimplePool
const url = relayUrl.value.trim();
const publishPromises = relayPool.publish([url], signedEvent);
await Promise.any(publishPromises);
logTestEvent('INFO', 'Get Auth Rules command sent successfully', 'SUCCESS');
} catch (error) {
logTestEvent('ERROR', `Get Auth Rules test failed: ${error.message}`, 'ERROR');
}
}
// Test: Clear All Auth Rules
async function testClearAuthRules() {
if (!isLoggedIn || !userPubkey) {
logTestEvent('ERROR', 'Must be logged in to test admin API', 'ERROR');
return;
}
if (!relayPool) {
logTestEvent('ERROR', 'SimplePool connection not available', 'ERROR');
return;
}
try {
logTestEvent('INFO', 'Testing Clear All Auth Rules command...', 'TEST');
// Create command array for clearing auth rules
const command_array = '["system_command", "clear_all_auth_rules"]';
// Encrypt the command content using NIP-44
const encrypted_content = await encryptForRelay(command_array);
if (!encrypted_content) {
throw new Error('Failed to encrypt clear auth rules command');
}
// Create kind 23456 admin event
const authEvent = {
kind: 23456,
pubkey: userPubkey,
created_at: Math.floor(Date.now() / 1000),
tags: [
["p", getRelayPubkey()]
],
content: encrypted_content
};
// Sign the event
const signedEvent = await window.nostr.signEvent(authEvent);
if (!signedEvent || !signedEvent.sig) {
throw new Error('Event signing failed');
}
logTestEvent('SENT', `Clear Auth Rules event: ${JSON.stringify(signedEvent)}`, 'EVENT');
// Publish via SimplePool
const url = relayUrl.value.trim();
const publishPromises = relayPool.publish([url], signedEvent);
await Promise.any(publishPromises);
logTestEvent('INFO', 'Clear Auth Rules command sent successfully', 'SUCCESS');
} catch (error) {
logTestEvent('ERROR', `Clear Auth Rules test failed: ${error.message}`, 'ERROR');
}
}
// Test: Add Blacklist
async function testAddBlacklist() {
const testPubkeyInput = document.getElementById('test-pubkey-input');
let testPubkey = testPubkeyInput ? testPubkeyInput.value.trim() : '';
// Use a default test pubkey if none provided
if (!testPubkey) {
testPubkey = '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef';
logTestEvent('INFO', `Using default test pubkey: ${testPubkey}`, 'INFO');
}
if (!isLoggedIn || !userPubkey) {
logTestEvent('ERROR', 'Must be logged in to test admin API', 'ERROR');
return;
}
if (!relayPool) {
logTestEvent('ERROR', 'SimplePool connection not available', 'ERROR');
return;
}
try {
logTestEvent('INFO', `Testing Add Blacklist for pubkey: ${testPubkey.substring(0, 16)}...`, 'TEST');
// Create command array for adding blacklist rule
const command_array = `["blacklist", "pubkey", "${testPubkey}"]`;
// Encrypt the command content using NIP-44
const encrypted_content = await encryptForRelay(command_array);
if (!encrypted_content) {
throw new Error('Failed to encrypt blacklist command');
}
// Create kind 23456 admin event
const authEvent = {
kind: 23456,
pubkey: userPubkey,
created_at: Math.floor(Date.now() / 1000),
tags: [
["p", getRelayPubkey()]
],
content: encrypted_content
};
// Sign the event
const signedEvent = await window.nostr.signEvent(authEvent);
if (!signedEvent || !signedEvent.sig) {
throw new Error('Event signing failed');
}
logTestEvent('SENT', `Add Blacklist event: ${JSON.stringify(signedEvent)}`, 'EVENT');
// Publish via SimplePool
const url = relayUrl.value.trim();
const publishPromises = relayPool.publish([url], signedEvent);
await Promise.any(publishPromises);
logTestEvent('INFO', 'Add Blacklist command sent successfully', 'SUCCESS');
} catch (error) {
logTestEvent('ERROR', `Add Blacklist test failed: ${error.message}`, 'ERROR');
}
}
// Test: Add Whitelist
async function testAddWhitelist() {
const testPubkeyInput = document.getElementById('test-pubkey-input');
let testPubkey = testPubkeyInput ? testPubkeyInput.value.trim() : '';
// Use a default test pubkey if none provided
if (!testPubkey) {
testPubkey = 'abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890';
logTestEvent('INFO', `Using default test pubkey: ${testPubkey}`, 'INFO');
}
if (!isLoggedIn || !userPubkey) {
logTestEvent('ERROR', 'Must be logged in to test admin API', 'ERROR');
return;
}
if (!relayPool) {
logTestEvent('ERROR', 'SimplePool connection not available', 'ERROR');
return;
}
try {
logTestEvent('INFO', `Testing Add Whitelist for pubkey: ${testPubkey.substring(0, 16)}...`, 'TEST');
// Create command array for adding whitelist rule
const command_array = `["whitelist", "pubkey", "${testPubkey}"]`;
// Encrypt the command content using NIP-44
const encrypted_content = await encryptForRelay(command_array);
if (!encrypted_content) {
throw new Error('Failed to encrypt whitelist command');
}
// Create kind 23456 admin event
const authEvent = {
kind: 23456,
pubkey: userPubkey,
created_at: Math.floor(Date.now() / 1000),
tags: [
["p", getRelayPubkey()]
],
content: encrypted_content
};
// Sign the event
const signedEvent = await window.nostr.signEvent(authEvent);
if (!signedEvent || !signedEvent.sig) {
throw new Error('Event signing failed');
}
logTestEvent('SENT', `Add Whitelist event: ${JSON.stringify(signedEvent)}`, 'EVENT');
// Publish via SimplePool
const url = relayUrl.value.trim();
const publishPromises = relayPool.publish([url], signedEvent);
await Promise.any(publishPromises);
logTestEvent('INFO', 'Add Whitelist command sent successfully', 'SUCCESS');
} catch (error) {
logTestEvent('ERROR', `Add Whitelist test failed: ${error.message}`, 'ERROR');
}
}
// Test: Post Basic Event
async function testPostEvent() {
if (!isLoggedIn || !userPubkey) {
logTestEvent('ERROR', 'Must be logged in to test event posting', 'ERROR');
return;
}
if (!relayPool) {
logTestEvent('ERROR', 'SimplePool connection not available', 'ERROR');
return;
}
try {
logTestEvent('INFO', 'Testing basic event posting...', 'TEST');
// Create a simple kind 1 text note event
const testEvent = {
kind: 1,
pubkey: userPubkey,
created_at: Math.floor(Date.now() / 1000),
tags: [
["t", "test"],
["client", "c-relay-admin-api"]
],
content: `Test event from C-Relay Admin API at ${new Date().toISOString()}`
};
logTestEvent('SENT', `Test event (before signing): ${JSON.stringify(testEvent)}`, 'EVENT');
// Sign the event using NIP-07
const signedEvent = await window.nostr.signEvent(testEvent);
if (!signedEvent || !signedEvent.sig) {
throw new Error('Event signing failed');
}
logTestEvent('SENT', `Signed test event: ${JSON.stringify(signedEvent)}`, 'EVENT');
// Publish via SimplePool to the same relay
const url = relayUrl.value.trim();
logTestEvent('INFO', `Publishing to relay: ${url}`, 'INFO');
const publishPromises = relayPool.publish([url], signedEvent);
await Promise.any(publishPromises);
logTestEvent('INFO', 'Test event published successfully!', 'SUCCESS');
logTestEvent('INFO', 'Check if the event appears in the subscription above...', 'INFO');
} catch (error) {
logTestEvent('ERROR', `Post Event test failed: ${error.message}`, 'ERROR');
console.error('Post Event test error:', error);
}
}
// Helper function to encrypt content for relay using NIP-44
async function encryptForRelay(content) {
try {
logTestEvent('INFO', `Encrypting content: ${content}`, 'DEBUG');
// Get the relay public key for encryption
const relayPubkey = getRelayPubkey();
// Check if we have access to NIP-44 encryption via nostr-tools
if (!window.NostrTools || !window.NostrTools.nip44) {
throw new Error('NIP-44 encryption not available - nostr-tools library missing');
}
// Get user's private key for encryption
// We need to use the NIP-07 extension to get the private key
if (!window.nostr || !window.nostr.nip44) {
throw new Error('NIP-44 encryption not available via NIP-07 extension');
}
// Use NIP-07 extension's NIP-44 encrypt method
const encrypted_content = await window.nostr.nip44.encrypt(relayPubkey, content);
if (!encrypted_content) {
throw new Error('NIP-44 encryption returned empty result');
}
logTestEvent('INFO', `Successfully encrypted content using NIP-44`, 'DEBUG');
logTestEvent('INFO', `Encrypted content: ${encrypted_content.substring(0, 50)}...`, 'DEBUG');
return encrypted_content;
} catch (error) {
logTestEvent('ERROR', `NIP-44 encryption failed: ${error.message}`, 'ERROR');
// Fallback: Try using nostr-tools directly if NIP-07 fails
try {
logTestEvent('INFO', 'Attempting fallback encryption with nostr-tools...', 'DEBUG');
if (!window.NostrTools || !window.NostrTools.nip44) {
throw new Error('nostr-tools NIP-44 not available');
}
// We need the user's private key, but we can't get it directly
// This is a security limitation - we should use NIP-07
throw new Error('Cannot access private key for direct encryption - use NIP-07 extension');
} catch (fallbackError) {
logTestEvent('ERROR', `Fallback encryption failed: ${fallbackError.message}`, 'ERROR');
return null;
}
}
}
// Helper function to get relay pubkey
function getRelayPubkey() {
// This should be extracted from the relay configuration
// For now, return a placeholder
return '4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa';
}
// Enhanced SimplePool message handler to capture test responses
function enhancePoolForTesting() {
// SimplePool handles message parsing automatically, so we just need to
// ensure our event handlers log appropriately. This is already done
// in the subscription onevent callback.
console.log('SimplePool enhanced for testing - automatic message handling enabled');
}
// Generate random test pubkey function
function generateRandomTestKey() {
// Generate 32 random bytes (64 hex characters) for a valid pubkey
const randomBytes = new Uint8Array(32);
crypto.getRandomValues(randomBytes);
// Convert to hex string
const hexPubkey = Array.from(randomBytes)
.map(b => b.toString(16).padStart(2, '0'))
.join('');
// Set the generated key in the input field
const testPubkeyInput = document.getElementById('test-pubkey-input');
if (testPubkeyInput) {
testPubkeyInput.value = hexPubkey;
logTestEvent('INFO', `Generated random test pubkey: ${hexPubkey.substring(0, 16)}...`, 'KEYGEN');
}
return hexPubkey;
}
// Event handlers for test buttons
document.addEventListener('DOMContentLoaded', () => {
// Test button event handlers
const testGetAuthRulesBtn = document.getElementById('test-get-auth-rules-btn');
const testClearAuthRulesBtn = document.getElementById('test-clear-auth-rules-btn');
const testAddBlacklistBtn = document.getElementById('test-add-blacklist-btn');
const testAddWhitelistBtn = document.getElementById('test-add-whitelist-btn');
const testPostEventBtn = document.getElementById('test-post-event-btn');
const clearTestLogBtn = document.getElementById('clear-test-log-btn');
const generateTestKeyBtn = document.getElementById('generate-test-key-btn');
if (testGetAuthRulesBtn) {
testGetAuthRulesBtn.addEventListener('click', testGetAuthRules);
}
if (testClearAuthRulesBtn) {
testClearAuthRulesBtn.addEventListener('click', testClearAuthRules);
}
if (testAddBlacklistBtn) {
testAddBlacklistBtn.addEventListener('click', testAddBlacklist);
}
if (testAddWhitelistBtn) {
testAddWhitelistBtn.addEventListener('click', testAddWhitelist);
}
if (testPostEventBtn) {
testPostEventBtn.addEventListener('click', testPostEvent);
}
if (clearTestLogBtn) {
clearTestLogBtn.addEventListener('click', () => {
const testLog = document.getElementById('test-event-log');
if (testLog) {
testLog.innerHTML = '<div class="log-entry"><span class="log-timestamp">SYSTEM:</span> Test log cleared.</div>';
}
});
}
if (generateTestKeyBtn) {
generateTestKeyBtn.addEventListener('click', generateRandomTestKey);
}
// Show test input section when needed
const testInputSection = document.getElementById('test-input-section');
if (testInputSection) {
testInputSection.style.display = 'block';
}
});
// Initialize the app
document.addEventListener('DOMContentLoaded', () => {
console.log('C-Relay Admin API interface loaded');
setTimeout(() => {
initializeApp();
// Enhance SimplePool for testing after initialization
setTimeout(enhancePoolForTesting, 2000);
}, 100);
});
</script>
</body>
</html>