diff --git a/api/index.html b/api/index.html
index 1463260..1e0095d 100644
--- a/api/index.html
+++ b/api/index.html
@@ -192,6 +192,92 @@
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;
@@ -270,6 +356,96 @@
+
+
@@ -334,17 +510,14 @@
+
-
+
+
+
-
-
-
-
-
-
-
-
+
+
@@ -420,6 +593,110 @@
}
}
+ // 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 {
@@ -428,6 +705,7 @@
methods: {
extension: true,
local: true,
+ seedphrase: true,
readonly: true,
connect: true,
remote: true,
@@ -438,8 +716,8 @@
hPosition: 1, // 0.0-1.0 or '95%' from left
vPosition: 0, // 0.0-1.0 or '50%' from top
appearance: {
- style: 'pill', // 'pill', 'square', 'circle', 'minimal'
- icon: '', // Clean display without icon placeholders
+ style: 'square', // 'pill', 'square', 'circle', 'minimal'
+ // icon: '[LOGIN]', // Now uses text-based icons like [LOGIN], [KEY], [NET]
text: 'Login'
},
behavior: {
@@ -447,16 +725,24 @@
showUserInfo: true,
autoSlide: true
},
- getUserInfo: true, // Enable profile fetching
- getUserRelay: [ // Specific relays for profile fetching
- 'wss://relay.laantungir.net'
- ]
+ 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);
@@ -1189,6 +1475,648 @@
logPanel.innerHTML = '
SYSTEM: Log cleared.
';
});
+ // ================================
+ // 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 = `
No auth rules configured `;
+ authRulesTableBody.appendChild(row);
+ return;
+ }
+
+ rules.forEach((rule, index) => {
+ const row = document.createElement('tr');
+ row.innerHTML = `
+
${rule.rule_type}
+
${rule.pattern_type || rule.operation || '-'}
+
${rule.pattern_value || rule.rule_target || '-'}
+
${rule.action || 'allow'}
+
${rule.enabled !== false ? 'Active' : 'Inactive'}
+
+
+ EDIT
+ DELETE
+
+
+ `;
+ 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 33335 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 33335 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 33335 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;
+ }
+
+ // Show warning about whitelist-only mode
+ if (warningDiv) {
+ warningDiv.style.display = 'block';
+ }
+
+ statusDiv.className = 'rule-status warning';
+ statusDiv.textContent = 'Adding to whitelist (will enable whitelist-only mode)...';
+
+ // 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 WebSocket (kind 33335 event)
+ async function addAuthRuleViaWebSocket(ruleData) {
+ if (!isLoggedIn || !userPubkey) {
+ throw new Error('Must be logged in to add auth rules');
+ }
+
+ if (!configWebSocket || configWebSocket.readyState !== WebSocket.OPEN) {
+ throw new Error('WebSocket connection not available');
+ }
+
+ try {
+ log(`Adding auth rule: ${ruleData.rule_type} - ${ruleData.pattern_value.substring(0, 16)}...`, 'INFO');
+
+ // Create kind 33335 auth rule event
+ const authEvent = {
+ kind: 33335,
+ pubkey: userPubkey,
+ created_at: Math.floor(Date.now() / 1000),
+ tags: [
+ ['d', 'auth-rules'], // Addressable event identifier
+ [ruleData.rule_type, ruleData.pattern_type, ruleData.pattern_value]
+ ],
+ content: JSON.stringify({
+ action: 'add',
+ rule_type: ruleData.rule_type,
+ pattern_type: ruleData.pattern_type,
+ pattern_value: ruleData.pattern_value,
+ rule_action: ruleData.action
+ })
+ };
+
+ // DEBUG: Log the complete event structure being sent
+ console.log('=== AUTH RULE EVENT DEBUG ===');
+ console.log('Rule Data:', ruleData);
+ 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
+ const signedEvent = await window.nostr.signEvent(authEvent);
+ if (!signedEvent || !signedEvent.sig) {
+ throw new Error('Event signing failed');
+ }
+
+ // Send EVENT message via existing WebSocket
+ return new Promise((resolve, reject) => {
+ const eventMessage = ["EVENT", signedEvent];
+
+ // Set up temporary message handler for OK response
+ const originalOnMessage = configWebSocket.onmessage;
+ let timeoutId = setTimeout(() => {
+ configWebSocket.onmessage = originalOnMessage;
+ reject(new Error('Timeout waiting for auth rule response'));
+ }, 10000);
+
+ configWebSocket.onmessage = function(event) {
+ try {
+ const message = JSON.parse(event.data);
+ if (Array.isArray(message) && message[0] === "OK" && message[1] === signedEvent.id) {
+ clearTimeout(timeoutId);
+ configWebSocket.onmessage = originalOnMessage;
+
+ if (message[2]) {
+ resolve();
+ log('Auth rule added successfully', 'INFO');
+ } else {
+ reject(new Error(message[3] || 'Auth rule rejected by relay'));
+ }
+ return;
+ }
+ } catch (parseError) {
+ // Ignore parse errors, pass to original handler
+ }
+
+ // Call original handler for other messages
+ if (originalOnMessage) {
+ originalOnMessage.call(this, event);
+ }
+ };
+
+ // Send the event
+ configWebSocket.send(JSON.stringify(eventMessage));
+ log('Auth rule event sent via WebSocket', 'INFO');
+ });
+
+ } catch (error) {
+ log(`Failed to add auth rule: ${error.message}`, 'ERROR');
+ throw error;
+ }
+ }
+
// Initialize the app
document.addEventListener('DOMContentLoaded', () => {
console.log('C-Relay Admin API interface loaded');
diff --git a/relay.pid b/relay.pid
index 1ccf48a..2dadd49 100644
--- a/relay.pid
+++ b/relay.pid
@@ -1 +1 @@
-295261
+1307796
diff --git a/src/config.c b/src/config.c
index 8d385d5..c55c253 100644
--- a/src/config.c
+++ b/src/config.c
@@ -2156,15 +2156,28 @@ int process_admin_config_event(cJSON* event, char* error_message, size_t error_s
// Handle kind 33335 auth rule events
int process_admin_auth_event(cJSON* event, char* error_message, size_t error_size) {
+ log_info("=== SERVER-SIDE AUTH RULE EVENT DEBUG ===");
+
+ // Print the entire received event for debugging
+ char* debug_event_str = cJSON_Print(event);
+ if (debug_event_str) {
+ printf("Received Auth Event JSON: %s\n", debug_event_str);
+ free(debug_event_str);
+ }
+
cJSON* tags_obj = cJSON_GetObjectItem(event, "tags");
if (!tags_obj || !cJSON_IsArray(tags_obj)) {
+ log_error("Auth event missing or invalid tags array");
snprintf(error_message, error_size, "invalid: auth rule event must have tags");
return -1;
}
+ printf("Tags array size: %d\n", cJSON_GetArraySize(tags_obj));
+
// Extract action from content or tags
cJSON* content_obj = cJSON_GetObjectItem(event, "content");
const char* content = content_obj ? cJSON_GetStringValue(content_obj) : "";
+ printf("Event content: '%s'\n", content);
// Parse the action from content (should be "add" or "remove")
cJSON* content_json = cJSON_Parse(content);
@@ -2176,6 +2189,7 @@ int process_admin_auth_event(cJSON* event, char* error_message, size_t error_siz
}
cJSON_Delete(content_json);
}
+ printf("Parsed action: '%s'\n", action);
// Begin transaction for atomic auth rule updates
int rc = sqlite3_exec(g_db, "BEGIN IMMEDIATE TRANSACTION", NULL, NULL, NULL);
@@ -2185,11 +2199,33 @@ int process_admin_auth_event(cJSON* event, char* error_message, size_t error_siz
}
int rules_processed = 0;
+ int tags_examined = 0;
+ int tags_skipped = 0;
// Process each tag as an auth rule specification
cJSON* tag = NULL;
cJSON_ArrayForEach(tag, tags_obj) {
- if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 3) {
+ tags_examined++;
+
+ printf("Examining tag #%d:\n", tags_examined);
+ char* tag_debug_str = cJSON_Print(tag);
+ if (tag_debug_str) {
+ printf(" Tag JSON: %s\n", tag_debug_str);
+ free(tag_debug_str);
+ }
+
+ if (!cJSON_IsArray(tag)) {
+ printf(" SKIPPED: Not an array\n");
+ tags_skipped++;
+ continue;
+ }
+
+ int tag_size = cJSON_GetArraySize(tag);
+ printf(" Tag array size: %d\n", tag_size);
+
+ if (tag_size < 3) {
+ printf(" SKIPPED: Array size < 3 (need at least 3 elements for auth rules)\n");
+ tags_skipped++;
continue;
}
@@ -2200,6 +2236,8 @@ int process_admin_auth_event(cJSON* event, char* error_message, size_t error_siz
if (!cJSON_IsString(rule_type_obj) ||
!cJSON_IsString(pattern_type_obj) ||
!cJSON_IsString(pattern_value_obj)) {
+ printf(" SKIPPED: One or more elements are not strings\n");
+ tags_skipped++;
continue;
}
@@ -2207,18 +2245,42 @@ int process_admin_auth_event(cJSON* event, char* error_message, size_t error_siz
const char* pattern_type = cJSON_GetStringValue(pattern_type_obj);
const char* pattern_value = cJSON_GetStringValue(pattern_value_obj);
+ printf(" Extracted rule: type='%s', pattern_type='%s', pattern_value='%s'\n",
+ rule_type, pattern_type, pattern_value);
+
+ // Map rule_type to correct action (FIX THE BUG HERE)
+ const char* mapped_action = "allow"; // default
+ if (strcmp(rule_type, "pubkey_blacklist") == 0 || strcmp(rule_type, "hash_blacklist") == 0) {
+ mapped_action = "deny";
+ } else if (strcmp(rule_type, "pubkey_whitelist") == 0) {
+ mapped_action = "allow";
+ }
+ printf(" Mapped action for rule_type '%s': '%s'\n", rule_type, mapped_action);
+
// Process the auth rule based on action
if (strcmp(action, "add") == 0) {
- if (add_auth_rule_from_config(rule_type, pattern_type, pattern_value, "allow") == 0) {
+ printf(" Attempting to add rule to database...\n");
+ if (add_auth_rule_from_config(rule_type, pattern_type, pattern_value, mapped_action) == 0) {
+ printf(" SUCCESS: Rule added to database\n");
rules_processed++;
+ } else {
+ printf(" FAILED: Could not add rule to database\n");
}
} else if (strcmp(action, "remove") == 0) {
+ printf(" Attempting to remove rule from database...\n");
if (remove_auth_rule_from_config(rule_type, pattern_type, pattern_value) == 0) {
+ printf(" SUCCESS: Rule removed from database\n");
rules_processed++;
+ } else {
+ printf(" FAILED: Could not remove rule from database\n");
}
}
}
+ printf("Processing summary: examined=%d, skipped=%d, processed=%d\n",
+ tags_examined, tags_skipped, rules_processed);
+ log_info("=== END SERVER-SIDE AUTH RULE EVENT DEBUG ===");
+
if (rules_processed > 0) {
sqlite3_exec(g_db, "COMMIT", NULL, NULL, NULL);