Files
c-relay-pg/plans/wot_web_ui_plan.md

13 KiB

Web of Trust — Admin Web UI Plan

Overview

Add a Web of Trust section to the existing Authorization page in the admin web interface. The section lets the admin see whether their kind 3 contact list is on the relay, select a WoT level, trigger a sync, and view the current whitelist count.

Architecture

Data Flow

flowchart LR
    A[Web UI] -->|config_set wot_enabled N| B[Kind 23456 Event]
    A -->|system_command wot_sync| B
    A -->|system_command wot_status| B
    B --> C[Relay Backend]
    C -->|Kind 23457 Response| D[Web UI updates display]

How It Works

  1. On page load — the Authorization page already calls loadAuthRules. We add a parallel call to loadWotStatus which sends a system_command / wot_status event.
  2. Backend responds with a kind 23457 JSON containing: wot_enabled level, wot_whitelist_count, and admin_kind3_exists boolean.
  3. UI renders a card showing:
    • Whether the admin's kind 3 event exists on the relay
    • Current WoT level as a 3-option radio/button group
    • Count of whitelisted pubkeys
    • A SYNC button to force resync
  4. Changing level sends config_set / wot_enabled / 0|1|2 via the existing admin API, then triggers wot_sync if level > 0.
  5. SYNC button sends system_command / wot_sync.

Detailed Changes

1. Backend: New system commands in src/config.c

Add two new branches to handle_system_command_unified:

wot_status command

Returns JSON response:

{
  "command": "wot_status",
  "status": "success",
  "wot_enabled": 2,
  "admin_kind3_exists": true,
  "wot_whitelist_count": 347,
  "timestamp": 1234567890
}

Implementation:

  • Read wot_enabled from config table
  • Query SELECT COUNT(*) FROM events WHERE kind = 3 AND pubkey = ? with admin_pubkey to check kind 3 existence
  • Query SELECT COUNT(*) FROM auth_rules WHERE rule_type = 'wot_whitelist' AND active = 1 for whitelist count
  • Return as kind 23457 signed response

wot_sync command

Calls wot_sync_from_admin_kind3 and returns result:

{
  "command": "wot_sync",
  "status": "success",
  "wot_whitelist_count": 347,
  "timestamp": 1234567890
}

2. Backend: Hook config_set for wot_enabled

In handle_config_set_unified, after the config value is updated, add a check:

// After successful update, trigger WoT sync if wot_enabled changed
if (strcmp(config_key, "wot_enabled") == 0) {
    int new_level = atoi(config_value);
    if (new_level > 0) {
        wot_sync_from_admin_kind3();
    } else {
        // Level 0: clear wot_whitelist rules and reset auth flags
        sqlite3_exec(g_db, "DELETE FROM auth_rules WHERE rule_type = 'wot_whitelist'", NULL, NULL, NULL);
        update_config_in_table("nip42_auth_required_subscriptions", "false");
    }
}

3. Frontend: HTML — WoT section in api/index.html

Add a new div inside the authRulesSection, before the auth rules table. This keeps WoT visually grouped with authorization:

<!-- Web of Trust Section -->
<div id="wotSection" class="input-group">
    <div class="section-header" style="font-size: 14px; margin-bottom: 10px;">
        WEB OF TRUST
    </div>
    
    <!-- Kind 3 Status Indicator -->
    <div id="wotKind3Status" class="wot-status-row">
        <span>Admin Contact List (kind 3):</span>
        <span id="wotKind3Indicator" class="wot-indicator wot-unknown">Checking...</span>
    </div>
    
    <!-- WoT Level Selector -->
    <div class="wot-level-selector">
        <label>WoT Level:</label>
        <div class="inline-buttons">
            <button type="button" id="wotLevel0Btn" class="wot-level-btn" onclick="setWotLevel(0)">
                OFF
            </button>
            <button type="button" id="wotLevel1Btn" class="wot-level-btn" onclick="setWotLevel(1)">
                WRITE ONLY
            </button>
            <button type="button" id="wotLevel2Btn" class="wot-level-btn" onclick="setWotLevel(2)">
                FULL
            </button>
        </div>
        <div class="wot-level-description" id="wotLevelDescription">
            Level 0: Open relay — anyone can read and write
        </div>
    </div>
    
    <!-- WoT Stats -->
    <div class="wot-stats-row">
        <span>Whitelisted Pubkeys:</span>
        <span id="wotWhitelistCount"></span>
    </div>
    
    <!-- Sync Button -->
    <div class="inline-buttons">
        <button type="button" id="wotSyncBtn" onclick="syncWot()">SYNC FROM KIND 3</button>
        <button type="button" id="wotRefreshBtn" onclick="loadWotStatus()">REFRESH STATUS</button>
    </div>
