# SQL Query Admin API Design
## Overview
This document describes the design for a general-purpose SQL query interface for the C-Relay admin API. This allows administrators to execute read-only SQL queries against the relay database through cryptographically signed kind 23456 events with NIP-44 encrypted command arrays.
## Security Model
### Authentication
- All queries must be sent as kind 23456 events with NIP-44 encrypted content
- Events must be signed by the admin's private key
- Admin pubkey verified against `config.admin_pubkey`
- Follows the same authentication pattern as existing admin commands
### Query Restrictions
While authentication is cryptographically secure, we implement defensive safeguards:
1. **Read-Only Enforcement**
- Only SELECT statements allowed
- Block: INSERT, UPDATE, DELETE, DROP, CREATE, ALTER, PRAGMA (write operations)
- Allow: SELECT, WITH (for CTEs)
2. **Resource Limits**
- Query timeout: 5 seconds (configurable)
- Result row limit: 1000 rows (configurable)
- Result size limit: 1MB (configurable)
3. **Query Logging**
- All queries logged with timestamp, admin pubkey, execution time
- Failed queries logged with error message
## Command Format
### Admin Event Structure (Kind 23456)
```json
{
"id": "event_id",
"pubkey": "admin_public_key",
"created_at": 1234567890,
"kind": 23456,
"content": "AqHBUgcM7dXFYLQuDVzGwMST1G8jtWYyVvYxXhVGEu4nAb4LVw...",
"tags": [
["p", "relay_public_key"]
],
"sig": "event_signature"
}
```
The `content` field contains a NIP-44 encrypted JSON array:
```json
["sql_query", "SELECT * FROM events LIMIT 10"]
```
### Response Format (Kind 23457)
```json
["EVENT", "temp_sub_id", {
"id": "response_event_id",
"pubkey": "relay_public_key",
"created_at": 1234567890,
"kind": 23457,
"content": "nip44_encrypted_content",
"tags": [
["p", "admin_public_key"],
["e", "request_event_id"]
],
"sig": "response_event_signature"
}]
```
The `content` field contains NIP-44 encrypted JSON:
```json
{
"query_type": "sql_query",
"request_id": "request_event_id",
"timestamp": 1234567890,
"query": "SELECT * FROM events LIMIT 10",
"execution_time_ms": 45,
"row_count": 10,
"columns": ["id", "pubkey", "created_at", "kind", "content"],
"rows": [
["abc123...", "def456...", 1234567890, 1, "Hello world"],
...
]
}
```
**Note:** The response includes the request event ID in two places:
1. **In tags**: `["e", "request_event_id"]` - Standard Nostr convention for event references
2. **In content**: `"request_id": "request_event_id"` - For easy access after decryption
### Error Response Format (Kind 23457)
```json
["EVENT", "temp_sub_id", {
"id": "response_event_id",
"pubkey": "relay_public_key",
"created_at": 1234567890,
"kind": 23457,
"content": "nip44_encrypted_content",
"tags": [
["p", "admin_public_key"],
["e", "request_event_id"]
],
"sig": "response_event_signature"
}]
```
The `content` field contains NIP-44 encrypted JSON:
```json
{
"query_type": "sql_query",
"request_id": "request_event_id",
"timestamp": 1234567890,
"query": "DELETE FROM events",
"status": "error",
"error": "Query blocked: DELETE statements not allowed",
"error_type": "blocked_statement"
}
```
## Available Database Tables and Views
### Core Tables
- **events** - All Nostr events (id, pubkey, created_at, kind, content, tags, sig)
- **config** - Configuration key-value pairs
- **auth_rules** - Authentication and authorization rules
- **subscription_events** - Subscription lifecycle events
- **event_broadcasts** - Event broadcast log
### Useful Views
- **recent_events** - Last 1000 events
- **event_stats** - Event statistics by type
- **configuration_events** - Kind 33334 configuration events
- **subscription_analytics** - Subscription metrics by date
- **active_subscriptions_log** - Currently active subscriptions
- **event_kinds_view** - Event distribution by kind
- **top_pubkeys_view** - Top 10 pubkeys by event count
- **time_stats_view** - Time-based statistics (24h, 7d, 30d)
## Implementation Plan
### Backend (dm_admin.c)
#### 1. Query Validation Function
```c
int validate_sql_query(const char* query, char* error_msg, size_t error_size);
```
- Check for blocked keywords (case-insensitive)
- Validate query syntax (basic checks)
- Return 0 on success, -1 on failure
#### 2. Query Execution Function
```c
char* execute_sql_query(const char* query, char* error_msg, size_t error_size);
```
- Set query timeout using sqlite3_busy_timeout()
- Execute query with row/size limits
- Build JSON response with results
- Log query execution
- Return JSON string or NULL on error
#### 3. Command Handler Integration
Add to `process_dm_admin_command()` in [`dm_admin.c`](src/dm_admin.c:131):
```c
else if (strcmp(command_type, "sql_query") == 0) {
const char* query = get_tag_value(event, "sql_query", 1);
if (!query) {
DEBUG_ERROR("DM Admin: Missing sql_query parameter");
snprintf(error_message, error_size, "invalid: missing SQL query");
} else {
result = handle_sql_query_unified(event, query, error_message, error_size, wsi);
}
}
```
Add unified handler function:
```c
int handle_sql_query_unified(cJSON* event, const char* query,
char* error_message, size_t error_size,
struct lws* wsi) {
// Get request event ID for response correlation
cJSON* request_id_obj = cJSON_GetObjectItem(event, "id");
if (!request_id_obj || !cJSON_IsString(request_id_obj)) {
snprintf(error_message, error_size, "Missing request event ID");
return -1;
}
const char* request_id = cJSON_GetStringValue(request_id_obj);
// Validate query
if (!validate_sql_query(query, error_message, error_size)) {
return -1;
}
// Execute query and include request_id in result
char* result_json = execute_sql_query(query, request_id, error_message, error_size);
if (!result_json) {
return -1;
}
// Send response as kind 23457 event with request ID in tags
cJSON* sender_pubkey_obj = cJSON_GetObjectItem(event, "pubkey");
if (!sender_pubkey_obj || !cJSON_IsString(sender_pubkey_obj)) {
free(result_json);
snprintf(error_message, error_size, "Missing sender pubkey");
return -1;
}
const char* sender_pubkey = cJSON_GetStringValue(sender_pubkey_obj);
int send_result = send_admin_response(sender_pubkey, result_json, request_id,
error_message, error_size, wsi);
free(result_json);
return send_result;
}
```
### Frontend (api/index.html)
#### SQL Query Section UI
Add to [`api/index.html`](api/index.html:1):
```html
SQL Query Console
Results
| ${escapeHtml(col)} | `; }); html += '
|---|
| ${cellValue} | `; }); html += '
No results returned
'; } } // Helper function to escape HTML function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } ``` ## Example Queries ### Subscription Statistics ```sql SELECT date, subscriptions_created, subscriptions_ended, avg_duration_seconds, unique_clients FROM subscription_analytics ORDER BY date DESC LIMIT 7; ``` ### Event Distribution by Kind ```sql SELECT kind, count, percentage FROM event_kinds_view ORDER BY count DESC; ``` ### Recent Events by Specific Pubkey ```sql SELECT id, created_at, kind, content FROM events WHERE pubkey = 'abc123...' ORDER BY created_at DESC LIMIT 20; ``` ### Active Subscriptions with Details ```sql SELECT subscription_id, client_ip, events_sent, duration_seconds, filter_json FROM active_subscriptions_log ORDER BY created_at DESC; ``` ### Database Size and Event Count ```sql SELECT (SELECT COUNT(*) FROM events) as total_events, (SELECT COUNT(*) FROM subscription_events) as total_subscriptions, (SELECT COUNT(*) FROM auth_rules WHERE active = 1) as active_rules; ``` ## Configuration Options Add to config table: ```sql INSERT INTO config (key, value, data_type, description, category) VALUES ('sql_query_enabled', 'true', 'boolean', 'Enable SQL query admin API', 'admin'), ('sql_query_timeout', '5', 'integer', 'Query timeout in seconds', 'admin'), ('sql_query_row_limit', '1000', 'integer', 'Maximum rows per query', 'admin'), ('sql_query_size_limit', '1048576', 'integer', 'Maximum result size in bytes', 'admin'), ('sql_query_log_enabled', 'true', 'boolean', 'Log all SQL queries', 'admin'); ``` ## Security Considerations ### What This Protects Against 1. **Unauthorized Access** - Only admin can execute queries (cryptographic verification) 2. **Data Modification** - Read-only enforcement prevents accidental/malicious changes 3. **Resource Exhaustion** - Timeouts and limits prevent DoS 4. **Audit Trail** - All queries logged for security review ### What This Does NOT Protect Against 1. **Admin Compromise** - If admin private key is stolen, attacker has full read access 2. **Information Disclosure** - Admin can read all data (by design) 3. **Complex Attacks** - Sophisticated SQL injection might bypass simple keyword blocking ### Recommendations 1. **Secure Admin Key** - Store admin private key securely, never commit to git 2. **Monitor Query Logs** - Review query logs regularly for suspicious activity 3. **Backup Database** - Regular backups in case of issues 4. **Test Queries** - Test complex queries on development relay first ## Testing Plan ### Unit Tests 1. Query validation (blocked keywords, syntax) 2. Result formatting (JSON structure) 3. Error handling (timeouts, limits) ### Integration Tests 1. Execute queries through NIP-17 DM 2. Verify authentication (admin vs non-admin) 3. Test resource limits (timeout, row limit) 4. Test error responses ### Security Tests 1. Attempt blocked statements (INSERT, DELETE, etc.) 2. Attempt SQL injection patterns 3. Test query timeout with slow queries 4. Test row limit with large result sets ## Future Enhancements 1. **Query History** - Store recent queries for quick re-execution 2. **Query Favorites** - Save frequently used queries 3. **Export Results** - Download results as CSV/JSON 4. **Query Builder** - Visual query builder for common operations 5. **Real-time Updates** - WebSocket updates for live data 6. **Query Sharing** - Share queries with other admins (if multi-admin support added) ## Migration Path ### Phase 1: Backend Implementation 1. Add query validation function 2. Add query execution function 3. Integrate with NIP-17 command handler 4. Add configuration options 5. Add query logging ### Phase 2: Frontend Implementation 1. Add SQL query section to index.html 2. Add query execution JavaScript 3. Add predefined query templates 4. Add results display formatting ### Phase 3: Testing and Documentation 1. Write unit tests 2. Write integration tests 3. Update user documentation 4. Create query examples guide ### Phase 4: Enhancement 1. Add query history 2. Add export functionality 3. Optimize performance 4. Add more predefined templates