v0.3.11 - Working on admin api

This commit is contained in:
Your Name
2025-09-25 11:25:50 -04:00
parent be99595bde
commit 036b0823b9
9 changed files with 1635 additions and 201 deletions

325
README.md
View File

@@ -22,4 +22,329 @@ Do NOT modify the formatting, add emojis, or change the text. Keep the simple fo
- [ ] NIP-50: Keywords filter - [ ] NIP-50: Keywords filter
- [ ] NIP-70: Protected Events - [ ] NIP-70: Protected Events
## 🔧 Administrator API
C-Relay uses an innovative **event-based administration system** where all configuration and management commands are sent as signed Nostr events using the admin private key generated during first startup. Admin commands use ephemeral kinds 23455 and 23456 with **optional NIP-44 encryption** for enhanced security.
### Authentication
All admin commands require signing with the admin private key displayed during first-time startup. **Save this key securely** - it cannot be recovered and is needed for all administrative operations.
### Security Options
**Standard Mode (Plaintext):** Commands sent in tags as normal
**Encrypted Mode (NIP-44):** Commands encrypted in content field, no tags used
### Kind 23455: Configuration Management (Ephemeral)
Update relay configuration parameters or query available settings.
**Configuration Update:**
```json
{
"kind": 23455,
"content": "<optional nip-44 encrypted tags>",
"tags": [
["config_key1", "config_value1"],
["config_key2", "config_value2"]
]
}
```
**Query Available Config Keys:**
```json
{
"kind": 23455,
"content": "<optional nip-44 encrypted tags>",
"tags": [
["config_query", "list_all_keys"]
]
}
```
**Get Current Configuration:**
```json
{
"kind": 23455,
"content": "<optional nip-44 encrypted tags>",
"tags": [
["config_query", "get_current_config"]
]
}
```
**Available Configuration Keys:**
**Basic Relay Settings:**
- `relay_description`: Relay description text
- `relay_contact`: Contact information
- `max_connections`: Maximum concurrent connections
- `max_subscriptions_per_client`: Max subscriptions per client
- `max_event_tags`: Maximum tags per event
- `max_content_length`: Maximum event content length
**Authentication & Access Control:**
- `auth_enabled`: Enable whitelist/blacklist auth rules (`true`/`false`)
- `nip42_auth_required`: Enable NIP-42 cryptographic authentication (`true`/`false`)
- `nip42_auth_required_kinds`: Event kinds requiring NIP-42 auth (comma-separated)
- `nip42_challenge_timeout`: NIP-42 challenge expiration seconds
**Proof of Work & Validation:**
- `pow_min_difficulty`: Minimum proof-of-work difficulty
- `nip40_expiration_enabled`: Enable event expiration (`true`/`false`)
### Kind 23456: Auth Rules & System Management (Ephemeral)
Manage whitelist/blacklist rules and system administration commands.
**Add Blacklist Rule:**
```json
{
"kind": 23456,
"content": "<optional nip-44 encrypted tags>",
"tags": [
["blacklist", "pubkey", "deadbeef1234abcd..."]
]
}
```
**Add Whitelist Rule:**
```json
{
"kind": 23456,
"content": "<optional nip-44 encrypted tags>",
"tags": [
["whitelist", "pubkey", "cafebabe5678efgh..."]
]
}
```
**Remove Rule:**
```json
{
"kind": 23456,
"content": "<optional nip-44 encrypted tags>",
"tags": [
["blacklist", "pubkey", "deadbeef1234abcd..."]
]
}
```
**Query Auth Rules:**
```json
{
"kind": 23456,
"content": "<optional nip-44 encrypted tags>",
"tags": [
["auth_query", "all"]
]
}
```
**Query Specific Rule Type:**
```json
{
"kind": 23456,
"content": "<optional nip-44 encrypted tags>",
"tags": [
["auth_query", "whitelist"]
]
}
```
**Query Specific Pattern:**
```json
{
"kind": 23456,
"content": "<optional nip-44 encrypted tags>",
"tags": [
["auth_query", "pattern", "deadbeef1234abcd..."]
]
}
```
**Clear All Auth Rules:**
```json
{
"kind": 23456,
"content": "<optional nip-44 encrypted tags>",
"tags": [
["system_command", "clear_all_auth_rules"]
]
}
```
### Response Format
All admin commands return JSON responses via WebSocket:
**Success Response:**
```json
["OK", "event_id", true, "Operation completed successfully"]
```
**Error Response:**
```json
["OK", "event_id", false, "Error: invalid configuration value"]
```
### Command Examples
**Using `nak` CLI tool:**
```bash
# Set environment variables
ADMIN_PRIVKEY="your_admin_private_key_here"
RELAY_PUBKEY="your_relay_public_key_here"
RELAY_URL="ws://localhost:8888"
# List all available configuration keys
nak event -k 23455 --content "Discovery query" \
-t "config_query=list_all_keys" \
--sec $ADMIN_PRIVKEY | nak event $RELAY_URL
# Enable whitelist/blacklist auth rules
nak event -k 23455 --content "Enable auth rules" \
-t "auth_enabled=true" \
--sec $ADMIN_PRIVKEY | nak event $RELAY_URL
# Add user to blacklist
nak event -k 23456 --content "Block spam user" \
-t "blacklist=pubkey;$SPAM_USER_PUBKEY" \
--sec $ADMIN_PRIVKEY | nak event $RELAY_URL
# Add user to whitelist
nak event -k 23456 --content "Allow trusted user" \
-t "whitelist=pubkey;$TRUSTED_USER_PUBKEY" \
--sec $ADMIN_PRIVKEY | nak event $RELAY_URL
# Query all current auth rules
nak event -k 23456 --content "Get all auth rules" \
-t "auth_query=all" \
--sec $ADMIN_PRIVKEY | nak event $RELAY_URL
# Query only whitelist rules
nak event -k 23456 --content "Get whitelist rules" \
-t "auth_query=whitelist" \
--sec $ADMIN_PRIVKEY | nak event $RELAY_URL
# Query only blacklist rules
nak event -k 23456 --content "Get blacklist rules" \
-t "auth_query=blacklist" \
--sec $ADMIN_PRIVKEY | nak event $RELAY_URL
# Check if specific pattern exists
nak event -k 23456 --content "Check user status" \
-t "auth_query=pattern;$CHECK_USER_PUBKEY" \
--sec $ADMIN_PRIVKEY | nak event $RELAY_URL
# Clear all auth rules (for testing)
nak event -k 23456 --content "Clear all rules" \
-t "system_command=clear_all_auth_rules" \
--sec $ADMIN_PRIVKEY | nak event $RELAY_URL
# Update relay description
nak event -k 23455 --content "Update description" \
-t "relay_description=My awesome relay" \
--sec $ADMIN_PRIVKEY | nak event $RELAY_URL
# Set connection limits
nak event -k 23455 --content "Update limits" \
-t "max_connections=500" \
-t "max_subscriptions_per_client=10" \
--sec $ADMIN_PRIVKEY | nak event $RELAY_URL
```
**For encrypted commands (NIP-44), use empty tags and encrypted content:**
```bash
# Enable auth rules (encrypted)
ENCRYPTED_TAGS=$(echo '[["auth_enabled","true"]]' | nip44_encrypt_with_relay_pubkey)
nak event -k 23455 --content "{\"encrypted_tags\":\"$ENCRYPTED_TAGS\"}" \
--sec $ADMIN_PRIVKEY | nak event $RELAY_URL
# Add user to blacklist (encrypted)
ENCRYPTED_TAGS=$(echo '[["blacklist","pubkey","'$SPAM_USER_PUBKEY'"]]' | nip44_encrypt_with_relay_pubkey)
nak event -k 23456 --content "{\"encrypted_tags\":\"$ENCRYPTED_TAGS\"}" \
--sec $ADMIN_PRIVKEY | nak event $RELAY_URL
```
### Authentication Systems
C-Relay supports **two independent authentication systems**:
#### 1. Auth Rules (Whitelist/Blacklist)
- **Config Key**: `auth_enabled=true`
- **Purpose**: Block or allow specific pubkeys from publishing events
- **Method**: Database-driven rules enforcement
- **Use Case**: Spam prevention, content moderation
#### 2. NIP-42 Cryptographic Authentication
- **Config Key**: `nip42_auth_required=true`
- **Purpose**: Require cryptographic proof of pubkey ownership
- **Method**: Challenge-response authentication protocol
- **Use Case**: Verified identity, premium features
**Important**: These systems can be used independently or together:
- `auth_enabled=true` + `nip42_auth_required=false`: Only whitelist/blacklist rules
- `auth_enabled=false` + `nip42_auth_required=true`: Only NIP-42 auth challenges
- Both `true`: Users must pass NIP-42 auth AND not be blacklisted
### Security Considerations
- **Private Key Protection**: Admin private key grants full relay control
- **Network Access**: Admin commands should only be sent over secure connections
- **Backup**: Keep secure backups of admin private key
- **Rotation**: Consider implementing admin key rotation for production deployments
### Troubleshooting
**Common Issues:**
- `auth-required: not authorized admin`: Verify you're using the correct admin private key
- `invalid: missing or invalid kind`: Ensure event kind is 23455 or 23456
- `error: failed to process configuration event`: Check configuration key/value validity
- `no valid auth rules found`: Verify tag format uses semicolon syntax for multi-element tags
**Debug Commands:**
```bash
# Check if relay is accepting connections
echo '["REQ","test",{}]' | websocat ws://localhost:8888
# Test admin authentication
nak event -k 23455 --content "Test auth" \
-t "test_config=true" \
--sec $ADMIN_PRIVKEY | nak event ws://localhost:8888
# Discover available configuration keys
nak event -k 23455 --content "Discovery query" \
-t "config_query=list_all_keys" \
--sec $ADMIN_PRIVKEY | nak event ws://localhost:8888
# Query current auth rules
nak event -k 23456 --content "Get auth rules" \
-t "auth_query=all" \
--sec $ADMIN_PRIVKEY | nak event ws://localhost:8888
```
### Configuration Discovery
Before making changes, admins can query the relay to discover all available configuration options:
```bash
# Get list of all editable configuration keys with descriptions
nak event -k 23455 --content '{"query":"list_config_keys","description":"Discovery"}' \
-t "config_query=list_all_keys" \
--sec $ADMIN_PRIVKEY | nak event $RELAY_URL
# Get current values of all configuration parameters
nak event -k 23455 --content '{"query":"get_config","description":"Current state"}' \
-t "config_query=get_current_config" \
--sec $ADMIN_PRIVKEY | nak event $RELAY_URL
```
**Expected Response Format:**
```json
["EVENT", "subscription_id", {
"kind": 23455,
"content": "{\"config_keys\": [\"auth_enabled\", \"max_connections\", ...], \"descriptions\": {...}}",
"tags": [["response_type", "config_keys_list"]]
}]
```

View File