</div>

<hr style="margin: 15px 0; border-color: var(--border-color);">

4. Frontend: CSS additions in api/index.css

/* Web of Trust styles */
.wot-status-row, .wot-stats-row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 8px 0;
    font-size: 14px;
}

.wot-indicator {
    padding: 2px 10px;
    border-radius: 4px;
    font-weight: bold;
    font-size: 12px;
}

.wot-indicator.wot-found { background: #28a745; color: white; }
.wot-indicator.wot-missing { background: #dc3545; color: white; }
.wot-indicator.wot-unknown { background: #6c757d; color: white; }

.wot-level-selector { padding: 10px 0; }
.wot-level-selector label { display: block; margin-bottom: 5px; font-weight: bold; }

.wot-level-btn { min-width: 100px; }
.wot-level-btn.active {
    background: var(--accent-color, #007bff);
    color: white;
    border-color: var(--accent-color, #007bff);
}

.wot-level-description {
    font-size: 12px;
    color: var(--text-secondary);
    margin-top: 5px;
    font-style: italic;
}

5. Frontend: JavaScript functions in api/index.js

loadWotStatus — query current WoT state

async function loadWotStatus() {
    try {
        if (!isLoggedIn || !userPubkey || !relayPool) return;
        
        const command_array = ["system_command", "wot_status"];
        const encrypted_content = await encryptForRelay(JSON.stringify(command_array));
        if (!encrypted_content) return;
        
        const event = {
            kind: 23456,
            pubkey: userPubkey,
            created_at: Math.floor(Date.now() / 1000),
            tags: [["p", getRelayPubkey()]],
            content: encrypted_content
        };
        
        const signedEvent = await window.nostr.signEvent(event);
        const url = relayConnectionUrl.value.trim();
        await relayPool.publish([url], signedEvent);
        
        log('WoT status query sent', 'INFO');
    } catch (error) {
        log('Failed to load WoT status: ' + error.message, 'ERROR');
    }
}

setWotLevel — change WoT level via config_set

async function setWotLevel(level) {
    try {
        if (!isLoggedIn || !userPubkey || !relayPool) return;
        
        // Send config_set for wot_enabled
        const command_array = ["config_set", "wot_enabled", String(level)];
        const encrypted_content = await encryptForRelay(JSON.stringify(command_array));
        if (!encrypted_content) return;
        
        const event = {
            kind: 23456,
            pubkey: userPubkey,
            created_at: Math.floor(Date.now() / 1000),
            tags: [["p", getRelayPubkey()]],
            content: encrypted_content
        };
        
        const signedEvent = await window.nostr.signEvent(event);
        const url = relayConnectionUrl.value.trim();
        await relayPool.publish([url], signedEvent);
        
        log('WoT level set to ' + level, 'INFO');
        
        // Refresh status after a short delay
        setTimeout(() => loadWotStatus(), 1500);
    } catch (error) {
        log('Failed to set WoT level: ' + error.message, 'ERROR');
    }
}

syncWot — force WoT sync

async function syncWot() {
    try {
        if (!isLoggedIn || !userPubkey || !relayPool) return;
        
        const command_array = ["system_command", "wot_sync"];
        const encrypted_content = await encryptForRelay(JSON.stringify(command_array));
        if (!encrypted_content) return;
        
        const event = {
            kind: 23456,
            pubkey: userPubkey,
            created_at: Math.floor(Date.now() / 1000),
            tags: [["p", getRelayPubkey()]],
            content: encrypted_content
        };
        
        const signedEvent = await window.nostr.signEvent(event);
        const url = relayConnectionUrl.value.trim();
        await relayPool.publish([url], signedEvent);
        
        log('WoT sync command sent', 'INFO');
        
        // Refresh status after sync completes
        setTimeout(() => loadWotStatus(), 2000);
    } catch (error) {
        log('Failed to sync WoT: ' + error.message, 'ERROR');
    }
}

handleWotStatusResponse — process backend response

function handleWotStatusResponse(responseData) {
    const kind3Indicator = document.getElementById('wotKind3Indicator');
    const whitelistCount = document.getElementById('wotWhitelistCount');
    const levelDesc = document.getElementById('wotLevelDescription');
    
    // Update kind 3 indicator
    if (kind3Indicator) {
        if (responseData.admin_kind3_exists) {
            kind3Indicator.textContent = 'Found ✓';
            kind3Indicator.className = 'wot-indicator wot-found';
        } else {
            kind3Indicator.textContent = 'Not Found ✗';
            kind3Indicator.className = 'wot-indicator wot-missing';
        }
    }
    
    // Update whitelist count
    if (whitelistCount) {
        whitelistCount.textContent = responseData.wot_whitelist_count || 0;
    }
    
    // Update level buttons
    const level = responseData.wot_enabled || 0;
    ['wotLevel0Btn', 'wotLevel1Btn', 'wotLevel2Btn'].forEach((id, i) => {
        const btn = document.getElementById(id);
        if (btn) {
            btn.classList.toggle('active', i === level);
        }
    });
    
    // Update description
    const descriptions = [
        'Level 0: Open relay — anyone can read and write',
        'Level 1: Write-only — only followed pubkeys can publish events',
        'Level 2: Full — only followed pubkeys can publish AND subscribe (NIP-42 required)'
    ];
    if (levelDesc) {
        levelDesc.textContent = descriptions[level] || descriptions[0];
    }
    
    // Enable/disable sync button based on kind 3 existence
    const syncBtn = document.getElementById('wotSyncBtn');
    if (syncBtn) {
        syncBtn.disabled = !responseData.admin_kind3_exists;
    }
}

Wire into existing response handler

In the existing handleSystemCommandResponse function, add:

if (responseData.command === 'wot_status' || responseData.command === 'wot_sync') {
    handleWotStatusResponse(responseData);
}

Wire into page navigation

In the authorization case of the page switch handler, add:

case 'authorization':
    loadAuthRules().catch(...);
    loadWotStatus().catch(error => {
        console.log('Auto-load WoT status failed: ' + error.message);
    });
    break;

Files to Modify

File Changes
src/config.c Add wot_status and wot_sync branches to handle_system_command_unified; add WoT sync hook to handle_config_set_unified
api/index.html Add WoT section HTML inside authRulesSection
api/index.css Add WoT-specific CSS styles
api/index.js Add loadWotStatus, setWotLevel, syncWot, handleWotStatusResponse functions; wire into page nav and response handler

Edge Cases

  1. Admin not logged in — WoT section shows but buttons are disabled; status shows "Checking..."
  2. No kind 3 event — Indicator shows red "Not Found ✗"; SYNC button is disabled; level selector still works but sync will whitelist only admin + relay
  3. Level change from 2 to 0 — Backend clears all wot_whitelist rules and resets nip42_auth_required_subscriptions
  4. Concurrent config_set and wot_sync — The backend handles these sequentially via SQLite transactions; no race condition

UI Mockup

┌─────────────────────────────────────────────┐
│ WEB OF TRUST                                │
│                                             │
│ Admin Contact List (kind 3):  [Found ✓]     │
│                                             │
│ WoT Level:                                  │
│ [  OFF  ] [ WRITE ONLY ] [  FULL  ]         │
│ Level 2: Full — only followed pubkeys can   │
│ publish AND subscribe (NIP-42 required)     │
│                                             │
│ Whitelisted Pubkeys: 347                    │
│                                             │
│ [ SYNC FROM KIND 3 ] [ REFRESH STATUS ]     │
├─────────────────────────────────────────────┤
│ ─────────────────────────────────────────── │
│ AUTH RULES MANAGEMENT                       │
│ ... existing auth rules table ...           │
└─────────────────────────────────────────────┘