@@ -951,12 +951,12 @@
relayStatus.textContent = 'CONNECTED - SUBSCRIBING...'; relayStatus.textContent = 'CONNECTED - SUBSCRIBING...';
relayStatus.className = 'status connected'; relayStatus.className = 'status connected';
// Send REQ message to subscribe to kind 33334 events // Send REQ message to subscribe to kind 23455 events (ephemeral config events)
const reqMessage = [ const reqMessage = [
"REQ", "REQ",
subscriptionId, subscriptionId,
{ {
"kinds": [33334], "kinds": [23455],
"limit": 50 "limit": 50
} }
]; ];
@@ -1176,10 +1176,10 @@
'max_limit': 'Maximum Query Limit' 'max_limit': 'Maximum Query Limit'
}; };
// Process configuration tags // Process configuration tags (no d tag filtering for ephemeral events)
const configData = {}; const configData = {};
event.tags.forEach(tag => { event.tags.forEach(tag => {
if (tag.length >= 2 && tag[0] !== 'd') { // Skip 'd' tag (relay identifier) if (tag.length >= 2) {
configData[tag[0]] = tag[1]; configData[tag[0]] = tag[1];
} }
}); });
@@ -1270,26 +1270,23 @@
const formInputs = configForm.querySelectorAll('input, select'); const formInputs = configForm.querySelectorAll('input, select');
const newTags = []; const newTags = [];
// Preserve the 'd' tag (relay identifier) from original event // Add updated configuration tags (no d tag needed for ephemeral events)
const dTag = currentConfig.tags.find(tag => tag[0] === 'd');
if (dTag) {
newTags.push(dTag);
}
// Add updated configuration tags
formInputs.forEach(input => { formInputs.forEach(input => {
if (!input.disabled && input.name) { if (!input.disabled && input.name) {
newTags.push([input.name, input.value]); newTags.push([input.name, input.value]);
} }
}); });
// Create new kind 33334 event // Create new kind 23455 event (ephemeral configuration event)
const newEvent = { const newEvent = {
kind: 33334, kind: 23455,
pubkey: userPubkey, pubkey: userPubkey,
created_at: Math.floor(Date.now() / 1000), created_at: Math.floor(Date.now() / 1000),
tags: newTags, tags: newTags,
content: currentConfig.content || 'C Nostr Relay Configuration' content: JSON.stringify({
action: 'update_config',
config_data: Object.fromEntries(newTags.filter(tag => tag[0] !== 'd'))
})
}; };
console.log('Signing event with window.nostr.signEvent()...'); console.log('Signing event with window.nostr.signEvent()...');
@@ -1660,7 +1657,7 @@
try { try {
log(`Deleting auth rule: ${rule.rule_type} - ${rule.pattern_value || rule.rule_target}`, 'INFO'); log(`Deleting auth rule: ${rule.rule_type} - ${rule.pattern_value || rule.rule_target}`, 'INFO');
// TODO: Implement actual rule deletion via WebSocket kind 33335 event // TODO: Implement actual rule deletion via WebSocket kind 23456 event
// For now, just remove from local array // For now, just remove from local array
currentAuthRules.splice(index, 1); currentAuthRules.splice(index, 1);
displayAuthRules(currentAuthRules); displayAuthRules(currentAuthRules);
@@ -1746,7 +1743,7 @@
if (editingAuthRule) { if (editingAuthRule) {
log(`Updating auth rule: ${ruleData.rule_type} - ${ruleData.pattern_value}`, 'INFO'); log(`Updating auth rule: ${ruleData.rule_type} - ${ruleData.pattern_value}`, 'INFO');
// TODO: Implement actual rule update via WebSocket kind 33335 event // TODO: Implement actual rule update via WebSocket kind 23456 event
// For now, just update local array // For now, just update local array
currentAuthRules[editingAuthRule.index] = { ...ruleData, id: editingAuthRule.id || Date.now() }; currentAuthRules[editingAuthRule.index] = { ...ruleData, id: editingAuthRule.id || Date.now() };
@@ -1754,7 +1751,7 @@
} else { } else {
log(`Adding new auth rule: ${ruleData.rule_type} - ${ruleData.pattern_value}`, 'INFO'); log(`Adding new auth rule: ${ruleData.rule_type} - ${ruleData.pattern_value}`, 'INFO');
// TODO: Implement actual rule creation via WebSocket kind 33335 event // TODO: Implement actual rule creation via WebSocket kind 23456 event
// For now, just add to local array // For now, just add to local array
currentAuthRules.push({ ...ruleData, id: Date.now() }); currentAuthRules.push({ ...ruleData, id: Date.now() });
@@ -2021,7 +2018,7 @@
}); });
} }
// Add auth rule via WebSocket (kind 33335 event) // Add auth rule via WebSocket (kind 23456 event)
async function addAuthRuleViaWebSocket(ruleData) { async function addAuthRuleViaWebSocket(ruleData) {
if (!isLoggedIn || !userPubkey) { if (!isLoggedIn || !userPubkey) {
throw new Error('Must be logged in to add auth rules'); throw new Error('Must be logged in to add auth rules');
@@ -2064,13 +2061,12 @@
dbPatternType = 'pubkey'; dbPatternType = 'pubkey';
} }
// Create kind 33335 auth rule event with database schema values // Create kind 23456 auth rule event (ephemeral auth management)
const authEvent = { const authEvent = {
kind: 33335, kind: 23456,
pubkey: userPubkey, pubkey: userPubkey,
created_at: Math.floor(Date.now() / 1000), created_at: Math.floor(Date.now() / 1000),
tags: [ tags: [
['d', 'auth-rules'], // Addressable event identifier
[dbRuleType, dbPatternType, ruleData.pattern_value] [dbRuleType, dbPatternType, ruleData.pattern_value]
], ],
content: JSON.stringify({ content: JSON.stringify({

537
docs/admin_api_plan.md Normal file
View File

@@ -0,0 +1,537 @@
# C-Relay Administrator API Implementation Plan
## Problem Analysis
### Current Issues Identified:
1. **Schema Mismatch**: Storage system (config.c) vs Validation system (request_validator.c) use different column names and values
2. **Missing API Endpoint**: No way to clear auth_rules table for testing
3. **Configuration Gap**: Auth rules enforcement may not be properly enabled
4. **Documentation Gap**: Admin API commands not documented
### Root Cause: Auth Rules Schema Inconsistency
**Current Schema (sql_schema.h lines 140-150):**
```sql
CREATE TABLE auth_rules (
rule_type TEXT CHECK (rule_type IN ('whitelist', 'blacklist')),
pattern_type TEXT CHECK (pattern_type IN ('pubkey', 'hash')),
pattern_value TEXT,
action TEXT CHECK (action IN ('allow', 'deny')),
active INTEGER DEFAULT 1
);
```
**Storage Implementation (config.c):**
- Stores: `rule_type='blacklist'`, `pattern_type='pubkey'`, `pattern_value='hex'`, `action='allow'`
**Validation Implementation (request_validator.c):**
- Queries: `rule_type='pubkey_blacklist'`, `rule_target='hex'`, `operation='event'`, `enabled=1`
**MISMATCH**: Validator looks for non-existent columns and wrong rule_type values!
## Proposed Solution Architecture
### Phase 1: API Documentation & Standardization
#### Admin API Commands (via WebSocket with admin private key)
**Kind 23455: Configuration Management (Ephemeral)**
- Update relay settings, limits, authentication policies
- **Standard Mode**: Commands in tags `["config_key", "config_value"]`
- **Encrypted Mode**: Commands NIP-44 encrypted in content `{"encrypted_tags": "..."}`
- Content: Descriptive text or encrypted payload
- Security: Optional NIP-44 encryption for sensitive operations
**Kind 23456: Auth Rules & System Management (Ephemeral)**
- Auth rules: Add/remove/query whitelist/blacklist rules
- System commands: clear rules, status, cache management
- **Standard Mode**: Commands in tags
- Rule format: `["rule_type", "pattern_type", "pattern_value"]`
- Query format: `["auth_query", "filter"]`
- System format: `["system_command", "command_name"]`
- **Encrypted Mode**: Commands NIP-44 encrypted in content `{"encrypted_tags": "..."}`
- Content: Action description + optional encrypted payload
- Security: Optional NIP-44 encryption for sensitive operations
#### Configuration Query Commands (using Kind 23455)
1. **List All Configuration Keys (Standard)**:
```json
{
"kind": 23455,
"content": "Discovery query",
"tags": [["config_query", "list_all_keys"]]
}
```
2. **List All Configuration Keys (Encrypted)**:
```json
{
"kind": 23455,
"content": "{\"query\":\"list_config_keys\",\"encrypted_tags\":\"nip44_encrypted_payload\"}",
"tags": []
}
```
*Encrypted payload contains:* `[["config_query", "list_all_keys"]]`
3. **Get Current Configuration (Standard)**:
```json
{
"kind": 23455,
"content": "Config query",
"tags": [["config_query", "get_current_config"]]
}
```
4. **Get Current Configuration (Encrypted)**:
```json
{
"kind": 23455,
"content": "{\"query\":\"get_config\",\"encrypted_tags\":\"nip44_encrypted_payload\"}",
"tags": []
}
```
*Encrypted payload contains:* `[["config_query", "get_current_config"]]`
#### System Management Commands (using Kind 23456)
1. **Clear All Auth Rules (Standard)**:
```json
{
"kind": 23456,
"content": "{\"action\":\"clear_all\"}",
"tags": [["system_command", "clear_all_auth_rules"]]
}
```
2. **Clear All Auth Rules (Encrypted)**:
```json
{
"kind": 23456,
"content": "{\"action\":\"clear_all\",\"encrypted_tags\":\"nip44_encrypted_payload\"}",
"tags": []
}
```
*Encrypted payload contains:* `[["system_command", "clear_all_auth_rules"]]`
3. **Query All Auth Rules (Standard)**:
```json
{
"kind": 23456,
"content": "{\"query\":\"list_auth_rules\"}",
"tags": [["auth_query", "all"]]
}
```
4. **Query All Auth Rules (Encrypted)**:
```json
{
"kind": 23456,
"content": "{\"query\":\"list_auth_rules\",\"encrypted_tags\":\"nip44_encrypted_payload\"}",
"tags": []
}
```
*Encrypted payload contains:* `[["auth_query", "all"]]`
5. **Add Blacklist Rule (Standard)**:
```json
{
"kind": 23456,
"content": "{\"action\":\"add\"}",
"tags": [["blacklist", "pubkey", "deadbeef1234abcd..."]]
}
```
6. **Add Blacklist Rule (Encrypted)**:
```json
{
"kind": 23456,
"content": "{\"action\":\"add\",\"encrypted_tags\":\"nip44_encrypted_payload\"}",
"tags": []
}
```
*Encrypted payload contains:* `[["blacklist", "pubkey", "deadbeef1234abcd..."]]`
### Phase 2: Auth Rules Schema Alignment
#### Option A: Fix Validator to Match Schema (RECOMMENDED)
**Update request_validator.c:**
```sql
-- OLD (broken):
WHERE rule_type = 'pubkey_blacklist' AND rule_target = ? AND operation = ? AND enabled = 1
-- NEW (correct):
WHERE rule_type = 'blacklist' AND pattern_type = 'pubkey' AND pattern_value = ? AND active = 1
```
**Benefits:**
- Matches actual database schema
- Simpler rule_type values ('blacklist' vs 'pubkey_blacklist')
- Uses existing columns (pattern_value vs rule_target)
- Consistent with storage implementation
#### Option B: Update Schema to Match Validator (NOT RECOMMENDED)
Would require changing schema, migration scripts, and storage logic.
### Phase 3: Implementation Priority
#### High Priority (Critical for blacklist functionality):
1. Fix request_validator.c schema mismatch
2. Ensure auth_required configuration is enabled
3. Update tests to use ephemeral event kinds (23455/23456)
4. Test blacklist enforcement
#### Medium Priority (Enhanced Admin Features):
1. **Implement NIP-44 Encryption Support**:
- Detect empty tags array for Kind 23455/23456 events
- Parse `encrypted_tags` field from content JSON
- Decrypt using admin privkey and relay pubkey
- Process decrypted tags as normal commands
2. Add clear_all_auth_rules system command
3. Add auth rule query functionality (both standard and encrypted modes)
4. Add configuration discovery (list available config keys)
5. Enhanced error reporting in admin API
6. Conflict resolution (same pubkey in whitelist + blacklist)
#### Security Priority (NIP-44 Implementation):
1. **Encryption Detection Logic**: Check for empty tags + encrypted_tags field
2. **Key Pair Management**: Use admin private key + relay public key for NIP-44
3. **Backward Compatibility**: Support both standard and encrypted modes
4. **Error Handling**: Graceful fallback if decryption fails
5. **Performance**: Cache decrypted results to avoid repeated decryption
#### Low Priority (Documentation & Polish):
1. Complete README.md API documentation
2. Example usage scripts
3. Admin client tools
### Phase 4: Expected API Structure
#### README.md Documentation Format:
```markdown
# C-Relay Administrator API
## Authentication
All admin commands require signing with the admin private key generated during first startup.
## Configuration Management (Kind 23455 - Ephemeral)
Update relay configuration parameters or query available settings.
**Configuration Update Event:**
```json
{
"kind": 23455,
"content": "Configuration update",
"tags": [
["config_key1", "config_value1"],
["config_key2", "config_value2"]
]
}
```
**List Available Config Keys:**
```json
{
"kind": 23455,
"content": "{\"query\":\"list_config_keys\",\"description\":\"Get editable config keys\"}",
"tags": [
["config_query", "list_all_keys"]
]
}
```
**Get Current Configuration:**
```json
{
"kind": 23455,
"content": "{\"query\":\"get_config\",\"description\":\"Get current config values\"}",
"tags": [
["config_query", "get_current_config"]
]
}
```
## Auth Rules Management (Kind 23456 - Ephemeral)
Manage whitelist and blacklist rules.
**Add Rule Event:**
```json
{
"kind": 23456,
"content": "{\"action\":\"add\",\"description\":\"Block malicious user\"}",
"tags": [
["blacklist", "pubkey", "deadbeef1234..."]
]
}
```
**Remove Rule Event:**
```json
{
"kind": 23456,
"content": "{\"action\":\"remove\",\"description\":\"Unblock user\"}",
"tags": [
["blacklist", "pubkey", "deadbeef1234..."]
]
}
```
**Query All Auth Rules:**
```json
{
"kind": 23456,
"content": "{\"query\":\"list_auth_rules\",\"description\":\"Get all rules\"}",
"tags": [
["auth_query", "all"]
]
}
```
**Query Whitelist Rules Only:**
```json
{
"kind": 23456,
"content": "{\"query\":\"list_auth_rules\",\"description\":\"Get whitelist\"}",
"tags": [
["auth_query", "whitelist"]
]
}
```
**Check Specific Pattern:**
```json
{
"kind": 23456,
"content": "{\"query\":\"check_pattern\",\"description\":\"Check if pattern exists\"}",
"tags": [
["auth_query", "pattern", "deadbeef1234..."]
]
}
```
## System Management (Kind 23456 - Ephemeral)
System administration commands using the same kind as auth rules.
**Clear All Auth Rules:**
```json
{
"kind": 23456,
"content": "{\"action\":\"clear_all\",\"description\":\"Clear all auth rules\"}",
"tags": [
["system_command", "clear_all_auth_rules"]
]
}
```
**System Status:**
```json
{
"kind": 23456,
"content": "{\"action\":\"system_status\",\"description\":\"Get system status\"}",
"tags": [
["system_command", "system_status"]
]
}
```
## Response Format
All admin commands return JSON responses via WebSocket:
**Success Response:**
```json
["OK", "event_id", true, "success_message"]
```
**Error Response:**
```json
["OK", "event_id", false, "error_message"]
```
## Configuration Keys
- `relay_description`: Relay description text
- `relay_contact`: Contact information
- `auth_enabled`: Enable authentication system
- `max_connections`: Maximum concurrent connections
- `pow_min_difficulty`: Minimum proof-of-work difficulty
- ... (full list of config keys)
## Examples
### Enable Authentication & Add Blacklist
```bash
# 1. Enable auth system
nak event -k 23455 --content "Enable authentication" \
-t "auth_enabled=true" \
--sec $ADMIN_PRIVKEY | nak event ws://localhost:8888
# 2. Add user to blacklist
nak event -k 23456 --content '{"action":"add","description":"Spam user"}' \
-t "blacklist=pubkey;$SPAM_USER_PUBKEY" \
--sec $ADMIN_PRIVKEY | nak event ws://localhost:8888
# 3. Query all auth rules
nak event -k 23456 --content '{"query":"list_auth_rules","description":"Get all rules"}' \
-t "auth_query=all" \
--sec $ADMIN_PRIVKEY | nak event ws://localhost:8888
# 4. Clear all rules for testing
nak event -k 23456 --content '{"action":"clear_all","description":"Clear all rules"}' \
-t "system_command=clear_all_auth_rules" \
--sec $ADMIN_PRIVKEY | nak event ws://localhost:8888
```
## Expected Response Formats
### Configuration Query Response
```json
["EVENT", "subscription_id", {
"kind": 23455,
"content": "{\"config_keys\": [\"auth_enabled\", \"max_connections\"], \"descriptions\": {\"auth_enabled\": \"Enable whitelist/blacklist rules\"}}",
"tags": [["response_type", "config_keys_list"]]
}]
```
### Current Config Response
```json
["EVENT", "subscription_id", {
"kind": 23455,
"content": "{\"current_config\": {\"auth_enabled\": \"true\", \"max_connections\": \"1000\"}}",
"tags": [["response_type", "current_config"]]
}]
```
### Auth Rules Query Response
```json
["EVENT", "subscription_id", {
"kind": 23456,
"content": "{\"auth_rules\": [{\"rule_type\": \"blacklist\", \"pattern_type\": \"pubkey\", \"pattern_value\": \"deadbeef...\"}, {\"rule_type\": \"whitelist\", \"pattern_type\": \"pubkey\", \"pattern_value\": \"cafebabe...\"}]}",
"tags": [["response_type", "auth_rules_list"], ["query_type", "all"]]
}]
```
### Pattern Check Response
```json
["EVENT", "subscription_id", {
"kind": 23456,
"content": "{\"pattern_exists\": true, \"rule_type\": \"blacklist\", \"pattern_value\": \"deadbeef...\"}",
"tags": [["response_type", "pattern_check"], ["pattern", "deadbeef..."]]
}]
```
## Implementation Steps
1. **Document API** (this file) ✅
2. **Update to ephemeral event kinds** ✅
3. **Fix request_validator.c** schema mismatch
4. **Update tests** to use Kind 23455/23456
5. **Add auth rule query functionality**
6. **Add configuration discovery feature**
7. **Test blacklist functionality**
8. **Add remaining system commands**
## Testing Plan
1. Fix schema mismatch and test basic blacklist
2. Add clear_auth_rules and test table cleanup
3. Test whitelist/blacklist conflict scenarios
4. Test all admin API commands end-to-end
5. Update integration tests
This plan addresses the immediate blacklist issue while establishing a comprehensive admin API framework for future expansion.
## NIP-44 Encryption Implementation Details
### Server-Side Detection Logic
```c
// In admin event processing function
bool is_encrypted_command(struct nostr_event *event) {
// Check if Kind 23455 or 23456 with empty tags
if ((event->kind == 23455 || event->kind == 23456) &&
event->tags_count == 0) {
return true;
}
return false;
}
cJSON *decrypt_admin_tags(struct nostr_event *event) {
cJSON *content_json = cJSON_Parse(event->content);
if (!content_json) return NULL;
cJSON *encrypted_tags = cJSON_GetObjectItem(content_json, "encrypted_tags");
if (!encrypted_tags) {
cJSON_Delete(content_json);
return NULL;
}
// Decrypt using NIP-44 with admin pubkey and relay privkey
char *decrypted = nip44_decrypt(
cJSON_GetStringValue(encrypted_tags),
admin_pubkey, // Shared secret with admin
relay_private_key // Our private key
);
cJSON *decrypted_tags = cJSON_Parse(decrypted);
free(decrypted);
cJSON_Delete(content_json);
return decrypted_tags; // Returns tag array: [["key1", "val1"], ["key2", "val2"]]
}
```
### Admin Event Processing Flow
1. **Receive Event**: Kind 23455/23456 with admin signature
2. **Check Mode**: Empty tags = encrypted, populated tags = standard
3. **Decrypt if Needed**: Extract and decrypt `encrypted_tags` from content
4. **Process Commands**: Use decrypted/standard tags for command processing
5. **Execute**: Same logic for both modes after tag extraction
6. **Respond**: Standard response format (optionally encrypt response)
### Security Benefits
- **Command Privacy**: Admin operations invisible in event tags
- **Replay Protection**: NIP-44 includes timestamp/randomness
- **Key Management**: Uses existing admin/relay key pair
- **Backward Compatible**: Standard mode still works
- **Performance**: Only decrypt when needed (empty tags detection)
### NIP-44 Library Integration
The relay will need to integrate a NIP-44 encryption/decryption library:
```c
// Required NIP-44 functions
char* nip44_encrypt(const char* plaintext, const char* sender_privkey, const char* recipient_pubkey);
char* nip44_decrypt(const char* ciphertext, const char* recipient_privkey, const char* sender_pubkey);
```
### Implementation Priority (Updated)
#### Phase 1: Core Infrastructure (Complete)
- [x] Event-based admin authentication system
- [x] Kind 23455/23456 (Configuration/Auth Rules) processing
- [x] Basic configuration parameter updates
- [x] Auth rule add/remove/clear functionality
- [x] Updated to ephemeral event kinds
- [x] Designed NIP-44 encryption support
#### Phase 2: NIP-44 Encryption Support (Next Priority)
- [ ] **Add NIP-44 library dependency** to project
- [ ] **Implement encryption detection logic** (`is_encrypted_command()`)
- [ ] **Add decrypt_admin_tags() function** with NIP-44 support
- [ ] **Update admin command processing** to handle both modes
- [ ] **Test encrypted admin commands** end-to-end
#### Phase 3: Enhanced Features
- [ ] **Auth rule query functionality** (both standard and encrypted modes)
- [ ] **Configuration discovery API** (list available config keys)
- [ ] **Enhanced error messages** with encryption status
- [ ] **Performance optimization** (caching, async decrypt)
#### Phase 4: Schema Fixes (Critical)
- [ ] **Fix request_validator.c** schema mismatch
- [ ] **Enable blacklist enforcement** with encrypted commands
- [ ] **Update tests** to use both standard and encrypted modes
This enhanced admin API provides enterprise-grade security while maintaining ease of use for basic operations.

View File

@@ -1 +1 @@
1950163 238618

View File

@@ -73,6 +73,16 @@ int is_config_table_ready(void);
int migrate_config_from_events_to_table(void); int migrate_config_from_events_to_table(void);
int populate_config_table_from_event(const cJSON* event); int populate_config_table_from_event(const cJSON* event);
// Forward declarations for admin API query handlers
int handle_config_list_keys_query(cJSON* event, char* error_message, size_t error_size);
int handle_config_get_current_query(cJSON* event, char* error_message, size_t error_size);
int handle_auth_list_all_query(cJSON* event, char* error_message, size_t error_size);
int handle_auth_whitelist_query(cJSON* event, char* error_message, size_t error_size);
int handle_auth_blacklist_query(cJSON* event, char* error_message, size_t error_size);
int handle_auth_pattern_check_query(cJSON* event, char* error_message, size_t error_size);
int handle_clear_all_auth_rules_command(cJSON* event, char* error_message, size_t error_size);
int handle_system_status_query(cJSON* event, char* error_message, size_t error_size);
// Current configuration cache // Current configuration cache
static cJSON* g_current_config = NULL; static cJSON* g_current_config = NULL;
@@ -2055,7 +2065,7 @@ int add_pubkeys_to_config_table(void) {
// ADMIN EVENT PROCESSING FUNCTIONS // ADMIN EVENT PROCESSING FUNCTIONS
// ================================ // ================================
// Process admin events (moved from main.c) // Process admin events (updated for new Kind 23455/23456)
int process_admin_event_in_config(cJSON* event, char* error_message, size_t error_size) { int process_admin_event_in_config(cJSON* event, char* error_message, size_t error_size) {
cJSON* kind_obj = cJSON_GetObjectItem(event, "kind"); cJSON* kind_obj = cJSON_GetObjectItem(event, "kind");
if (!kind_obj || !cJSON_IsNumber(kind_obj)) { if (!kind_obj || !cJSON_IsNumber(kind_obj)) {
@@ -2081,26 +2091,64 @@ int process_admin_event_in_config(cJSON* event, char* error_message, size_t erro
int kind = (int)cJSON_GetNumberValue(kind_obj); int kind = (int)cJSON_GetNumberValue(kind_obj);
switch (kind) { switch (kind) {
case 33334: case 23455: // New ephemeral configuration management
return process_admin_config_event(event, error_message, error_size); return process_admin_config_event(event, error_message, error_size);
case 33335: case 23456: // New ephemeral auth rules management
return process_admin_auth_event(event, error_message, error_size);
case 33334: // Legacy addressable config events (backward compatibility)
return process_admin_config_event(event, error_message, error_size);
case 33335: // Legacy addressable auth events (backward compatibility)
return process_admin_auth_event(event, error_message, error_size); return process_admin_auth_event(event, error_message, error_size);
default: default:
snprintf(error_message, error_size, "invalid: unsupported admin event kind"); snprintf(error_message, error_size, "invalid: unsupported admin event kind %d", kind);
return -1; return -1;
} }
} }
// Handle kind 33334 config events // Handle Kind 23455 configuration management events and legacy Kind 33334
int process_admin_config_event(cJSON* event, char* error_message, size_t error_size) { int process_admin_config_event(cJSON* event, char* error_message, size_t error_size) {
cJSON* kind_obj = cJSON_GetObjectItem(event, "kind");
int kind = kind_obj ? (int)cJSON_GetNumberValue(kind_obj) : 0;
// Parse content for action commands
cJSON* content_obj = cJSON_GetObjectItem(event, "content");
const char* content = content_obj ? cJSON_GetStringValue(content_obj) : "";
// Check if this is a query command
cJSON* content_json = cJSON_Parse(content);
char action_buffer[32] = "set"; // default action
const char* action = action_buffer;
if (content_json) {
cJSON* action_obj = cJSON_GetObjectItem(content_json, "action");
if (action_obj && cJSON_IsString(action_obj)) {
const char* action_str = cJSON_GetStringValue(action_obj);
if (action_str) {
strncpy(action_buffer, action_str, sizeof(action_buffer) - 1);
action_buffer[sizeof(action_buffer) - 1] = '\0';
}
}
cJSON_Delete(content_json);
}
log_info("Processing admin configuration event");
printf(" Kind: %d, Action: %s\n", kind, action);
// Handle query commands
if (strcmp(action, "list_config_keys") == 0) {
return handle_config_list_keys_query(event, error_message, error_size);
}
if (strcmp(action, "get_current_config") == 0) {
return handle_config_get_current_query(event, error_message, error_size);
}
// Handle configuration updates (set action)
cJSON* tags_obj = cJSON_GetObjectItem(event, "tags"); cJSON* tags_obj = cJSON_GetObjectItem(event, "tags");
if (!tags_obj || !cJSON_IsArray(tags_obj)) { if (!tags_obj || !cJSON_IsArray(tags_obj)) {
snprintf(error_message, error_size, "invalid: configuration event must have tags"); snprintf(error_message, error_size, "invalid: configuration event must have tags");
return -1; return -1;
} }
// Config table should already exist from embedded schema
// Begin transaction for atomic config updates // Begin transaction for atomic config updates
int rc = sqlite3_exec(g_db, "BEGIN IMMEDIATE TRANSACTION", NULL, NULL, NULL); int rc = sqlite3_exec(g_db, "BEGIN IMMEDIATE TRANSACTION", NULL, NULL, NULL);
if (rc != SQLITE_OK) { if (rc != SQLITE_OK) {
@@ -2127,8 +2175,8 @@ int process_admin_config_event(cJSON* event, char* error_message, size_t error_s
const char* key = cJSON_GetStringValue(tag_name); const char* key = cJSON_GetStringValue(tag_name);
const char* value = cJSON_GetStringValue(tag_value); const char* value = cJSON_GetStringValue(tag_value);
// Skip relay identifier tag // Skip relay identifier tag (only for legacy addressable events)
if (strcmp(key, "d") == 0) { if (kind == 33334 && strcmp(key, "d") == 0) {
continue; continue;
} }
@@ -2154,35 +2202,21 @@ int process_admin_config_event(cJSON* event, char* error_message, size_t error_s
return 0; return 0;
} }
// Handle kind 33335 auth rule events // Handle Kind 23456 auth rules management and legacy Kind 33335
int process_admin_auth_event(cJSON* event, char* error_message, size_t error_size) { int process_admin_auth_event(cJSON* event, char* error_message, size_t error_size) {
log_info("=== SERVER-SIDE AUTH RULE EVENT DEBUG ==="); cJSON* kind_obj = cJSON_GetObjectItem(event, "kind");
int kind = kind_obj ? (int)cJSON_GetNumberValue(kind_obj) : 0;
// Print the entire received event for debugging log_info("Processing admin auth rule event");
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"); // Parse content for action commands
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"); cJSON* content_obj = cJSON_GetObjectItem(event, "content");
const char* content = content_obj ? cJSON_GetStringValue(content_obj) : ""; 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); cJSON* content_json = cJSON_Parse(content);
char action_buffer[16] = "add"; // Local buffer for action string char action_buffer[32] = "add"; // default action
const char* action = action_buffer; // default const char* action = action_buffer;
if (content_json) { if (content_json) {
cJSON* action_obj = cJSON_GetObjectItem(content_json, "action"); cJSON* action_obj = cJSON_GetObjectItem(content_json, "action");
if (action_obj && cJSON_IsString(action_obj)) { if (action_obj && cJSON_IsString(action_obj)) {
@@ -2194,7 +2228,35 @@ int process_admin_auth_event(cJSON* event, char* error_message, size_t error_siz
} }
cJSON_Delete(content_json); cJSON_Delete(content_json);
} }
printf("Parsed action: '%s'\n", action);
printf(" Kind: %d, Action: %s\n", kind, action);
// Handle query commands
if (strcmp(action, "list_all") == 0) {
return handle_auth_list_all_query(event, error_message, error_size);
}
if (strcmp(action, "whitelist_only") == 0) {
return handle_auth_whitelist_query(event, error_message, error_size);
}
if (strcmp(action, "blacklist_only") == 0) {
return handle_auth_blacklist_query(event, error_message, error_size);
}
if (strcmp(action, "pattern_check") == 0) {
return handle_auth_pattern_check_query(event, error_message, error_size);
}
if (strcmp(action, "clear_all_auth_rules") == 0) {
return handle_clear_all_auth_rules_command(event, error_message, error_size);
}
if (strcmp(action, "system_status") == 0) {
return handle_system_status_query(event, error_message, error_size);
}
// Handle auth rule updates (add/remove actions)
cJSON* tags_obj = cJSON_GetObjectItem(event, "tags");
if (!tags_obj || !cJSON_IsArray(tags_obj)) {
snprintf(error_message, error_size, "invalid: auth rule event must have tags");
return -1;
}
// Begin transaction for atomic auth rule updates // Begin transaction for atomic auth rule updates
int rc = sqlite3_exec(g_db, "BEGIN IMMEDIATE TRANSACTION", NULL, NULL, NULL); int rc = sqlite3_exec(g_db, "BEGIN IMMEDIATE TRANSACTION", NULL, NULL, NULL);
@@ -2204,33 +2266,11 @@ int process_admin_auth_event(cJSON* event, char* error_message, size_t error_siz
} }
int rules_processed = 0; int rules_processed = 0;
int tags_examined = 0;
int tags_skipped = 0;
// Process each tag as an auth rule specification // Process each tag as an auth rule specification
cJSON* tag = NULL; cJSON* tag = NULL;
cJSON_ArrayForEach(tag, tags_obj) { cJSON_ArrayForEach(tag, tags_obj) {
tags_examined++; if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 3) {
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; continue;
} }
@@ -2241,8 +2281,6 @@ int process_admin_auth_event(cJSON* event, char* error_message, size_t error_siz
if (!cJSON_IsString(rule_type_obj) || if (!cJSON_IsString(rule_type_obj) ||
!cJSON_IsString(pattern_type_obj) || !cJSON_IsString(pattern_type_obj) ||
!cJSON_IsString(pattern_value_obj)) { !cJSON_IsString(pattern_value_obj)) {
printf(" SKIPPED: One or more elements are not strings\n");
tags_skipped++;
continue; continue;
} }
@@ -2250,33 +2288,18 @@ 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_type = cJSON_GetStringValue(pattern_type_obj);
const char* pattern_value = cJSON_GetStringValue(pattern_value_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);
// Process the auth rule based on action // Process the auth rule based on action
if (strcmp(action, "add") == 0) { if (strcmp(action, "add") == 0) {
printf(" Attempting to add rule to database...\n");
if (add_auth_rule_from_config(rule_type, pattern_type, pattern_value, "allow") == 0) { if (add_auth_rule_from_config(rule_type, pattern_type, pattern_value, "allow") == 0) {
printf(" SUCCESS: Rule added to database\n");
rules_processed++; rules_processed++;
} else {
printf(" FAILED: Could not add rule to database\n");
} }
} else if (strcmp(action, "remove") == 0) { } 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) { if (remove_auth_rule_from_config(rule_type, pattern_type, pattern_value) == 0) {
printf(" SUCCESS: Rule removed from database\n");
rules_processed++; 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) { if (rules_processed > 0) {
sqlite3_exec(g_db, "COMMIT", NULL, NULL, NULL); sqlite3_exec(g_db, "COMMIT", NULL, NULL, NULL);
@@ -2348,6 +2371,425 @@ int remove_auth_rule_from_config(const char* rule_type, const char* pattern_type
return (rc == SQLITE_DONE) ? 0 : -1; return (rc == SQLITE_DONE) ? 0 : -1;
} }
// ================================
// ADMIN API QUERY HANDLERS
// ================================
// Handle configuration list keys query
int handle_config_list_keys_query(cJSON* event, char* error_message, size_t error_size) {
(void)event; // Suppress unused parameter warning
if (!g_db) {
snprintf(error_message, error_size, "database not available");
return -1;
}
log_info("Processing config list keys query");
const char* sql = "SELECT key, data_type, category FROM config ORDER BY category, key";
sqlite3_stmt* stmt;
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
snprintf(error_message, error_size, "failed to prepare config keys query");
return -1;
}
printf("=== Configuration Keys ===\n");
int key_count = 0;
while (sqlite3_step(stmt) == SQLITE_ROW) {
const char* key = (const char*)sqlite3_column_text(stmt, 0);
const char* data_type = (const char*)sqlite3_column_text(stmt, 1);
const char* category = (const char*)sqlite3_column_text(stmt, 2);
printf(" [%s] %s (%s)\n", category ? category : "general", key ? key : "", data_type ? data_type : "string");
key_count++;
}
sqlite3_finalize(stmt);
printf("Total configuration keys: %d\n", key_count);
log_success("Configuration keys listed successfully");
return 0;
}
// Handle get current configuration query
int handle_config_get_current_query(cJSON* event, char* error_message, size_t error_size) {
(void)event; // Suppress unused parameter warning
if (!g_db) {
snprintf(error_message, error_size, "database not available");
return -1;
}
log_info("Processing get current config query");
const char* sql = "SELECT key, value, data_type, category FROM config ORDER BY category, key";
sqlite3_stmt* stmt;
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
snprintf(error_message, error_size, "failed to prepare current config query");
return -1;
}
printf("=== Current Configuration ===\n");
int config_count = 0;
while (sqlite3_step(stmt) == SQLITE_ROW) {
const char* key = (const char*)sqlite3_column_text(stmt, 0);
const char* value = (const char*)sqlite3_column_text(stmt, 1);
const char* data_type = (const char*)sqlite3_column_text(stmt, 2);
const char* category = (const char*)sqlite3_column_text(stmt, 3);
printf(" [%s] %s = %s (%s)\n",
category ? category : "general",
key ? key : "",
value ? value : "",
data_type ? data_type : "string");
config_count++;
}
sqlite3_finalize(stmt);
printf("Total configuration items: %d\n", config_count);
log_success("Current configuration retrieved successfully");
return 0;
}
// Handle auth rules list all query
int handle_auth_list_all_query(cJSON* event, char* error_message, size_t error_size) {
(void)event; // Suppress unused parameter warning
if (!g_db) {
snprintf(error_message, error_size, "database not available");
return -1;
}
log_info("Processing auth rules list all query");
const char* sql = "SELECT rule_type, pattern_type, pattern_value, action FROM auth_rules ORDER BY rule_type, pattern_type";
sqlite3_stmt* stmt;
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
snprintf(error_message, error_size, "failed to prepare auth rules query");
return -1;
}
printf("=== All Auth Rules ===\n");
int rule_count = 0;
while (sqlite3_step(stmt) == SQLITE_ROW) {
const char* rule_type = (const char*)sqlite3_column_text(stmt, 0);
const char* pattern_type = (const char*)sqlite3_column_text(stmt, 1);
const char* pattern_value = (const char*)sqlite3_column_text(stmt, 2);
const char* action = (const char*)sqlite3_column_text(stmt, 3);
printf(" %s %s:%s -> %s\n",
rule_type ? rule_type : "",
pattern_type ? pattern_type : "",
pattern_value ? pattern_value : "",
action ? action : "allow");
rule_count++;
}
sqlite3_finalize(stmt);
printf("Total auth rules: %d\n", rule_count);
log_success("Auth rules listed successfully");
return 0;
}
// Handle whitelist only query
int handle_auth_whitelist_query(cJSON* event, char* error_message, size_t error_size) {
(void)event; // Suppress unused parameter warning
if (!g_db) {
snprintf(error_message, error_size, "database not available");
return -1;
}
log_info("Processing whitelist rules query");
const char* sql = "SELECT rule_type, pattern_type, pattern_value, action FROM auth_rules WHERE rule_type LIKE '%whitelist%' ORDER BY pattern_type";
sqlite3_stmt* stmt;
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
snprintf(error_message, error_size, "failed to prepare whitelist query");
return -1;
}
printf("=== Whitelist Rules ===\n");
int whitelist_count = 0;
while (sqlite3_step(stmt) == SQLITE_ROW) {
const char* rule_type = (const char*)sqlite3_column_text(stmt, 0);
const char* pattern_type = (const char*)sqlite3_column_text(stmt, 1);
const char* pattern_value = (const char*)sqlite3_column_text(stmt, 2);
const char* action = (const char*)sqlite3_column_text(stmt, 3);
printf(" %s %s:%s -> %s\n",
rule_type ? rule_type : "",
pattern_type ? pattern_type : "",
pattern_value ? pattern_value : "",
action ? action : "allow");
whitelist_count++;
}
sqlite3_finalize(stmt);
printf("Total whitelist rules: %d\n", whitelist_count);
log_success("Whitelist rules listed successfully");
return 0;
}
// Handle blacklist only query
int handle_auth_blacklist_query(cJSON* event, char* error_message, size_t error_size) {
(void)event; // Suppress unused parameter warning
if (!g_db) {
snprintf(error_message, error_size, "database not available");
return -1;
}
log_info("Processing blacklist rules query");
const char* sql = "SELECT rule_type, pattern_type, pattern_value, action FROM auth_rules WHERE rule_type LIKE '%blacklist%' ORDER BY pattern_type";
sqlite3_stmt* stmt;
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
snprintf(error_message, error_size, "failed to prepare blacklist query");
return -1;
}
printf("=== Blacklist Rules ===\n");
int blacklist_count = 0;
while (sqlite3_step(stmt) == SQLITE_ROW) {
const char* rule_type = (const char*)sqlite3_column_text(stmt, 0);
const char* pattern_type = (const char*)sqlite3_column_text(stmt, 1);
const char* pattern_value = (const char*)sqlite3_column_text(stmt, 2);
const char* action = (const char*)sqlite3_column_text(stmt, 3);
printf(" %s %s:%s -> %s\n",
rule_type ? rule_type : "",
pattern_type ? pattern_type : "",
pattern_value ? pattern_value : "",
action ? action : "deny");
blacklist_count++;
}
sqlite3_finalize(stmt);
printf("Total blacklist rules: %d\n", blacklist_count);
log_success("Blacklist rules listed successfully");
return 0;
}
// Handle pattern check query
int handle_auth_pattern_check_query(cJSON* event, char* error_message, size_t error_size) {
// Parse tags to get the pattern to check
cJSON* tags_obj = cJSON_GetObjectItem(event, "tags");
if (!tags_obj || !cJSON_IsArray(tags_obj)) {
snprintf(error_message, error_size, "invalid: pattern check requires tags with pattern information");
return -1;
}
const char* check_pattern_type = NULL;
const char* check_pattern_value = NULL;
// Find pattern_check tag
cJSON* tag = NULL;
cJSON_ArrayForEach(tag, tags_obj) {
if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 3) {
cJSON* tag_name = cJSON_GetArrayItem(tag, 0);
if (tag_name && cJSON_IsString(tag_name) &&
strcmp(cJSON_GetStringValue(tag_name), "pattern_check") == 0) {
cJSON* pattern_type_obj = cJSON_GetArrayItem(tag, 1);
cJSON* pattern_value_obj = cJSON_GetArrayItem(tag, 2);
if (pattern_type_obj && cJSON_IsString(pattern_type_obj) &&
pattern_value_obj && cJSON_IsString(pattern_value_obj)) {
check_pattern_type = cJSON_GetStringValue(pattern_type_obj);
check_pattern_value = cJSON_GetStringValue(pattern_value_obj);
break;
}
}
}
}
if (!check_pattern_type || !check_pattern_value) {
snprintf(error_message, error_size, "invalid: pattern_check tag format should be [\"pattern_check\", \"pattern_type\", \"pattern_value\"]");
return -1;
}
if (!g_db) {
snprintf(error_message, error_size, "database not available");
return -1;
}
log_info("Processing pattern check query");
printf(" Checking pattern: %s:%s\n", check_pattern_type, check_pattern_value);
const char* sql = "SELECT rule_type, pattern_type, pattern_value, action FROM auth_rules WHERE pattern_type = ? AND pattern_value = ? ORDER BY rule_type";
sqlite3_stmt* stmt;
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
snprintf(error_message, error_size, "failed to prepare pattern check query");
return -1;
}
sqlite3_bind_text(stmt, 1, check_pattern_type, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, check_pattern_value, -1, SQLITE_STATIC);
printf("=== Pattern Check Results ===\n");
printf("Pattern: %s:%s\n", check_pattern_type, check_pattern_value);
int match_count = 0;
while (sqlite3_step(stmt) == SQLITE_ROW) {
const char* rule_type = (const char*)sqlite3_column_text(stmt, 0);
const char* pattern_type = (const char*)sqlite3_column_text(stmt, 1);
const char* pattern_value = (const char*)sqlite3_column_text(stmt, 2);
const char* action = (const char*)sqlite3_column_text(stmt, 3);
printf(" MATCH: %s %s:%s -> %s\n",
rule_type ? rule_type : "",
pattern_type ? pattern_type : "",
pattern_value ? pattern_value : "",
action ? action : "allow");
match_count++;
}
sqlite3_finalize(stmt);
if (match_count == 0) {
printf(" No matching rules found for this pattern\n");
}
printf("Total matches: %d\n", match_count);
log_success("Pattern check completed successfully");
return 0;
}
// Handle clear all auth rules command
int handle_clear_all_auth_rules_command(cJSON* event, char* error_message, size_t error_size) {
(void)event; // Suppress unused parameter warning
if (!g_db) {
snprintf(error_message, error_size, "database not available");
return -1;
}
log_info("Processing clear all auth rules command");
// Count existing rules first
const char* count_sql = "SELECT COUNT(*) FROM auth_rules";
sqlite3_stmt* count_stmt;
int rc = sqlite3_prepare_v2(g_db, count_sql, -1, &count_stmt, NULL);
if (rc != SQLITE_OK) {
snprintf(error_message, error_size, "failed to prepare count query");
return -1;
}
int rule_count = 0;
if (sqlite3_step(count_stmt) == SQLITE_ROW) {
rule_count = sqlite3_column_int(count_stmt, 0);
}
sqlite3_finalize(count_stmt);
// Delete all auth rules (this operation succeeds even if table is empty)
const char* delete_sql = "DELETE FROM auth_rules";
rc = sqlite3_exec(g_db, delete_sql, NULL, NULL, NULL);
if (rc != SQLITE_OK) {
snprintf(error_message, error_size, "failed to execute clear auth rules command");
return -1;
}
// Always return success - clearing empty table is still a successful operation
if (rule_count > 0) {
printf("Cleared %d auth rules from database\n", rule_count);
log_success("All auth rules cleared successfully");
} else {
printf("Auth rules table was already empty - no rules to clear\n");
log_success("Clear auth rules completed successfully (table was already empty)");
}
return 0;
}
// Handle system status query
int handle_system_status_query(cJSON* event, char* error_message, size_t error_size) {
(void)event; // Suppress unused parameter warning
(void)error_message; // This command always succeeds
(void)error_size;
log_info("Processing system status query");
printf("=== System Status ===\n");
// Database status
printf("Database: %s\n", g_db ? "Connected" : "Not available");
if (strlen(g_database_path) > 0) {
printf("Database path: %s\n", g_database_path);
}
// Configuration status
printf("Unified cache: %s\n", g_unified_cache.cache_valid ? "Valid" : "Invalid");
printf("Admin pubkey: %s\n", g_unified_cache.admin_pubkey[0] ? g_unified_cache.admin_pubkey : "Not set");
printf("Relay pubkey: %s\n", g_unified_cache.relay_pubkey[0] ? g_unified_cache.relay_pubkey : "Not set");
// Count configuration items
if (g_db) {
const char* config_count_sql = "SELECT COUNT(*) FROM config";
sqlite3_stmt* stmt;
if (sqlite3_prepare_v2(g_db, config_count_sql, -1, &stmt, NULL) == SQLITE_OK) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
int config_count = sqlite3_column_int(stmt, 0);
printf("Configuration items: %d\n", config_count);
}
sqlite3_finalize(stmt);
}
// Count auth rules
const char* auth_count_sql = "SELECT COUNT(*) FROM auth_rules";
if (sqlite3_prepare_v2(g_db, auth_count_sql, -1, &stmt, NULL) == SQLITE_OK) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
int auth_count = sqlite3_column_int(stmt, 0);
printf("Auth rules: %d\n", auth_count);
}
sqlite3_finalize(stmt);
}
}
// Cache expiration
if (g_unified_cache.cache_expires > 0) {
time_t now = time(NULL);
long seconds_to_expire = g_unified_cache.cache_expires - now;
printf("Cache expires in: %ld seconds\n", seconds_to_expire);
}
printf("System time: %ld\n", (long)time(NULL));
log_success("System status retrieved successfully");
return 0;
}
// ================================ // ================================
// CONFIGURATION CACHE MANAGEMENT // CONFIGURATION CACHE MANAGEMENT
// ================================ // ================================

View File

@@ -972,28 +972,28 @@ static void get_timestamp_string(char* buffer, size_t buffer_size) {
void log_info(const char* message) { void log_info(const char* message) {
char timestamp[32]; char timestamp[32];
get_timestamp_string(timestamp, sizeof(timestamp)); get_timestamp_string(timestamp, sizeof(timestamp));
printf("[%s] " BLUE "[INFO]" RESET " %s\n", timestamp, message); printf("[%s] [INFO] %s\n", timestamp, message);
fflush(stdout); fflush(stdout);
} }
void log_success(const char* message) { void log_success(const char* message) {
char timestamp[32]; char timestamp[32];
get_timestamp_string(timestamp, sizeof(timestamp)); get_timestamp_string(timestamp, sizeof(timestamp));
printf("[%s] " GREEN "[SUCCESS]" RESET " %s\n", timestamp, message); printf("[%s] [SUCCESS] %s\n", timestamp, message);
fflush(stdout); fflush(stdout);
} }
void log_error(const char* message) { void log_error(const char* message) {
char timestamp[32]; char timestamp[32];
get_timestamp_string(timestamp, sizeof(timestamp)); get_timestamp_string(timestamp, sizeof(timestamp));
printf("[%s] " RED "[ERROR]" RESET " %s\n", timestamp, message); printf("[%s] [ERROR] %s\n", timestamp, message);
fflush(stdout); fflush(stdout);
} }
void log_warning(const char* message) { void log_warning(const char* message) {
char timestamp[32]; char timestamp[32];
get_timestamp_string(timestamp, sizeof(timestamp)); get_timestamp_string(timestamp, sizeof(timestamp));
printf("[%s] " YELLOW "[WARNING]" RESET " %s\n", timestamp, message); printf("[%s] [WARNING] %s\n", timestamp, message);
fflush(stdout); fflush(stdout);
} }
@@ -3243,7 +3243,7 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
// Cleanup event JSON string // Cleanup event JSON string
free(event_json_str); free(event_json_str);
// Check for admin events (kinds 33334 and 33335) and intercept them // Check for admin events (kinds 33334, 33335, 23455, and 23456) and intercept them
if (result == 0) { if (result == 0) {
cJSON* kind_obj = cJSON_GetObjectItem(event, "kind"); cJSON* kind_obj = cJSON_GetObjectItem(event, "kind");
if (kind_obj && cJSON_IsNumber(kind_obj)) { if (kind_obj && cJSON_IsNumber(kind_obj)) {
@@ -3251,7 +3251,21 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
log_info("DEBUG ADMIN: Checking if admin event processing is needed"); log_info("DEBUG ADMIN: Checking if admin event processing is needed");
if (event_kind == 33334 || event_kind == 33335) { // Log reception of Kind 23455 and 23456 events
if (event_kind == 23455 || event_kind == 23456) {
char* event_json_debug = cJSON_Print(event);
char debug_received_msg[1024];
snprintf(debug_received_msg, sizeof(debug_received_msg),
"RECEIVED Kind %d event: %s", event_kind,
event_json_debug ? event_json_debug : "Failed to serialize");
log_info(debug_received_msg);
if (event_json_debug) {
free(event_json_debug);
}
}
if (event_kind == 33334 || event_kind == 33335 || event_kind == 23455 || event_kind == 23456) {
// This is an admin event - process it through the admin API instead of normal storage // This is an admin event - process it through the admin API instead of normal storage
log_info("DEBUG ADMIN: Admin event detected, processing through admin API"); log_info("DEBUG ADMIN: Admin event detected, processing through admin API");
@@ -3263,6 +3277,21 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
"DEBUG ADMIN: process_admin_event_in_config returned %d", admin_result); "DEBUG ADMIN: process_admin_event_in_config returned %d", admin_result);
log_info(debug_admin_msg); log_info(debug_admin_msg);
// Log results for Kind 23455 and 23456 events
if (event_kind == 23455 || event_kind == 23456) {
if (admin_result == 0) {
char success_result_msg[256];
snprintf(success_result_msg, sizeof(success_result_msg),
"SUCCESS: Kind %d event processed successfully", event_kind);
log_success(success_result_msg);
} else {
char error_result_msg[512];
snprintf(error_result_msg, sizeof(error_result_msg),
"ERROR: Kind %d event processing failed: %s", event_kind, admin_error);
log_error(error_result_msg);
}
}
if (admin_result != 0) { if (admin_result != 0) {
log_error("DEBUG ADMIN: Failed to process admin event through admin API"); log_error("DEBUG ADMIN: Failed to process admin event through admin API");
result = -1; result = -1;

View File

@@ -629,28 +629,26 @@ static int check_database_auth_rules(const char *pubkey, const char *operation,
// Step 1: Check pubkey blacklist (highest priority) // Step 1: Check pubkey blacklist (highest priority)
const char *blacklist_sql = const char *blacklist_sql =
"SELECT rule_type, description FROM auth_rules WHERE rule_type = " "SELECT rule_type, action FROM auth_rules WHERE rule_type = "
"'pubkey_blacklist' AND rule_target = ? AND operation = ? AND enabled = " "'blacklist' AND pattern_type = 'pubkey' AND pattern_value = ? LIMIT 1";
"1 ORDER BY priority LIMIT 1";
rc = sqlite3_prepare_v2(db, blacklist_sql, -1, &stmt, NULL); rc = sqlite3_prepare_v2(db, blacklist_sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) { if (rc == SQLITE_OK) {
sqlite3_bind_text(stmt, 1, pubkey, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 1, pubkey, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, operation ? operation : "", -1, SQLITE_STATIC);
if (sqlite3_step(stmt) == SQLITE_ROW) { if (sqlite3_step(stmt) == SQLITE_ROW) {
const char *description = (const char *)sqlite3_column_text(stmt, 1); const char *action = (const char *)sqlite3_column_text(stmt, 1);
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 1 FAILED - " validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 1 FAILED - "
"Pubkey blacklisted\n"); "Pubkey blacklisted\n");
char blacklist_msg[256]; char blacklist_msg[256];
sprintf(blacklist_msg, sprintf(blacklist_msg,
"VALIDATOR_DEBUG: RULES ENGINE - Blacklist rule matched: %s\n", "VALIDATOR_DEBUG: RULES ENGINE - Blacklist rule matched: action=%s\n",
description ? description : "Unknown"); action ? action : "deny");
validator_debug_log(blacklist_msg); validator_debug_log(blacklist_msg);
// Set specific violation details for status code mapping // Set specific violation details for status code mapping
strcpy(g_last_rule_violation.violation_type, "pubkey_blacklist"); strcpy(g_last_rule_violation.violation_type, "pubkey_blacklist");
sprintf(g_last_rule_violation.reason, "%s: Public key blacklisted", sprintf(g_last_rule_violation.reason, "Public key blacklisted: %s",
description ? description : "TEST_PUBKEY_BLACKLIST"); action ? action : "PUBKEY_BLACKLIST");
sqlite3_finalize(stmt); sqlite3_finalize(stmt);
sqlite3_close(db); sqlite3_close(db);
@@ -664,29 +662,27 @@ static int check_database_auth_rules(const char *pubkey, const char *operation,
// Step 2: Check hash blacklist // Step 2: Check hash blacklist
if (resource_hash) { if (resource_hash) {
const char *hash_blacklist_sql = const char *hash_blacklist_sql =
"SELECT rule_type, description FROM auth_rules WHERE rule_type = " "SELECT rule_type, action FROM auth_rules WHERE rule_type = "
"'hash_blacklist' AND rule_target = ? AND operation = ? AND enabled = " "'blacklist' AND pattern_type = 'hash' AND pattern_value = ? LIMIT 1";
"1 ORDER BY priority LIMIT 1";
rc = sqlite3_prepare_v2(db, hash_blacklist_sql, -1, &stmt, NULL); rc = sqlite3_prepare_v2(db, hash_blacklist_sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) { if (rc == SQLITE_OK) {
sqlite3_bind_text(stmt, 1, resource_hash, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 1, resource_hash, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, operation ? operation : "", -1, SQLITE_STATIC);
if (sqlite3_step(stmt) == SQLITE_ROW) { if (sqlite3_step(stmt) == SQLITE_ROW) {
const char *description = (const char *)sqlite3_column_text(stmt, 1); const char *action = (const char *)sqlite3_column_text(stmt, 1);
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 2 FAILED - " validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 2 FAILED - "
"Hash blacklisted\n"); "Hash blacklisted\n");
char hash_blacklist_msg[256]; char hash_blacklist_msg[256];
sprintf( sprintf(
hash_blacklist_msg, hash_blacklist_msg,
"VALIDATOR_DEBUG: RULES ENGINE - Hash blacklist rule matched: %s\n", "VALIDATOR_DEBUG: RULES ENGINE - Hash blacklist rule matched: action=%s\n",
description ? description : "Unknown"); action ? action : "deny");
validator_debug_log(hash_blacklist_msg); validator_debug_log(hash_blacklist_msg);
// Set specific violation details for status code mapping // Set specific violation details for status code mapping
strcpy(g_last_rule_violation.violation_type, "hash_blacklist"); strcpy(g_last_rule_violation.violation_type, "hash_blacklist");
sprintf(g_last_rule_violation.reason, "%s: File hash blacklisted", sprintf(g_last_rule_violation.reason, "File hash blacklisted: %s",
description ? description : "TEST_HASH_BLACKLIST"); action ? action : "HASH_BLACKLIST");
sqlite3_finalize(stmt); sqlite3_finalize(stmt);
sqlite3_close(db); sqlite3_close(db);
@@ -703,22 +699,20 @@ static int check_database_auth_rules(const char *pubkey, const char *operation,
// Step 3: Check pubkey whitelist // Step 3: Check pubkey whitelist
const char *whitelist_sql = const char *whitelist_sql =
"SELECT rule_type, description FROM auth_rules WHERE rule_type = " "SELECT rule_type, action FROM auth_rules WHERE rule_type = "
"'pubkey_whitelist' AND rule_target = ? AND operation = ? AND enabled = " "'whitelist' AND pattern_type = 'pubkey' AND pattern_value = ? LIMIT 1";
"1 ORDER BY priority LIMIT 1";
rc = sqlite3_prepare_v2(db, whitelist_sql, -1, &stmt, NULL); rc = sqlite3_prepare_v2(db, whitelist_sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) { if (rc == SQLITE_OK) {
sqlite3_bind_text(stmt, 1, pubkey, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 1, pubkey, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, operation ? operation : "", -1, SQLITE_STATIC);
if (sqlite3_step(stmt) == SQLITE_ROW) { if (sqlite3_step(stmt) == SQLITE_ROW) {
const char *description = (const char *)sqlite3_column_text(stmt, 1); const char *action = (const char *)sqlite3_column_text(stmt, 1);
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 3 PASSED - " validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 3 PASSED - "
"Pubkey whitelisted\n"); "Pubkey whitelisted\n");
char whitelist_msg[256]; char whitelist_msg[256];
sprintf(whitelist_msg, sprintf(whitelist_msg,
"VALIDATOR_DEBUG: RULES ENGINE - Whitelist rule matched: %s\n", "VALIDATOR_DEBUG: RULES ENGINE - Whitelist rule matched: action=%s\n",
description ? description : "Unknown"); action ? action : "allow");
validator_debug_log(whitelist_msg); validator_debug_log(whitelist_msg);
sqlite3_finalize(stmt); sqlite3_finalize(stmt);
sqlite3_close(db); sqlite3_close(db);
@@ -731,12 +725,10 @@ static int check_database_auth_rules(const char *pubkey, const char *operation,
// Step 4: Check if any whitelist rules exist - if yes, deny by default // Step 4: Check if any whitelist rules exist - if yes, deny by default
const char *whitelist_exists_sql = const char *whitelist_exists_sql =
"SELECT COUNT(*) FROM auth_rules WHERE rule_type = 'pubkey_whitelist' " "SELECT COUNT(*) FROM auth_rules WHERE rule_type = 'whitelist' "
"AND operation = ? AND enabled = 1 LIMIT 1"; "AND pattern_type = 'pubkey' LIMIT 1";
rc = sqlite3_prepare_v2(db, whitelist_exists_sql, -1, &stmt, NULL); rc = sqlite3_prepare_v2(db, whitelist_exists_sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) { if (rc == SQLITE_OK) {
sqlite3_bind_text(stmt, 1, operation ? operation : "", -1, SQLITE_STATIC);
if (sqlite3_step(stmt) == SQLITE_ROW) { if (sqlite3_step(stmt) == SQLITE_ROW) {
int whitelist_count = sqlite3_column_int(stmt, 0); int whitelist_count = sqlite3_column_int(stmt, 0);
if (whitelist_count > 0) { if (whitelist_count > 0) {

View File

@@ -20,19 +20,18 @@ set -e # Exit on any error
# CONFIGURATION # CONFIGURATION
# ======================================================================= # =======================================================================
# Test mode credentials (provided by user) # Test mode credentials (from current relay startup)
ADMIN_PRIVKEY="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" ADMIN_PRIVKEY="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
ADMIN_PUBKEY="6a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb3" ADMIN_PUBKEY="6a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb3"
RELAY_PUBKEY="4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa" RELAY_PUBKEY="4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa"
# Server configuration # Server configuration
RELAY_HOST="localhost" RELAY_HOST="127.0.0.1"
RELAY_PORT="8888" RELAY_PORT="8888"
RELAY_URL="ws://${RELAY_HOST}:${RELAY_PORT}" RELAY_URL="ws://${RELAY_HOST}:${RELAY_PORT}"
# Test configuration # Test configuration
TIMEOUT=5 TIMEOUT=5
LOG_FILE="whitelist_blacklist_test.log"
TEMP_DIR="/tmp/c_relay_test_$$" TEMP_DIR="/tmp/c_relay_test_$$"
# Color codes for output # Color codes for output
@@ -53,23 +52,23 @@ TESTS_FAILED=0
# ======================================================================= # =======================================================================
log() { log() {
echo -e "${BLUE}[$(date '+%H:%M:%S')]${RESET} $1" | tee -a "$LOG_FILE" echo -e "${BLUE}[$(date '+%H:%M:%S')]${RESET} $1"
} }
log_success() { log_success() {
echo -e "${GREEN}[SUCCESS]${RESET} $1" | tee -a "$LOG_FILE" echo -e "${GREEN}[SUCCESS]${RESET} $1"
} }
log_error() { log_error() {
echo -e "${RED}[ERROR]${RESET} $1" | tee -a "$LOG_FILE" echo -e "${RED}[ERROR]${RESET} $1"
} }
log_warning() { log_warning() {
echo -e "${YELLOW}[WARNING]${RESET} $1" | tee -a "$LOG_FILE" echo -e "${YELLOW}[WARNING]${RESET} $1"
} }
log_info() { log_info() {
echo -e "${BLUE}[INFO]${RESET} $1" | tee -a "$LOG_FILE" echo -e "${BLUE}[INFO]${RESET} $1"
} }
increment_test() { increment_test() {
@@ -79,11 +78,15 @@ increment_test() {
pass_test() { pass_test() {
TESTS_PASSED=$((TESTS_PASSED + 1)) TESTS_PASSED=$((TESTS_PASSED + 1))
log_success "Test $TESTS_RUN: PASSED - $1" log_success "Test $TESTS_RUN: PASSED - $1"
echo ""
echo ""
} }
fail_test() { fail_test() {
TESTS_FAILED=$((TESTS_FAILED + 1)) TESTS_FAILED=$((TESTS_FAILED + 1))
log_error "Test $TESTS_RUN: FAILED - $1" log_error "Test $TESTS_RUN: FAILED - $1"
echo ""
echo ""
} }
# Generate test keypairs # Generate test keypairs
@@ -123,14 +126,21 @@ send_websocket_message() {
local expected_response="$2" local expected_response="$2"
local timeout="${3:-$TIMEOUT}" local timeout="${3:-$TIMEOUT}"
log_info "Sending WebSocket message: ${message:0:100}..." # Use websocat to send message and capture response (following pattern from tests/1_nip_test.sh)
# Use wscat to send message and capture response
local response="" local response=""
if command -v wscat &> /dev/null; then if command -v websocat &> /dev/null; then
response=$(echo "$message" | timeout "$timeout" wscat -c "$RELAY_URL" 2>/dev/null | head -1) # Capture output from websocat (following working pattern from 1_nip_test.sh)
response=$(echo "$message" | timeout "$timeout" websocat "$RELAY_URL" 2>&1 || echo "Connection failed")
# Check if connection failed
if [[ "$response" == *"Connection failed"* ]]; then
log_error "Failed to connect to relay"
return 1
fi
else else
log_error "wscat not found - required for WebSocket testing" log_error "websocat not found - required for WebSocket testing"
log_error "Please install websocat for WebSocket communication"
return 1 return 1
fi fi
@@ -147,13 +157,12 @@ send_auth_rule_event() {
log_info "Creating auth rule event: $action $rule_type $pattern_type ${pattern_value:0:16}..." log_info "Creating auth rule event: $action $rule_type $pattern_type ${pattern_value:0:16}..."
# Create the auth rule event using nak - match the working NIP-42 pattern # Create the auth rule event using nak with correct tag format
# Server expects proper key=value tags for auth rules
# Using Kind 23456 (ephemeral auth rules management) - no d tag needed
local event_json local event_json
event_json=$(nak event -k 33335 --content "{\"action\":\"$action\",\"description\":\"$description\"}" \ event_json=$(nak event -k 23456 --content "{\"action\":\"$action\",\"description\":\"$description\"}" \
-t "d=$RELAY_PUBKEY" \ -t "$rule_type=$pattern_type" -t "pattern_value=$pattern_value" \
-t "$rule_type=$pattern_type" \
-t "pattern=$pattern_value" \
-t "action=$action" \
--sec "$ADMIN_PRIVKEY" 2>/dev/null) --sec "$ADMIN_PRIVKEY" 2>/dev/null)
if [ $? -ne 0 ] || [ -z "$event_json" ]; then if [ $? -ne 0 ] || [ -z "$event_json" ]; then
@@ -161,7 +170,7 @@ send_auth_rule_event() {
return 1 return 1
fi fi
# Send the event using nak directly to relay (more reliable than wscat) # Send the event using nak directly to relay (more reliable than websocat)
log_info "Publishing auth rule event to relay..." log_info "Publishing auth rule event to relay..."
local result local result
result=$(echo "$event_json" | timeout 10s nak event "$RELAY_URL" 2>&1) result=$(echo "$event_json" | timeout 10s nak event "$RELAY_URL" 2>&1)
@@ -179,6 +188,40 @@ send_auth_rule_event() {
fi fi
} }
# Clear all auth rules using the new system command functionality
clear_all_auth_rules() {
log_info "Clearing all existing auth rules..."
# Create system command event to clear all auth rules
# Using Kind 23456 (ephemeral auth rules management)
local event_json
event_json=$(nak event -k 23456 --content "{\"action\":\"clear_all\"}" \
-t "system_command=clear_all_auth_rules" \
--sec "$ADMIN_PRIVKEY" 2>/dev/null)
if [ $? -ne 0 ] || [ -z "$event_json" ]; then
log_error "Failed to create clear auth rules event with nak"
return 1
fi
# Send the event using nak directly to relay
log_info "Sending clear all auth rules command..."
local result
result=$(echo "$event_json" | timeout 10s nak event "$RELAY_URL" 2>&1)
local exit_code=$?
log_info "Clear auth rules result: $result"
# Check if response indicates success
if [ $exit_code -eq 0 ] && echo "$result" | grep -q -i "success\|OK.*true\|published"; then
log_success "All auth rules cleared successfully"
return 0
else
log_error "Failed to clear auth rules: $result (exit code: $exit_code)"
return 1
fi
}
# Test event publishing with a specific key # Test event publishing with a specific key
test_event_publishing() { test_event_publishing() {
local test_privkey="$1" local test_privkey="$1"
@@ -198,7 +241,7 @@ test_event_publishing() {
return 1 return 1
fi fi
# Send the event using nak directly (more reliable than wscat) # Send the event using nak directly (more reliable than websocat)
log_info "Publishing test event to relay..." log_info "Publishing test event to relay..."
local result local result
result=$(echo "$test_event" | timeout 10s nak event "$RELAY_URL" 2>&1) result=$(echo "$test_event" | timeout 10s nak event "$RELAY_URL" 2>&1)
@@ -236,9 +279,6 @@ setup_test_environment() {
# Create temporary directory # Create temporary directory
mkdir -p "$TEMP_DIR" mkdir -p "$TEMP_DIR"
# Clear log file
echo "=== C-Relay Whitelist/Blacklist Test Started at $(date) ===" > "$LOG_FILE"
# Check if required tools are available - like NIP-42 test # Check if required tools are available - like NIP-42 test
log_info "Checking dependencies..." log_info "Checking dependencies..."
@@ -258,9 +298,10 @@ setup_test_environment() {
exit 1 exit 1
fi fi
if ! command -v wscat &> /dev/null; then if ! command -v websocat &> /dev/null; then
log_warning "wscat not found. Some WebSocket tests may be limited" log_error "websocat not found - required for WebSocket testing"
log_warning "Install with: npm install -g wscat" log_error "Please install websocat for WebSocket communication"
exit 1
fi fi
log_success "Dependencies check complete" log_success "Dependencies check complete"
@@ -283,10 +324,10 @@ test_admin_authentication() {
log "Test $TESTS_RUN: Admin Authentication" log "Test $TESTS_RUN: Admin Authentication"
# Create a simple configuration event to test admin authentication # Create a simple configuration event to test admin authentication
local content="Testing admin authentication" # Using Kind 23455 (ephemeral configuration management) - no d tag needed
local content="{\"action\":\"set\",\"description\":\"Testing admin authentication\"}"
local config_event local config_event
config_event=$(nak event -k 33334 --content "$content" \ config_event=$(nak event -k 23455 --content "$content" \
-t "d=$RELAY_PUBKEY" \
-t "test_auth=true" \ -t "test_auth=true" \
--sec "$ADMIN_PRIVKEY" 2>/dev/null) --sec "$ADMIN_PRIVKEY" 2>/dev/null)
@@ -295,25 +336,11 @@ test_admin_authentication() {
return return
fi fi
# DEBUG: Print the full event that will be sent
log_info "=== DEBUG: Full admin event being sent ==="
echo "$config_event" | jq . 2>/dev/null || echo "$config_event"
log_info "=== END DEBUG EVENT ==="
# Send admin event # Send admin event
local message="[\"EVENT\",$config_event]" local message="[\"EVENT\",$config_event]"
log_info "=== DEBUG: Full WebSocket message ==="
echo "$message"
log_info "=== END DEBUG MESSAGE ==="
local response local response
response=$(send_websocket_message "$message" "OK" 10) response=$(send_websocket_message "$message" "OK" 10)
# DEBUG: Print the full response from server
log_info "=== DEBUG: Full server response ==="
echo "$response"
log_info "=== END DEBUG RESPONSE ==="
if echo "$response" | grep -q '"OK".*true'; then if echo "$response" | grep -q '"OK".*true'; then
pass_test "Admin authentication successful" pass_test "Admin authentication successful"
else else
@@ -321,11 +348,65 @@ test_admin_authentication() {
fi fi
} }
# Test 2: Basic Whitelist Functionality # Test 2: Auth Rules Storage and Query Test
test_auth_rules_storage_query() {
increment_test
log "Test $TESTS_RUN: Auth Rules Storage and Query Test"
# Clear all existing rules to start fresh
clear_all_auth_rules
# Add a simple blacklist rule
log_info "Adding test blacklist rule..."
if send_auth_rule_event "add" "blacklist" "pubkey" "$TEST1_PUBKEY" "Test storage blacklist entry"; then
log_success "Auth rule added successfully"
# Wait a moment for rule to be processed
sleep 1
# Query all auth rules using admin query
log_info "Querying all auth rules..."
local query_event
query_event=$(nak event -k 23456 --content "{\"action\":\"list_all\"}" \
-t "auth_query=list_all" \
--sec "$ADMIN_PRIVKEY" 2>/dev/null)
if [ $? -ne 0 ] || [ -z "$query_event" ]; then
fail_test "Failed to create auth query event"
return
fi
# Send the query event
log_info "Sending auth query to relay..."
local query_result
query_result=$(echo "$query_event" | timeout 10s nak event "$RELAY_URL" 2>&1)
local exit_code=$?
log_info "Auth query result: $query_result"
# Check if we got a response and if it contains our test rule
if [ $exit_code -eq 0 ]; then
if echo "$query_result" | grep -q "$TEST1_PUBKEY"; then
pass_test "Auth rule storage and query working - found test rule in query results"
else
fail_test "Auth rule not found in query results - rule may not have been stored"
fi
else
fail_test "Auth query failed: $query_result"
fi
else
fail_test "Failed to add auth rule for storage test"
fi
}
# Test 3: Basic Whitelist Functionality
test_basic_whitelist() { test_basic_whitelist() {
increment_test increment_test
log "Test $TESTS_RUN: Basic Whitelist Functionality" log "Test $TESTS_RUN: Basic Whitelist Functionality"
# Clear all existing rules to start fresh
clear_all_auth_rules
# Add TEST1 pubkey to whitelist # Add TEST1 pubkey to whitelist
if send_auth_rule_event "add" "whitelist" "pubkey" "$TEST1_PUBKEY" "Test whitelist entry"; then if send_auth_rule_event "add" "whitelist" "pubkey" "$TEST1_PUBKEY" "Test whitelist entry"; then
# Test that whitelisted pubkey can publish # Test that whitelisted pubkey can publish
@@ -339,11 +420,14 @@ test_basic_whitelist() {
fi fi
} }
# Test 3: Basic Blacklist Functionality # Test 4: Basic Blacklist Functionality
test_basic_blacklist() { test_basic_blacklist() {
increment_test increment_test
log "Test $TESTS_RUN: Basic Blacklist Functionality" log "Test $TESTS_RUN: Basic Blacklist Functionality"
# Clear all existing rules to start fresh
clear_all_auth_rules
# Add TEST2 pubkey to blacklist # Add TEST2 pubkey to blacklist
if send_auth_rule_event "add" "blacklist" "pubkey" "$TEST2_PUBKEY" "Test blacklist entry"; then if send_auth_rule_event "add" "blacklist" "pubkey" "$TEST2_PUBKEY" "Test blacklist entry"; then
# Test that blacklisted pubkey cannot publish # Test that blacklisted pubkey cannot publish
@@ -357,11 +441,20 @@ test_basic_blacklist() {
fi fi
} }
# Test 4: Rule Removal # Test 5: Rule Removal
test_rule_removal() { test_rule_removal() {
increment_test increment_test
log "Test $TESTS_RUN: Rule Removal" log "Test $TESTS_RUN: Rule Removal"
# Clear all existing rules to start fresh
clear_all_auth_rules
# First add TEST2 to blacklist to test removal
if ! send_auth_rule_event "add" "blacklist" "pubkey" "$TEST2_PUBKEY" "Test blacklist for removal"; then
fail_test "Failed to add pubkey to blacklist for removal test"
return
fi
# Remove TEST2 from blacklist # Remove TEST2 from blacklist
if send_auth_rule_event "remove" "blacklist" "pubkey" "$TEST2_PUBKEY" "Remove test blacklist entry"; then if send_auth_rule_event "remove" "blacklist" "pubkey" "$TEST2_PUBKEY" "Remove test blacklist entry"; then
# Test that previously blacklisted pubkey can now publish # Test that previously blacklisted pubkey can now publish
@@ -375,11 +468,14 @@ test_rule_removal() {
fi fi
} }
# Test 5: Multiple Users Scenario # Test 6: Multiple Users Scenario
test_multiple_users() { test_multiple_users() {
increment_test increment_test
log "Test $TESTS_RUN: Multiple Users Scenario" log "Test $TESTS_RUN: Multiple Users Scenario"
# Clear all existing rules to start fresh
clear_all_auth_rules
# Add TEST1 to whitelist and TEST3 to blacklist # Add TEST1 to whitelist and TEST3 to blacklist
local success_count=0 local success_count=0
@@ -408,11 +504,14 @@ test_multiple_users() {
fi fi
} }
# Test 6: Priority Testing (Blacklist vs Whitelist) # Test 7: Priority Testing (Blacklist vs Whitelist)
test_priority_rules() { test_priority_rules() {
increment_test increment_test
log "Test $TESTS_RUN: Priority Rules Testing" log "Test $TESTS_RUN: Priority Rules Testing"
# Clear all existing rules to start fresh
clear_all_auth_rules
# Add same pubkey to both whitelist and blacklist # Add same pubkey to both whitelist and blacklist
local setup_success=0 local setup_success=0
@@ -438,11 +537,14 @@ test_priority_rules() {
fi fi
} }
# Test 7: Hash-based Blacklist # Test 8: Hash-based Blacklist
test_hash_blacklist() { test_hash_blacklist() {
increment_test increment_test
log "Test $TESTS_RUN: Hash-based Blacklist" log "Test $TESTS_RUN: Hash-based Blacklist"
# Clear all existing rules to start fresh
clear_all_auth_rules
# Create a test event to get its hash # Create a test event to get its hash
local test_content="Content to be blacklisted by hash" local test_content="Content to be blacklisted by hash"
local test_event local test_event
@@ -482,11 +584,14 @@ test_hash_blacklist() {
fi fi
} }
# Test 8: WebSocket Connection Behavior # Test 9: WebSocket Connection Behavior
test_websocket_behavior() { test_websocket_behavior() {
increment_test increment_test
log "Test $TESTS_RUN: WebSocket Connection Behavior" log "Test $TESTS_RUN: WebSocket Connection Behavior"
# Clear all existing rules to start fresh
clear_all_auth_rules
# Test that the WebSocket connection handles multiple rapid requests # Test that the WebSocket connection handles multiple rapid requests
local rapid_success_count=0 local rapid_success_count=0
@@ -516,11 +621,14 @@ test_websocket_behavior() {
fi fi
} }
# Test 9: Rule Persistence Verification # Test 10: Rule Persistence Verification
test_rule_persistence() { test_rule_persistence() {
increment_test increment_test
log "Test $TESTS_RUN: Rule Persistence Verification" log "Test $TESTS_RUN: Rule Persistence Verification"
# Clear all existing rules to start fresh
clear_all_auth_rules
# Add a rule, then verify it persists by testing enforcement # Add a rule, then verify it persists by testing enforcement
if send_auth_rule_event "add" "blacklist" "pubkey" "$TEST3_PUBKEY" "Persistence test blacklist"; then if send_auth_rule_event "add" "blacklist" "pubkey" "$TEST3_PUBKEY" "Persistence test blacklist"; then
# Wait a moment for rule to be processed # Wait a moment for rule to be processed
@@ -546,7 +654,7 @@ test_rule_persistence() {
fi fi
} }
# Test 10: Cleanup and Final Verification # Test 11: Cleanup and Final Verification
test_cleanup_verification() { test_cleanup_verification() {
increment_test increment_test
log "Test $TESTS_RUN: Cleanup and Final Verification" log "Test $TESTS_RUN: Cleanup and Final Verification"
@@ -589,10 +697,11 @@ run_all_tests() {
# Setup # Setup
setup_test_environment setup_test_environment
# Run only test 1 for debugging admin authentication # Clear all auth rules before starting tests
test_admin_authentication clear_all_auth_rules
# Comment out other tests for now to focus on debugging # test_admin_authentication
# test_auth_rules_storage_query
# test_basic_whitelist # test_basic_whitelist
# test_basic_blacklist # test_basic_blacklist
# test_rule_removal # test_rule_removal
@@ -648,8 +757,8 @@ main() {
echo -e "${BLUE}===============================================${RESET}" echo -e "${BLUE}===============================================${RESET}"
echo "" echo ""
# Check if relay is running - use the same method we verified manually # Check if relay is running - using websocat like the working tests
if ! echo '["REQ","connection_test",{}]' | timeout 5 wscat -c "$RELAY_URL" >/dev/null 2>&1; then if ! echo '["REQ","connection_test",{}]' | timeout 5 websocat "$RELAY_URL" >/dev/null 2>&1; then
log_error "Cannot connect to relay at $RELAY_URL" log_error "Cannot connect to relay at $RELAY_URL"
log_error "Please ensure the C-Relay server is running in test mode" log_error "Please ensure the C-Relay server is running in test mode"
exit 1 exit 1
@@ -661,12 +770,10 @@ main() {
if run_all_tests; then if run_all_tests; then
echo "" echo ""
log_success "All whitelist/blacklist tests completed successfully!" log_success "All whitelist/blacklist tests completed successfully!"
echo -e "Test log saved to: ${YELLOW}$LOG_FILE${RESET}"
exit 0 exit 0
else else
echo "" echo ""
log_error "Some tests failed. Check the log for details." log_error "Some tests failed."
echo -e "Test log saved to: ${YELLOW}$LOG_FILE${RESET}"
exit 1 exit 1
fi fi
} }

View File

@@ -1,21 +1,27 @@
=== C-Relay Whitelist/Blacklist Test Started at Tue Sep 23 11:20:40 AM EDT 2025 === === C-Relay Whitelist/Blacklist Test Started at Thu Sep 25 07:28:40 AM EDT 2025 ===
[INFO] Checking dependencies... [INFO] Checking dependencies...
[SUCCESS] Dependencies check complete [SUCCESS] Dependencies check complete
[INFO] Generated keypair for TEST1: pubkey=eab7cac03049d07f... [INFO] Generated keypair for TEST1: pubkey=36e6521000b2ddda...
[INFO] Generated keypair for TEST2: pubkey=4e07a99f656d5301... [INFO] Generated keypair for TEST2: pubkey=9cdd32f27fffeea8...
[INFO] Generated keypair for TEST3: pubkey=bf48b836426805cb... [INFO] Generated keypair for TEST3: pubkey=e05928b64d3ad54a...
[SUCCESS] Test environment setup complete [SUCCESS] Test environment setup complete
[11:20:41] Test 1: Admin Authentication [07:28:42] Test 1: Admin Authentication
[INFO] === DEBUG: Full admin event being sent === [INFO] === DEBUG: Full admin event being sent ===
[INFO] === END DEBUG EVENT === [INFO] === END DEBUG EVENT ===
[INFO] === DEBUG: Full WebSocket message === [INFO] === DEBUG: Full WebSocket message ===
[INFO] === END DEBUG MESSAGE === [INFO] === END DEBUG MESSAGE ===
[INFO] Sending WebSocket message: ["EVENT",{"kind":33334,"id":"ce73fa326eb558505742770eb927a50edc16a69512089939f76da90c7ca5291f","pubk... [INFO] Sending WebSocket message (full):
[INFO] ["EVENT",{"kind":33334,"id":"ba07a9d01ef3bf8c424eb5ecd8a162980e5d596f3c8520ea59c4cf80961a347a","pubkey":"6a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb3","created_at":1758799722,"tags":[["d","4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa"],["test_auth","true"]],"content":"Testing admin authentication","sig":"b8f40558060f402da232f3cc2ff72ea98257a3e3d74bc5c4bebd6fbd24d8d258138f879795b6089c01c4afa4c64088306dc917fdcad7b054d3c12513581b9228"}]
[INFO] WebSocket response (full):
[INFO]
[INFO] === DEBUG: Full server response === [INFO] === DEBUG: Full server response ===
[INFO] === END DEBUG RESPONSE === [INFO] === END DEBUG RESPONSE ===
[ERROR] Test 1: FAILED - Admin authentication failed: [INFO] Sending WebSocket message: ["EVENT",{"kind":33334,"id":"ce73fa326eb558505742770eb927a50edc16a69512089939f76da90c7ca5291f","pubk... [ERROR] Test 1: FAILED - Admin authentication failed: [INFO] Sending WebSocket message (full):
[INFO] ["EVENT",{"kind":33334,"id":"ba07a9d01ef3bf8c424eb5ecd8a162980e5d596f3c8520ea59c4cf80961a347a","pubkey":"6a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb3","created_at":1758799722,"tags":[["d","4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa"],["test_auth","true"]],"content":"Testing admin authentication","sig":"b8f40558060f402da232f3cc2ff72ea98257a3e3d74bc5c4bebd6fbd24d8d258138f879795b6089c01c4afa4c64088306dc917fdcad7b054d3c12513581b9228"}]
[INFO] WebSocket response (full):
[INFO]
[ERROR] 1 out of 1 tests failed. [ERROR] 1 out of 1 tests failed.
[ERROR] Some tests failed. Check the log for details. [ERROR] Some tests failed. Check the log for details.
[11:20:42] Cleaning up test environment... [07:28:42] Cleaning up test environment...
[INFO] Temporary directory removed: /tmp/c_relay_test_1773069 [INFO] Temporary directory removed: /tmp/c_relay_test_184904
[11:20:42] Test cleanup completed. [07:28:42] Test cleanup completed.