Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6dac231040 | ||
|
|
6fd3e531c3 | ||
|
|
c1c05991cf | ||
|
|
ab378e14d1 | ||
|
|
c0f9bf9ef5 | ||
|
|
bc6a7b3f20 | ||
|
|
036b0823b9 | ||
|
|
be99595bde | ||
|
|
01836a4b4c | ||
|
|
9f3b3dd773 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -8,4 +8,5 @@ src/version.h
|
||||
dev-config/
|
||||
db/
|
||||
copy_executable_local.sh
|
||||
nostr_login_lite/
|
||||
nostr_login_lite/
|
||||
style_guide/
|
||||
28
AGENTS.md
28
AGENTS.md
@@ -27,7 +27,7 @@
|
||||
## Critical Integration Issues
|
||||
|
||||
### Event-Based Configuration System
|
||||
- **No traditional config files** - all configuration stored as kind 33334 Nostr events
|
||||
- **No traditional config files** - all configuration stored in config table
|
||||
- Admin private key shown **only once** on first startup
|
||||
- Configuration changes require cryptographically signed events
|
||||
- Database path determined by generated relay pubkey
|
||||
@@ -35,7 +35,7 @@
|
||||
### First-Time Startup Sequence
|
||||
1. Relay generates admin keypair and relay keypair
|
||||
2. Creates database file with relay pubkey as filename
|
||||
3. Stores default configuration as kind 33334 event
|
||||
3. Stores default configuration in config table
|
||||
4. **CRITICAL**: Admin private key displayed once and never stored on disk
|
||||
|
||||
### Port Management
|
||||
@@ -48,20 +48,30 @@
|
||||
- Schema version 4 with JSON tag storage
|
||||
- **Critical**: Event expiration filtering done at application level, not SQL level
|
||||
|
||||
### Configuration Event Structure
|
||||
### Admin API Event Structure
|
||||
```json
|
||||
{
|
||||
"kind": 33334,
|
||||
"content": "C Nostr Relay Configuration",
|
||||
"kind": 23456,
|
||||
"content": "base64_nip44_encrypted_command_array",
|
||||
"tags": [
|
||||
["d", "<relay_pubkey>"],
|
||||
["relay_description", "value"],
|
||||
["max_subscriptions_per_client", "25"],
|
||||
["pow_min_difficulty", "16"]
|
||||
["p", "<relay_pubkey>"]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Configuration Commands** (encrypted in content):
|
||||
- `["relay_description", "My Relay"]`
|
||||
- `["max_subscriptions_per_client", "25"]`
|
||||
- `["pow_min_difficulty", "16"]`
|
||||
|
||||
**Auth Rule Commands** (encrypted in content):
|
||||
- `["blacklist", "pubkey", "hex_pubkey_value"]`
|
||||
- `["whitelist", "pubkey", "hex_pubkey_value"]`
|
||||
|
||||
**Query Commands** (encrypted in content):
|
||||
- `["auth_query", "all"]`
|
||||
- `["system_command", "system_status"]`
|
||||
|
||||
### Process Management
|
||||
```bash
|
||||
# Kill existing relay processes
|
||||
|
||||
180
README.md
180
README.md
@@ -22,4 +22,184 @@ Do NOT modify the formatting, add emojis, or change the text. Keep the simple fo
|
||||
- [ ] NIP-50: Keywords filter
|
||||
- [ ] 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. All admin commands use **NIP-44 encrypted command arrays** for security and compatibility.
|
||||
|
||||
### 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.
|
||||
|
||||
### Event Structure
|
||||
|
||||
All admin commands use the same unified event structure with NIP-44 encrypted content:
|
||||
|
||||
**Admin Command Event:**
|
||||
```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 representing the command.
|
||||
|
||||
**Admin Response Event:**
|
||||
```json
|
||||
["EVENT", "temp_sub_id", {
|
||||
"id": "response_event_id",
|
||||
"pubkey": "relay_public_key",
|
||||
"created_at": 1234567890,
|
||||
"kind": 23457,
|
||||
"content": "BpKCVhfN8eYtRmPqSvWxZnMkL2gHjUiOp3rTyEwQaS5dFg...",
|
||||
"tags": [
|
||||
["p", "admin_public_key"]
|
||||
],
|
||||
"sig": "response_event_signature"
|
||||
}]
|
||||
```
|
||||
|
||||
The `content` field contains a NIP-44 encrypted JSON response object.
|
||||
|
||||
### Admin Commands
|
||||
|
||||
All commands are sent as NIP-44 encrypted JSON arrays in the event content. The following table lists all available commands:
|
||||
|
||||
| Command Type | Command Format | Description |
|
||||
|--------------|----------------|-------------|
|
||||
| **Configuration Management** |
|
||||
| `config_update` | `["config_update", [{"key": "auth_enabled", "value": "true", "data_type": "boolean", "category": "auth"}, {"key": "relay_description", "value": "My Relay", "data_type": "string", "category": "relay"}, ...]]` | Update relay configuration parameters (supports multiple updates) |
|
||||
| `config_query` | `["config_query", "all"]` | Query all configuration parameters |
|
||||
| **Auth Rules Management** |
|
||||
| `auth_add_blacklist` | `["blacklist", "pubkey", "abc123..."]` | Add pubkey to blacklist |
|
||||
| `auth_add_whitelist` | `["whitelist", "pubkey", "def456..."]` | Add pubkey to whitelist |
|
||||
| `auth_delete_rule` | `["delete_auth_rule", "blacklist", "pubkey", "abc123..."]` | Delete specific auth rule |
|
||||
| `auth_query_all` | `["auth_query", "all"]` | Query all auth rules |
|
||||
| `auth_query_type` | `["auth_query", "whitelist"]` | Query specific rule type |
|
||||
| `auth_query_pattern` | `["auth_query", "pattern", "abc123..."]` | Query specific pattern |
|
||||
| **System Commands** |
|
||||
| `system_clear_auth` | `["system_command", "clear_all_auth_rules"]` | Clear all auth rules |
|
||||
| `system_status` | `["system_command", "system_status"]` | Get system status |
|
||||
|
||||
### 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`)
|
||||
|
||||
### Response Format
|
||||
|
||||
All admin commands return **signed EVENT responses** via WebSocket following standard Nostr protocol. Responses use JSON content with structured data.
|
||||
|
||||
#### Response Examples
|
||||
|
||||
**Success Response:**
|
||||
```json
|
||||
["EVENT", "temp_sub_id", {
|
||||
"id": "response_event_id",
|
||||
"pubkey": "relay_public_key",
|
||||
"created_at": 1234567890,
|
||||
"kind": 23457,
|
||||
"content": "nip44 encrypted:{\"query_type\": \"config_update\", \"status\": \"success\", \"message\": \"Operation completed successfully\", \"timestamp\": 1234567890}",
|
||||
"tags": [
|
||||
["p", "admin_public_key"]
|
||||
],
|
||||
"sig": "response_event_signature"
|
||||
}]
|
||||
```
|
||||
|
||||
**Error Response:**
|
||||
```json
|
||||
["EVENT", "temp_sub_id", {
|
||||
"id": "response_event_id",
|
||||
"pubkey": "relay_public_key",
|
||||
"created_at": 1234567890,
|
||||
"kind": 23457,
|
||||
"content": "nip44 encrypted:{\"query_type\": \"config_update\", \"status\": \"error\", \"error\": \"invalid configuration value\", \"timestamp\": 1234567890}",
|
||||
"tags": [
|
||||
["p", "admin_public_key"]
|
||||
],
|
||||
"sig": "response_event_signature"
|
||||
}]
|
||||
```
|
||||
|
||||
**Auth Rules Query Response:**
|
||||
```json
|
||||
["EVENT", "temp_sub_id", {
|
||||
"id": "response_event_id",
|
||||
"pubkey": "relay_public_key",
|
||||
"created_at": 1234567890,
|
||||
"kind": 23457,
|
||||
"content": "nip44 encrypted:{\"query_type\": \"auth_rules_all\", \"total_results\": 2, \"timestamp\": 1234567890, \"data\": [{\"rule_type\": \"blacklist\", \"pattern_type\": \"pubkey\", \"pattern_value\": \"abc123...\", \"action\": \"allow\"}]}",
|
||||
"tags": [
|
||||
["p", "admin_public_key"]
|
||||
],
|
||||
"sig": "response_event_signature"
|
||||
}]
|
||||
```
|
||||
|
||||
**Configuration Query Response:**
|
||||
```json
|
||||
["EVENT", "temp_sub_id", {
|
||||
"id": "response_event_id",
|
||||
"pubkey": "relay_public_key",
|
||||
"created_at": 1234567890,
|
||||
"kind": 23457,
|
||||
"content": "nip44 encrypted:{\"query_type\": \"config_all\", \"total_results\": 27, \"timestamp\": 1234567890, \"data\": [{\"key\": \"auth_enabled\", \"value\": \"false\", \"data_type\": \"boolean\", \"category\": \"auth\", \"description\": \"Enable NIP-42 authentication\"}, {\"key\": \"relay_description\", \"value\": \"My Relay\", \"data_type\": \"string\", \"category\": \"relay\", \"description\": \"Relay description text\"}]}",
|
||||
"tags": [
|
||||
["p", "admin_public_key"]
|
||||
],
|
||||
"sig": "response_event_signature"
|
||||
}]
|
||||
```
|
||||
|
||||
**Configuration Update Success Response:**
|
||||
```json
|
||||
["EVENT", "temp_sub_id", {
|
||||
"id": "response_event_id",
|
||||
"pubkey": "relay_public_key",
|
||||
"created_at": 1234567890,
|
||||
"kind": 23457,
|
||||
"content": "nip44 encrypted:{\"query_type\": \"config_update\", \"total_results\": 2, \"timestamp\": 1234567890, \"status\": \"success\", \"data\": [{\"key\": \"auth_enabled\", \"value\": \"true\", \"status\": \"updated\"}, {\"key\": \"relay_description\", \"value\": \"My Updated Relay\", \"status\": \"updated\"}]}",
|
||||
"tags": [
|
||||
["p", "admin_public_key"]
|
||||
],
|
||||
"sig": "response_event_signature"
|
||||
}]
|
||||
```
|
||||
|
||||
**Configuration Update Error Response:**
|
||||
```json
|
||||
["EVENT", "temp_sub_id", {
|
||||
"id": "response_event_id",
|
||||
"pubkey": "relay_public_key",
|
||||
"created_at": 1234567890,
|
||||
"kind": 23457,
|
||||
"content": "nip44 encrypted:{\"query_type\": \"config_update\", \"status\": \"error\", \"error\": \"field validation failed: invalid port number '99999' (must be 1-65535)\", \"timestamp\": 1234567890}",
|
||||
"tags": [
|
||||
["p", "admin_public_key"]
|
||||
],
|
||||
"sig": "response_event_signature"
|
||||
}]
|
||||
```
|
||||
|
||||
3377
api/index.html
3377
api/index.html
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@
|
||||
* Two-file architecture:
|
||||
* 1. Load nostr.bundle.js (official nostr-tools bundle)
|
||||
* 2. Load nostr-lite.js (this file - NOSTR_LOGIN_LITE library with CSS-only themes)
|
||||
* Generated on: 2025-09-16T15:52:30.145Z
|
||||
* Generated on: 2025-09-16T22:12:00.192Z
|
||||
*/
|
||||
|
||||
// Verify dependencies are loaded
|
||||
@@ -20,509 +20,10 @@ if (typeof window !== 'undefined') {
|
||||
|
||||
console.log('NOSTR_LOGIN_LITE: Dependencies verified ✓');
|
||||
console.log('NOSTR_LOGIN_LITE: NostrTools available with keys:', Object.keys(window.NostrTools));
|
||||
console.log('NOSTR_LOGIN_LITE: NIP-06 available:', !!window.NostrTools.nip06);
|
||||
console.log('NOSTR_LOGIN_LITE: NIP-46 available:', !!window.NostrTools.nip46);
|
||||
}
|
||||
|
||||
// ===== NIP-46 Extension Integration =====
|
||||
// Add NIP-46 functionality to NostrTools if not already present
|
||||
if (typeof window.NostrTools !== 'undefined' && !window.NostrTools.nip46) {
|
||||
console.log('NOSTR_LOGIN_LITE: Adding NIP-46 extension to NostrTools');
|
||||
|
||||
const { nip44, generateSecretKey, getPublicKey, finalizeEvent, verifyEvent, utils } = window.NostrTools;
|
||||
|
||||
// NIP-05 regex for parsing
|
||||
const NIP05_REGEX = /^(?:([\w.+-]+)@)?([\w_-]+(.[\w_-]+)+)$/;
|
||||
const BUNKER_REGEX = /^bunker:\/\/([0-9a-f]{64})\??([?\/\w:.=&%-]*)$/;
|
||||
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
|
||||
// Event kinds
|
||||
const NostrConnect = 24133;
|
||||
const ClientAuth = 22242;
|
||||
const Handlerinformation = 31990;
|
||||
|
||||
// Fetch implementation
|
||||
let _fetch;
|
||||
try {
|
||||
_fetch = fetch;
|
||||
} catch {
|
||||
_fetch = null;
|
||||
}
|
||||
|
||||
function useFetchImplementation(fetchImplementation) {
|
||||
_fetch = fetchImplementation;
|
||||
}
|
||||
|
||||
// Simple Pool implementation for NIP-46
|
||||
class SimplePool {
|
||||
constructor() {
|
||||
this.relays = new Map();
|
||||
this.subscriptions = new Map();
|
||||
}
|
||||
|
||||
async ensureRelay(url) {
|
||||
if (!this.relays.has(url)) {
|
||||
console.log(`NIP-46: Connecting to relay ${url}`);
|
||||
const ws = new WebSocket(url);
|
||||
const relay = {
|
||||
ws,
|
||||
connected: false,
|
||||
subscriptions: new Map()
|
||||
};
|
||||
|
||||
this.relays.set(url, relay);
|
||||
|
||||
// Wait for connection with proper event handlers
|
||||
await new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
console.error(`NIP-46: Connection timeout for ${url}`);
|
||||
reject(new Error(`Connection timeout to ${url}`));
|
||||
}, 10000); // 10 second timeout
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log(`NIP-46: Successfully connected to relay ${url}, WebSocket state: ${ws.readyState}`);
|
||||
relay.connected = true;
|
||||
clearTimeout(timeout);
|
||||
resolve();
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error(`NIP-46: Failed to connect to ${url}:`, error);
|
||||
clearTimeout(timeout);
|
||||
reject(new Error(`Failed to connect to ${url}: ${error.message || 'Connection failed'}`));
|
||||
};
|
||||
|
||||
ws.onclose = (event) => {
|
||||
console.log(`NIP-46: Disconnected from relay ${url}:`, event.code, event.reason);
|
||||
relay.connected = false;
|
||||
if (this.relays.has(url)) {
|
||||
this.relays.delete(url);
|
||||
}
|
||||
clearTimeout(timeout);
|
||||
reject(new Error(`Connection closed during setup: ${event.reason || 'Unknown reason'}`));
|
||||
};
|
||||
});
|
||||
} else {
|
||||
const relay = this.relays.get(url);
|
||||
// Verify the existing connection is still open
|
||||
if (!relay.connected || relay.ws.readyState !== WebSocket.OPEN) {
|
||||
console.log(`NIP-46: Reconnecting to relay ${url}`);
|
||||
this.relays.delete(url);
|
||||
return await this.ensureRelay(url); // Recursively reconnect
|
||||
}
|
||||
}
|
||||
|
||||
const relay = this.relays.get(url);
|
||||
console.log(`NIP-46: Relay ${url} ready, WebSocket state: ${relay.ws.readyState}`);
|
||||
return relay;
|
||||
}
|
||||
|
||||
subscribe(relays, filters, params = {}) {
|
||||
const subId = Math.random().toString(36).substring(7);
|
||||
|
||||
relays.forEach(async (url) => {
|
||||
try {
|
||||
const relay = await this.ensureRelay(url);
|
||||
|
||||
relay.ws.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data[0] === 'EVENT' && data[1] === subId) {
|
||||
params.onevent?.(data[2]);
|
||||
} else if (data[0] === 'EOSE' && data[1] === subId) {
|
||||
params.oneose?.();
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Failed to parse message:', err);
|
||||
}
|
||||
};
|
||||
|
||||
// Ensure filters is an array
|
||||
const filtersArray = Array.isArray(filters) ? filters : [filters];
|
||||
const reqMsg = JSON.stringify(['REQ', subId, ...filtersArray]);
|
||||
relay.ws.send(reqMsg);
|
||||
|
||||
} catch (err) {
|
||||
console.warn('Failed to connect to relay:', url, err);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
close: () => {
|
||||
relays.forEach(async (url) => {
|
||||
const relay = this.relays.get(url);
|
||||
if (relay?.connected) {
|
||||
relay.ws.send(JSON.stringify(['CLOSE', subId]));
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async publish(relays, event) {
|
||||
console.log(`NIP-46: Publishing event to ${relays.length} relays:`, event);
|
||||
|
||||
const promises = relays.map(async (url) => {
|
||||
try {
|
||||
console.log(`NIP-46: Attempting to publish to ${url}`);
|
||||
const relay = await this.ensureRelay(url);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
console.error(`NIP-46: Publish timeout to ${url}`);
|
||||
reject(new Error(`Publish timeout to ${url}`));
|
||||
}, 10000); // Increased timeout to 10 seconds
|
||||
|
||||
// Set up message handler for this specific event
|
||||
const messageHandler = (msg) => {
|
||||
try {
|
||||
const data = JSON.parse(msg.data);
|
||||
if (data[0] === 'OK' && data[1] === event.id) {
|
||||
clearTimeout(timeout);
|
||||
relay.ws.removeEventListener('message', messageHandler);
|
||||
if (data[2]) {
|
||||
console.log(`NIP-46: Publish success to ${url}:`, data[3]);
|
||||
resolve(data[3]);
|
||||
} else {
|
||||
console.error(`NIP-46: Publish rejected by ${url}:`, data[3]);
|
||||
reject(new Error(`Publish rejected: ${data[3]}`));
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`NIP-46: Error parsing message from ${url}:`, err);
|
||||
clearTimeout(timeout);
|
||||
relay.ws.removeEventListener('message', messageHandler);
|
||||
reject(err);
|
||||
}
|
||||
};
|
||||
|
||||
relay.ws.addEventListener('message', messageHandler);
|
||||
|
||||
// Double-check WebSocket state before sending
|
||||
console.log(`NIP-46: About to publish to ${url}, WebSocket state: ${relay.ws.readyState} (0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED)`);
|
||||
if (relay.ws.readyState === WebSocket.OPEN) {
|
||||
console.log(`NIP-46: Sending event to ${url}`);
|
||||
relay.ws.send(JSON.stringify(['EVENT', event]));
|
||||
} else {
|
||||
console.error(`NIP-46: WebSocket not ready for ${url}, state: ${relay.ws.readyState}`);
|
||||
clearTimeout(timeout);
|
||||
relay.ws.removeEventListener('message', messageHandler);
|
||||
reject(new Error(`WebSocket not ready for ${url}, state: ${relay.ws.readyState}`));
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(`NIP-46: Failed to publish to ${url}:`, err);
|
||||
return Promise.reject(new Error(`Failed to publish to ${url}: ${err.message}`));
|
||||
}
|
||||
});
|
||||
|
||||
const results = await Promise.allSettled(promises);
|
||||
console.log(`NIP-46: Publish results:`, results);
|
||||
return results;
|
||||
}
|
||||
|
||||
async querySync(relays, filter, params = {}) {
|
||||
return new Promise((resolve) => {
|
||||
const events = [];
|
||||
this.subscribe(relays, [filter], {
|
||||
...params,
|
||||
onevent: (event) => events.push(event),
|
||||
oneose: () => resolve(events)
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Bunker URL utilities
|
||||
function toBunkerURL(bunkerPointer) {
|
||||
let bunkerURL = new URL(`bunker://${bunkerPointer.pubkey}`);
|
||||
bunkerPointer.relays.forEach((relay) => {
|
||||
bunkerURL.searchParams.append('relay', relay);
|
||||
});
|
||||
if (bunkerPointer.secret) {
|
||||
bunkerURL.searchParams.set('secret', bunkerPointer.secret);
|
||||
}
|
||||
return bunkerURL.toString();
|
||||
}
|
||||
|
||||
async function parseBunkerInput(input) {
|
||||
let match = input.match(BUNKER_REGEX);
|
||||
if (match) {
|
||||
try {
|
||||
const pubkey = match[1];
|
||||
const qs = new URLSearchParams(match[2]);
|
||||
return {
|
||||
pubkey,
|
||||
relays: qs.getAll('relay'),
|
||||
secret: qs.get('secret')
|
||||
};
|
||||
} catch (_err) {
|
||||
// Continue to NIP-05 parsing
|
||||
}
|
||||
}
|
||||
return queryBunkerProfile(input);
|
||||
}
|
||||
|
||||
async function queryBunkerProfile(nip05) {
|
||||
if (!_fetch) {
|
||||
throw new Error('Fetch implementation not available');
|
||||
}
|
||||
|
||||
const match = nip05.match(NIP05_REGEX);
|
||||
if (!match) return null;
|
||||
|
||||
const [_, name = '_', domain] = match;
|
||||
try {
|
||||
const url = `https://${domain}/.well-known/nostr.json?name=${name}`;
|
||||
const res = await (await _fetch(url, { redirect: 'error' })).json();
|
||||
let pubkey = res.names[name];
|
||||
let relays = res.nip46[pubkey] || [];
|
||||
return { pubkey, relays, secret: null };
|
||||
} catch (_err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// BunkerSigner class
|
||||
class BunkerSigner {
|
||||
constructor(clientSecretKey, bp, params = {}) {
|
||||
if (bp.relays.length === 0) {
|
||||
throw new Error('no relays are specified for this bunker');
|
||||
}
|
||||
|
||||
this.params = params;
|
||||
this.pool = params.pool || new SimplePool();
|
||||
this.secretKey = clientSecretKey;
|
||||
this.conversationKey = nip44.getConversationKey(clientSecretKey, bp.pubkey);
|
||||
this.bp = bp;
|
||||
this.isOpen = false;
|
||||
this.idPrefix = Math.random().toString(36).substring(7);
|
||||
this.serial = 0;
|
||||
this.listeners = {};
|
||||
this.waitingForAuth = {};
|
||||
this.ready = false;
|
||||
this.readyPromise = this.setupSubscription(params);
|
||||
}
|
||||
|
||||
async setupSubscription(params) {
|
||||
console.log('NIP-46: Setting up subscription to relays:', this.bp.relays);
|
||||
const listeners = this.listeners;
|
||||
const waitingForAuth = this.waitingForAuth;
|
||||
const convKey = this.conversationKey;
|
||||
|
||||
// Ensure all relays are connected first
|
||||
await Promise.all(this.bp.relays.map(url => this.pool.ensureRelay(url)));
|
||||
console.log('NIP-46: All relays connected, setting up subscription');
|
||||
|
||||
this.subCloser = this.pool.subscribe(
|
||||
this.bp.relays,
|
||||
[{ kinds: [NostrConnect], authors: [this.bp.pubkey], '#p': [getPublicKey(this.secretKey)] }],
|
||||
{
|
||||
onevent: async (event) => {
|
||||
const o = JSON.parse(nip44.decrypt(event.content, convKey));
|
||||
const { id, result, error } = o;
|
||||
|
||||
if (result === 'auth_url' && waitingForAuth[id]) {
|
||||
delete waitingForAuth[id];
|
||||
if (params.onauth) {
|
||||
params.onauth(error);
|
||||
} else {
|
||||
console.warn(
|
||||
`NIP-46: remote signer ${this.bp.pubkey} tried to send an "auth_url"='${error}' but there was no onauth() callback configured.`
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let handler = listeners[id];
|
||||
if (handler) {
|
||||
if (error) handler.reject(error);
|
||||
else if (result) handler.resolve(result);
|
||||
delete listeners[id];
|
||||
}
|
||||
},
|
||||
onclose: () => {
|
||||
this.subCloser = undefined;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
this.isOpen = true;
|
||||
this.ready = true;
|
||||
console.log('NIP-46: BunkerSigner setup complete and ready');
|
||||
}
|
||||
|
||||
async ensureReady() {
|
||||
if (!this.ready) {
|
||||
console.log('NIP-46: Waiting for BunkerSigner to be ready...');
|
||||
await this.readyPromise;
|
||||
}
|
||||
}
|
||||
|
||||
async close() {
|
||||
this.isOpen = false;
|
||||
this.subCloser?.close();
|
||||
}
|
||||
|
||||
async sendRequest(method, params) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
await this.ensureReady(); // Wait for BunkerSigner to be ready
|
||||
|
||||
if (!this.isOpen) {
|
||||
throw new Error('this signer is not open anymore, create a new one');
|
||||
}
|
||||
if (!this.subCloser) {
|
||||
await this.setupSubscription(this.params);
|
||||
}
|
||||
|
||||
this.serial++;
|
||||
const id = `${this.idPrefix}-${this.serial}`;
|
||||
const encryptedContent = nip44.encrypt(JSON.stringify({ id, method, params }), this.conversationKey);
|
||||
|
||||
const verifiedEvent = finalizeEvent(
|
||||
{
|
||||
kind: NostrConnect,
|
||||
tags: [['p', this.bp.pubkey]],
|
||||
content: encryptedContent,
|
||||
created_at: Math.floor(Date.now() / 1000)
|
||||
},
|
||||
this.secretKey
|
||||
);
|
||||
|
||||
this.listeners[id] = { resolve, reject };
|
||||
this.waitingForAuth[id] = true;
|
||||
|
||||
console.log(`NIP-46: Sending ${method} request with id ${id}`);
|
||||
const publishResults = await this.pool.publish(this.bp.relays, verifiedEvent);
|
||||
// Check if at least one publish succeeded
|
||||
const hasSuccess = publishResults.some(result => result.status === 'fulfilled');
|
||||
if (!hasSuccess) {
|
||||
throw new Error('Failed to publish to any relay');
|
||||
}
|
||||
console.log(`NIP-46: ${method} request sent successfully`);
|
||||
} catch (err) {
|
||||
console.error(`NIP-46: sendRequest ${method} failed:`, err);
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async ping() {
|
||||
let resp = await this.sendRequest('ping', []);
|
||||
if (resp !== 'pong') {
|
||||
throw new Error(`result is not pong: ${resp}`);
|
||||
}
|
||||
}
|
||||
|
||||
async connect() {
|
||||
await this.sendRequest('connect', [this.bp.pubkey, this.bp.secret || '']);
|
||||
}
|
||||
|
||||
async getPublicKey() {
|
||||
if (!this.cachedPubKey) {
|
||||
this.cachedPubKey = await this.sendRequest('get_public_key', []);
|
||||
}
|
||||
return this.cachedPubKey;
|
||||
}
|
||||
|
||||
async signEvent(event) {
|
||||
let resp = await this.sendRequest('sign_event', [JSON.stringify(event)]);
|
||||
let signed = JSON.parse(resp);
|
||||
if (verifyEvent(signed)) {
|
||||
return signed;
|
||||
} else {
|
||||
throw new Error(`event returned from bunker is improperly signed: ${JSON.stringify(signed)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async nip04Encrypt(thirdPartyPubkey, plaintext) {
|
||||
return await this.sendRequest('nip04_encrypt', [thirdPartyPubkey, plaintext]);
|
||||
}
|
||||
|
||||
async nip04Decrypt(thirdPartyPubkey, ciphertext) {
|
||||
return await this.sendRequest('nip04_decrypt', [thirdPartyPubkey, ciphertext]);
|
||||
}
|
||||
|
||||
async nip44Encrypt(thirdPartyPubkey, plaintext) {
|
||||
return await this.sendRequest('nip44_encrypt', [thirdPartyPubkey, plaintext]);
|
||||
}
|
||||
|
||||
async nip44Decrypt(thirdPartyPubkey, ciphertext) {
|
||||
return await this.sendRequest('nip44_decrypt', [thirdPartyPubkey, ciphertext]);
|
||||
}
|
||||
}
|
||||
|
||||
async function createAccount(bunker, params, username, domain, email, localSecretKey = generateSecretKey()) {
|
||||
if (email && !EMAIL_REGEX.test(email)) {
|
||||
throw new Error('Invalid email');
|
||||
}
|
||||
|
||||
let rpc = new BunkerSigner(localSecretKey, bunker.bunkerPointer, params);
|
||||
let pubkey = await rpc.sendRequest('create_account', [username, domain, email || '']);
|
||||
rpc.bp.pubkey = pubkey;
|
||||
await rpc.connect();
|
||||
return rpc;
|
||||
}
|
||||
|
||||
async function fetchBunkerProviders(pool, relays) {
|
||||
const events = await pool.querySync(relays, {
|
||||
kinds: [Handlerinformation],
|
||||
'#k': [NostrConnect.toString()]
|
||||
});
|
||||
|
||||
events.sort((a, b) => b.created_at - a.created_at);
|
||||
|
||||
const validatedBunkers = await Promise.all(
|
||||
events.map(async (event, i) => {
|
||||
try {
|
||||
const content = JSON.parse(event.content);
|
||||
try {
|
||||
if (events.findIndex((ev) => JSON.parse(ev.content).nip05 === content.nip05) !== i) {
|
||||
return undefined;
|
||||
}
|
||||
} catch (err) {
|
||||
// Continue processing
|
||||
}
|
||||
|
||||
const bp = await queryBunkerProfile(content.nip05);
|
||||
if (bp && bp.pubkey === event.pubkey && bp.relays.length) {
|
||||
return {
|
||||
bunkerPointer: bp,
|
||||
nip05: content.nip05,
|
||||
domain: content.nip05.split('@')[1],
|
||||
name: content.name || content.display_name,
|
||||
picture: content.picture,
|
||||
about: content.about,
|
||||
website: content.website,
|
||||
local: false
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
return undefined;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return validatedBunkers.filter((b) => b !== undefined);
|
||||
}
|
||||
|
||||
// Extend NostrTools with NIP-46 functionality
|
||||
window.NostrTools.nip46 = {
|
||||
BunkerSigner,
|
||||
parseBunkerInput,
|
||||
toBunkerURL,
|
||||
queryBunkerProfile,
|
||||
createAccount,
|
||||
fetchBunkerProviders,
|
||||
useFetchImplementation,
|
||||
BUNKER_REGEX,
|
||||
SimplePool
|
||||
};
|
||||
|
||||
console.log('NIP-46 extension loaded successfully');
|
||||
console.log('Available: NostrTools.nip46');
|
||||
}
|
||||
|
||||
// ======================================
|
||||
// NOSTR_LOGIN_LITE Components
|
||||
// ======================================
|
||||
@@ -854,7 +355,7 @@ class Modal {
|
||||
overflow: hidden;
|
||||
`;
|
||||
} else {
|
||||
// Modal content: centered with margin
|
||||
// Modal content: centered with margin, no fixed height
|
||||
modalContent.style.cssText = `
|
||||
position: relative;
|
||||
background: var(--nl-secondary-color);
|
||||
@@ -864,7 +365,6 @@ class Modal {
|
||||
margin: 50px auto;
|
||||
border-radius: var(--nl-border-radius, 15px);
|
||||
border: var(--nl-border-width) solid var(--nl-primary-color);
|
||||
max-height: 600px;
|
||||
overflow: hidden;
|
||||
`;
|
||||
}
|
||||
@@ -929,8 +429,6 @@ class Modal {
|
||||
this.modalBody = document.createElement('div');
|
||||
this.modalBody.style.cssText = `
|
||||
padding: 24px;
|
||||
overflow-y: auto;
|
||||
max-height: 500px;
|
||||
background: transparent;
|
||||
font-family: var(--nl-font-family, 'Courier New', monospace);
|
||||
`;
|
||||
@@ -1019,6 +517,16 @@ class Modal {
|
||||
});
|
||||
}
|
||||
|
||||
// Seed Phrase option - only show if explicitly enabled
|
||||
if (this.options?.methods?.seedphrase === true) {
|
||||
options.push({
|
||||
type: 'seedphrase',
|
||||
title: 'Seed Phrase',
|
||||
description: 'Import from mnemonic seed phrase',
|
||||
icon: '🌱'
|
||||
});
|
||||
}
|
||||
|
||||
// Nostr Connect option (check both 'connect' and 'remote' for compatibility)
|
||||
if (this.options?.methods?.connect !== false && this.options?.methods?.remote !== false) {
|
||||
options.push({
|
||||
@@ -1076,6 +584,27 @@ class Modal {
|
||||
button.style.background = 'var(--nl-secondary-color)';
|
||||
};
|
||||
|
||||
const iconDiv = document.createElement('div');
|
||||
// Replace emoji icons with text-based ones
|
||||
const iconMap = {
|
||||
'🔌': '[EXT]',
|
||||
'🔑': '[KEY]',
|
||||
'🌱': '[SEED]',
|
||||
'🌐': '[NET]',
|
||||
'👁️': '[VIEW]',
|
||||
'📱': '[SMS]'
|
||||
};
|
||||
iconDiv.textContent = iconMap[option.icon] || option.icon;
|
||||
iconDiv.style.cssText = `
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-right: 16px;
|
||||
width: 50px;
|
||||
text-align: center;
|
||||
color: var(--nl-primary-color);
|
||||
font-family: var(--nl-font-family, 'Courier New', monospace);
|
||||
`;
|
||||
|
||||
const contentDiv = document.createElement('div');
|
||||
contentDiv.style.cssText = 'flex: 1; text-align: left;';
|
||||
|
||||
@@ -1099,6 +628,7 @@ class Modal {
|
||||
contentDiv.appendChild(titleDiv);
|
||||
contentDiv.appendChild(descDiv);
|
||||
|
||||
button.appendChild(iconDiv);
|
||||
button.appendChild(contentDiv);
|
||||
this.modalBody.appendChild(button);
|
||||
});
|
||||
@@ -1115,6 +645,9 @@ class Modal {
|
||||
case 'local':
|
||||
this._showLocalKeyScreen();
|
||||
break;
|
||||
case 'seedphrase':
|
||||
this._showSeedPhraseScreen();
|
||||
break;
|
||||
case 'connect':
|
||||
this._showConnectScreen();
|
||||
break;
|
||||
@@ -2159,6 +1692,287 @@ class Modal {
|
||||
this._setAuthMethod('readonly');
|
||||
}
|
||||
|
||||
_showSeedPhraseScreen() {
|
||||
this.modalBody.innerHTML = '';
|
||||
|
||||
const title = document.createElement('h3');
|
||||
title.textContent = 'Import from Seed Phrase';
|
||||
title.style.cssText = 'margin: 0 0 16px 0; font-size: 18px; font-weight: 600;';
|
||||
|
||||
const description = document.createElement('p');
|
||||
description.textContent = 'Enter your 12 or 24-word mnemonic seed phrase to derive Nostr accounts:';
|
||||
description.style.cssText = 'margin-bottom: 12px; color: #6b7280; font-size: 14px;';
|
||||
|
||||
const textarea = document.createElement('textarea');
|
||||
// Remove default placeholder text as requested
|
||||
textarea.placeholder = '';
|
||||
textarea.style.cssText = `
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
padding: 12px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 12px;
|
||||
resize: none;
|
||||
font-family: monospace;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
`;
|
||||
|
||||
// Add real-time mnemonic validation
|
||||
const formatHint = document.createElement('div');
|
||||
formatHint.style.cssText = 'margin-bottom: 16px; font-size: 12px; color: #6b7280; min-height: 16px;';
|
||||
|
||||
textarea.oninput = () => {
|
||||
const value = textarea.value.trim();
|
||||
if (!value) {
|
||||
formatHint.textContent = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const isValid = this._validateMnemonic(value);
|
||||
if (isValid) {
|
||||
const wordCount = value.split(/\s+/).length;
|
||||
formatHint.textContent = `✅ Valid ${wordCount}-word mnemonic detected`;
|
||||
formatHint.style.color = '#059669';
|
||||
} else {
|
||||
formatHint.textContent = '❌ Invalid mnemonic - must be 12 or 24 valid BIP-39 words';
|
||||
formatHint.style.color = '#dc2626';
|
||||
}
|
||||
};
|
||||
|
||||
// Generate new seed phrase button
|
||||
const generateButton = document.createElement('button');
|
||||
generateButton.textContent = 'Generate New Seed Phrase';
|
||||
generateButton.onclick = () => this._generateNewSeedPhrase(textarea, formatHint);
|
||||
generateButton.style.cssText = this._getButtonStyle() + 'margin-bottom: 12px;';
|
||||
|
||||
const importButton = document.createElement('button');
|
||||
importButton.textContent = 'Import Accounts';
|
||||
importButton.onclick = () => this._importFromSeedPhrase(textarea.value);
|
||||
importButton.style.cssText = this._getButtonStyle();
|
||||
|
||||
const backButton = document.createElement('button');
|
||||
backButton.textContent = 'Back';
|
||||
backButton.onclick = () => this._renderLoginOptions();
|
||||
backButton.style.cssText = this._getButtonStyle('secondary') + 'margin-top: 12px;';
|
||||
|
||||
this.modalBody.appendChild(title);
|
||||
this.modalBody.appendChild(description);
|
||||
this.modalBody.appendChild(textarea);
|
||||
this.modalBody.appendChild(formatHint);
|
||||
this.modalBody.appendChild(generateButton);
|
||||
this.modalBody.appendChild(importButton);
|
||||
this.modalBody.appendChild(backButton);
|
||||
}
|
||||
|
||||
_generateNewSeedPhrase(textarea, formatHint) {
|
||||
try {
|
||||
// Check if NIP-06 is available
|
||||
if (!window.NostrTools?.nip06) {
|
||||
throw new Error('NIP-06 not available in bundle');
|
||||
}
|
||||
|
||||
// Generate a random 12-word mnemonic using NostrTools
|
||||
const mnemonic = window.NostrTools.nip06.generateSeedWords();
|
||||
|
||||
// Set the generated mnemonic in the textarea
|
||||
textarea.value = mnemonic;
|
||||
|
||||
// Trigger validation to show it's valid
|
||||
const wordCount = mnemonic.split(/\s+/).length;
|
||||
formatHint.textContent = `✅ Generated valid ${wordCount}-word mnemonic`;
|
||||
formatHint.style.color = '#059669';
|
||||
|
||||
console.log('Generated new seed phrase:', wordCount, 'words');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to generate seed phrase:', error);
|
||||
formatHint.textContent = '❌ Failed to generate seed phrase - NIP-06 not available';
|
||||
formatHint.style.color = '#dc2626';
|
||||
}
|
||||
}
|
||||
|
||||
_validateMnemonic(mnemonic) {
|
||||
try {
|
||||
// Check if NIP-06 is available
|
||||
if (!window.NostrTools?.nip06) {
|
||||
console.error('NIP-06 not available in bundle');
|
||||
return false;
|
||||
}
|
||||
|
||||
const words = mnemonic.trim().split(/\s+/);
|
||||
|
||||
// Must be 12 or 24 words
|
||||
if (words.length !== 12 && words.length !== 24) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try to validate using NostrTools nip06 - this will throw if invalid
|
||||
window.NostrTools.nip06.privateKeyFromSeedWords(mnemonic, '', 0);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log('Mnemonic validation failed:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
_importFromSeedPhrase(mnemonic) {
|
||||
try {
|
||||
const trimmed = mnemonic.trim();
|
||||
if (!trimmed) {
|
||||
throw new Error('Please enter a mnemonic seed phrase');
|
||||
}
|
||||
|
||||
// Validate the mnemonic
|
||||
if (!this._validateMnemonic(trimmed)) {
|
||||
throw new Error('Invalid mnemonic. Please enter a valid 12 or 24-word BIP-39 seed phrase');
|
||||
}
|
||||
|
||||
// Generate accounts 0-5 using NIP-06
|
||||
const accounts = [];
|
||||
for (let i = 0; i < 6; i++) {
|
||||
try {
|
||||
const privateKey = window.NostrTools.nip06.privateKeyFromSeedWords(trimmed, '', i);
|
||||
const publicKey = window.NostrTools.getPublicKey(privateKey);
|
||||
const nsec = window.NostrTools.nip19.nsecEncode(privateKey);
|
||||
const npub = window.NostrTools.nip19.npubEncode(publicKey);
|
||||
|
||||
accounts.push({
|
||||
index: i,
|
||||
privateKey,
|
||||
publicKey,
|
||||
nsec,
|
||||
npub
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Failed to derive account ${i}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
if (accounts.length === 0) {
|
||||
throw new Error('Failed to derive any accounts from seed phrase');
|
||||
}
|
||||
|
||||
console.log(`Successfully derived ${accounts.length} accounts from seed phrase`);
|
||||
this._showAccountSelection(accounts);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Seed phrase import failed:', error);
|
||||
this._showError('Seed phrase import failed: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
_showAccountSelection(accounts) {
|
||||
this.modalBody.innerHTML = '';
|
||||
|
||||
const title = document.createElement('h3');
|
||||
title.textContent = 'Select Account';
|
||||
title.style.cssText = 'margin: 0 0 16px 0; font-size: 18px; font-weight: 600;';
|
||||
|
||||
const description = document.createElement('p');
|
||||
description.textContent = `Select which account to use (${accounts.length} accounts derived from seed phrase):`;
|
||||
description.style.cssText = 'margin-bottom: 20px; color: #6b7280; font-size: 14px;';
|
||||
|
||||
this.modalBody.appendChild(title);
|
||||
this.modalBody.appendChild(description);
|
||||
|
||||
// Create table for account selection
|
||||
const table = document.createElement('table');
|
||||
table.style.cssText = `
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 20px;
|
||||
font-family: var(--nl-font-family, 'Courier New', monospace);
|
||||
font-size: 12px;
|
||||
`;
|
||||
|
||||
// Table header
|
||||
const thead = document.createElement('thead');
|
||||
thead.innerHTML = `
|
||||
<tr style="background: #f3f4f6;">
|
||||
<th style="padding: 8px; text-align: center; border: 1px solid #d1d5db; font-weight: bold;">#</th>
|
||||
<th style="padding: 8px; text-align: left; border: 1px solid #d1d5db; font-weight: bold;">Public Key (npub)</th>
|
||||
<th style="padding: 8px; text-align: center; border: 1px solid #d1d5db; font-weight: bold;">Action</th>
|
||||
</tr>
|
||||
`;
|
||||
table.appendChild(thead);
|
||||
|
||||
// Table body
|
||||
const tbody = document.createElement('tbody');
|
||||
accounts.forEach(account => {
|
||||
const row = document.createElement('tr');
|
||||
row.style.cssText = 'border: 1px solid #d1d5db;';
|
||||
|
||||
const indexCell = document.createElement('td');
|
||||
indexCell.textContent = account.index;
|
||||
indexCell.style.cssText = 'padding: 8px; text-align: center; border: 1px solid #d1d5db; font-weight: bold;';
|
||||
|
||||
const pubkeyCell = document.createElement('td');
|
||||
pubkeyCell.style.cssText = 'padding: 8px; border: 1px solid #d1d5db; font-family: monospace; word-break: break-all;';
|
||||
|
||||
// Show truncated npub for readability
|
||||
const truncatedNpub = `${account.npub.slice(0, 12)}...${account.npub.slice(-8)}`;
|
||||
pubkeyCell.innerHTML = `
|
||||
<code style="background: #f3f4f6; padding: 2px 4px; border-radius: 2px;">${truncatedNpub}</code><br>
|
||||
<small style="color: #6b7280;">Full: ${account.npub}</small>
|
||||
`;
|
||||
|
||||
const actionCell = document.createElement('td');
|
||||
actionCell.style.cssText = 'padding: 8px; text-align: center; border: 1px solid #d1d5db;';
|
||||
|
||||
const selectButton = document.createElement('button');
|
||||
selectButton.textContent = 'Use';
|
||||
selectButton.onclick = () => this._selectAccount(account);
|
||||
selectButton.style.cssText = `
|
||||
padding: 4px 12px;
|
||||
font-size: 11px;
|
||||
background: var(--nl-secondary-color);
|
||||
color: var(--nl-primary-color);
|
||||
border: 1px solid var(--nl-primary-color);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-family: var(--nl-font-family, 'Courier New', monospace);
|
||||
`;
|
||||
selectButton.onmouseover = () => {
|
||||
selectButton.style.borderColor = 'var(--nl-accent-color)';
|
||||
};
|
||||
selectButton.onmouseout = () => {
|
||||
selectButton.style.borderColor = 'var(--nl-primary-color)';
|
||||
};
|
||||
|
||||
actionCell.appendChild(selectButton);
|
||||
|
||||
row.appendChild(indexCell);
|
||||
row.appendChild(pubkeyCell);
|
||||
row.appendChild(actionCell);
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
table.appendChild(tbody);
|
||||
|
||||
this.modalBody.appendChild(table);
|
||||
|
||||
// Back button
|
||||
const backButton = document.createElement('button');
|
||||
backButton.textContent = 'Back to Seed Phrase';
|
||||
backButton.onclick = () => this._showSeedPhraseScreen();
|
||||
backButton.style.cssText = this._getButtonStyle('secondary');
|
||||
|
||||
this.modalBody.appendChild(backButton);
|
||||
}
|
||||
|
||||
_selectAccount(account) {
|
||||
console.log('Selected account:', account.index, account.npub);
|
||||
|
||||
// Use the same auth method as local keys, but with seedphrase identifier
|
||||
this._setAuthMethod('local', {
|
||||
secret: account.nsec,
|
||||
pubkey: account.publicKey,
|
||||
source: 'seedphrase',
|
||||
accountIndex: account.index
|
||||
});
|
||||
}
|
||||
|
||||
_showOtpScreen() {
|
||||
// Placeholder for OTP functionality
|
||||
this._showError('OTP/DM not yet implemented - coming soon!');
|
||||
@@ -2503,13 +2317,13 @@ class FloatingTab {
|
||||
// Determine which relays to use
|
||||
const relays = this.options.getUserRelay.length > 0
|
||||
? this.options.getUserRelay
|
||||
: (this.modal?.options?.relays || ['wss://relay.damus.io', 'wss://nos.lol']);
|
||||
: ['wss://relay.damus.io', 'wss://nos.lol'];
|
||||
|
||||
console.log('FloatingTab: Fetching profile from relays:', relays);
|
||||
|
||||
try {
|
||||
// Create a SimplePool instance for querying
|
||||
const pool = new window.NostrTools.nip46.SimplePool();
|
||||
const pool = new window.NostrTools.SimplePool();
|
||||
|
||||
// Query for kind 0 (user metadata) events
|
||||
const events = await pool.querySync(relays, {
|
||||
@@ -2532,9 +2346,27 @@ class FloatingTab {
|
||||
const profile = JSON.parse(latestEvent.content);
|
||||
console.log('FloatingTab: Parsed profile:', profile);
|
||||
|
||||
// Return relevant profile fields
|
||||
// Find the best name from any key containing "name" (case-insensitive)
|
||||
let bestName = null;
|
||||
const nameKeys = Object.keys(profile).filter(key =>
|
||||
key.toLowerCase().includes('name') &&
|
||||
typeof profile[key] === 'string' &&
|
||||
profile[key].trim().length > 0
|
||||
);
|
||||
|
||||
if (nameKeys.length > 0) {
|
||||
// Find the shortest name value
|
||||
bestName = nameKeys
|
||||
.map(key => profile[key].trim())
|
||||
.reduce((shortest, current) =>
|
||||
current.length < shortest.length ? current : shortest
|
||||
);
|
||||
console.log('FloatingTab: Found name keys:', nameKeys, 'selected:', bestName);
|
||||
}
|
||||
|
||||
// Return relevant profile fields with the best name
|
||||
return {
|
||||
name: profile.name || null,
|
||||
name: bestName,
|
||||
display_name: profile.display_name || null,
|
||||
about: profile.about || null,
|
||||
picture: profile.picture || null,
|
||||
@@ -2695,10 +2527,10 @@ class NostrLite {
|
||||
|
||||
this.options = {
|
||||
theme: 'default',
|
||||
relays: ['wss://relay.damus.io', 'wss://nos.lol'],
|
||||
methods: {
|
||||
extension: true,
|
||||
local: true,
|
||||
seedphrase: false,
|
||||
readonly: true,
|
||||
connect: false,
|
||||
otp: false
|
||||
@@ -3127,8 +2959,8 @@ class WindowNostr {
|
||||
}
|
||||
|
||||
async getRelays() {
|
||||
// Return configured relays from nostr-lite options
|
||||
return this.nostrLite.options?.relays || ['wss://relay.damus.io'];
|
||||
// Return default relays since we removed the relays configuration
|
||||
return ['wss://relay.damus.io', 'wss://nos.lol'];
|
||||
}
|
||||
|
||||
get nip04() {
|
||||
|
||||
5472
api/nostr.bundle.js
5472
api/nostr.bundle.js
File diff suppressed because it is too large
Load Diff
460
docs/admin_api_plan.md
Normal file
460
docs/admin_api_plan.md
Normal file
@@ -0,0 +1,460 @@
|
||||
# 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 23456: Unified Admin API (Ephemeral)**
|
||||
- Configuration management: Update relay settings, limits, authentication policies
|
||||
- Auth rules: Add/remove/query whitelist/blacklist rules
|
||||
- System commands: clear rules, status, cache management
|
||||
- **Unified Format**: All commands use NIP-44 encrypted content with `["p", "relay_pubkey"]` tags
|
||||
- **Command Types**:
|
||||
- Configuration: `["config_key", "config_value"]`
|
||||
- Auth rules: `["rule_type", "pattern_type", "pattern_value"]`
|
||||
- Queries: `["auth_query", "filter"]` or `["system_command", "command_name"]`
|
||||
- **Security**: All admin commands use NIP-44 encryption for privacy and security
|
||||
|
||||
#### Configuration Commands (using Kind 23456)
|
||||
|
||||
1. **Update Configuration**:
|
||||
```json
|
||||
{
|
||||
"kind": 23456,
|
||||
"content": "base64_nip44_encrypted_command_array",
|
||||
"tags": [["p", "relay_pubkey"]]
|
||||
}
|
||||
```
|
||||
*Encrypted content contains:* `["relay_description", "My Relay"]`
|
||||
|
||||
2. **Query System Status**:
|
||||
```json
|
||||
{
|
||||
"kind": 23456,
|
||||
"content": "base64_nip44_encrypted_command_array",
|
||||
"tags": [["p", "relay_pubkey"]]
|
||||
}
|
||||
```
|
||||
*Encrypted content contains:* `["system_command", "system_status"]`
|
||||
|
||||
#### Auth Rules and System Commands (using Kind 23456)
|
||||
|
||||
1. **Clear All Auth Rules**:
|
||||
```json
|
||||
{
|
||||
"kind": 23456,
|
||||
"content": "base64_nip44_encrypted_command_array",
|
||||
"tags": [["p", "relay_pubkey"]]
|
||||
}
|
||||
```
|
||||
*Encrypted content contains:* `["system_command", "clear_all_auth_rules"]`
|
||||
|
||||
2. **Query All Auth Rules**:
|
||||
```json
|
||||
{
|
||||
"kind": 23456,
|
||||
"content": "base64_nip44_encrypted_command_array",
|
||||
"tags": [["p", "relay_pubkey"]]
|
||||
}
|
||||
```
|
||||
*Encrypted content contains:* `["auth_query", "all"]`
|
||||
|
||||
3. **Add Blacklist Rule**:
|
||||
```json
|
||||
{
|
||||
"kind": 23456,
|
||||
"content": "base64_nip44_encrypted_command_array",
|
||||
"tags": [["p", "relay_pubkey"]]
|
||||
}
|
||||
```
|
||||
*Encrypted content 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 unified ephemeral event kind (23456)
|
||||
4. Test blacklist enforcement
|
||||
|
||||
#### Medium Priority (Enhanced Admin Features):
|
||||
1. **Implement NIP-44 Encryption Support**:
|
||||
- Detect NIP-44 encrypted content for Kind 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.
|
||||
|
||||
## Unified Admin API (Kind 23456 - Ephemeral)
|
||||
Update relay configuration parameters or query available settings.
|
||||
|
||||
**Configuration Update Event:**
|
||||
```json
|
||||
{
|
||||
"kind": 23456,
|
||||
"content": "base64_nip44_encrypted_command_array",
|
||||
"tags": [["p", "relay_pubkey"]]
|
||||
}
|
||||
```
|
||||
*Encrypted content contains:* `["relay_description", "My Relay Description"]`
|
||||
|
||||
**Auth Rules Management:**
|
||||
|
||||
**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 23456 --content "base64_nip44_encrypted_command" \
|
||||
-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": 23457,
|
||||
"content": "base64_nip44_encrypted_response",
|
||||
"tags": [["p", "admin_pubkey"]]
|
||||
}]
|
||||
```
|
||||
|
||||
### Current Config Response
|
||||
```json
|
||||
["EVENT", "subscription_id", {
|
||||
"kind": 23457,
|
||||
"content": "base64_nip44_encrypted_response",
|
||||
"tags": [["p", "admin_pubkey"]]
|
||||
}]
|
||||
```
|
||||
|
||||
### 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 unified Kind 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 23456 with NIP-44 encrypted content
|
||||
if (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 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 23456 (Unified Admin API) 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.
|
||||
@@ -198,25 +198,54 @@ fi
|
||||
|
||||
echo "Build successful. Proceeding with relay restart..."
|
||||
|
||||
# Kill existing relay if running
|
||||
# Kill existing relay if running - start aggressive immediately
|
||||
echo "Stopping any existing relay servers..."
|
||||
pkill -f "c_relay_" 2>/dev/null
|
||||
sleep 2 # Give time for shutdown
|
||||
|
||||
# Check if port is still bound
|
||||
if lsof -i :8888 >/dev/null 2>&1; then
|
||||
echo "Port 8888 still in use, force killing..."
|
||||
fuser -k 8888/tcp 2>/dev/null || echo "No process on port 8888"
|
||||
# Get all relay processes and kill them immediately with -9
|
||||
RELAY_PIDS=$(pgrep -f "c_relay_" || echo "")
|
||||
if [ -n "$RELAY_PIDS" ]; then
|
||||
echo "Force killing relay processes immediately: $RELAY_PIDS"
|
||||
kill -9 $RELAY_PIDS 2>/dev/null
|
||||
else
|
||||
echo "No existing relay processes found"
|
||||
fi
|
||||
|
||||
# Get any remaining processes
|
||||
REMAINING_PIDS=$(pgrep -f "c_relay_" || echo "")
|
||||
if [ -n "$REMAINING_PIDS" ]; then
|
||||
echo "Force killing remaining processes: $REMAINING_PIDS"
|
||||
kill -9 $REMAINING_PIDS 2>/dev/null
|
||||
# Ensure port 8888 is completely free with retry loop
|
||||
echo "Ensuring port 8888 is available..."
|
||||
for attempt in {1..15}; do
|
||||
if ! lsof -i :8888 >/dev/null 2>&1; then
|
||||
echo "Port 8888 is now free"
|
||||
break
|
||||
fi
|
||||
|
||||
echo "Attempt $attempt: Port 8888 still in use, force killing..."
|
||||
# Kill anything using port 8888
|
||||
fuser -k 8888/tcp 2>/dev/null || true
|
||||
|
||||
# Double-check for any remaining relay processes
|
||||
REMAINING_PIDS=$(pgrep -f "c_relay_" || echo "")
|
||||
if [ -n "$REMAINING_PIDS" ]; then
|
||||
echo "Killing remaining relay processes: $REMAINING_PIDS"
|
||||
kill -9 $REMAINING_PIDS 2>/dev/null || true
|
||||
fi
|
||||
|
||||
sleep 2
|
||||
|
||||
if [ $attempt -eq 15 ]; then
|
||||
echo "ERROR: Could not free port 8888 after 15 attempts"
|
||||
echo "Current processes using port:"
|
||||
lsof -i :8888 2>/dev/null || echo "No process details available"
|
||||
echo "You may need to manually kill processes or reboot"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Final safety check - ensure no relay processes remain
|
||||
FINAL_PIDS=$(pgrep -f "c_relay_" || echo "")
|
||||
if [ -n "$FINAL_PIDS" ]; then
|
||||
echo "Final cleanup: killing processes $FINAL_PIDS"
|
||||
kill -9 $FINAL_PIDS 2>/dev/null || true
|
||||
sleep 1
|
||||
else
|
||||
echo "No existing relay found"
|
||||
fi
|
||||
|
||||
# Clean up PID file
|
||||
@@ -253,14 +282,14 @@ cd build
|
||||
# Start relay in background and capture its PID
|
||||
if [ "$USE_TEST_KEYS" = true ]; then
|
||||
echo "Using deterministic test keys for development..."
|
||||
./$(basename $BINARY_PATH) -a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -r 1111111111111111111111111111111111111111111111111111111111111111 > ../relay.log 2>&1 &
|
||||
./$(basename $BINARY_PATH) -a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -r 1111111111111111111111111111111111111111111111111111111111111111 --strict-port > ../relay.log 2>&1 &
|
||||
elif [ -n "$RELAY_ARGS" ]; then
|
||||
echo "Starting relay with custom configuration..."
|
||||
./$(basename $BINARY_PATH) $RELAY_ARGS > ../relay.log 2>&1 &
|
||||
./$(basename $BINARY_PATH) $RELAY_ARGS --strict-port > ../relay.log 2>&1 &
|
||||
else
|
||||
# No command line arguments needed for random key generation
|
||||
echo "Starting relay with random key generation..."
|
||||
./$(basename $BINARY_PATH) > ../relay.log 2>&1 &
|
||||
./$(basename $BINARY_PATH) --strict-port > ../relay.log 2>&1 &
|
||||
fi
|
||||
RELAY_PID=$!
|
||||
# Change back to original directory
|
||||
|
||||
455
nip11_relay_connection_implementation_plan.md
Normal file
455
nip11_relay_connection_implementation_plan.md
Normal file
@@ -0,0 +1,455 @@
|
||||
# NIP-11 Relay Connection Implementation Plan
|
||||
|
||||
## Overview
|
||||
Implement NIP-11 relay information fetching in the web admin interface to replace hardcoded relay pubkey and provide proper relay connection flow.
|
||||
|
||||
## Current Issues
|
||||
1. **Hardcoded Relay Pubkey**: `getRelayPubkey()` returns hardcoded value `'4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa'`
|
||||
2. **Relay URL in Debug Section**: Currently in "DEBUG - TEST FETCH WITHOUT LOGIN" section (lines 336-385)
|
||||
3. **No Relay Verification**: Users can attempt admin operations without verifying relay identity
|
||||
4. **Missing NIP-11 Support**: No fetching of relay information document
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### 1. New Relay Connection Section (HTML Structure)
|
||||
|
||||
Add after User Info section (around line 332):
|
||||
|
||||
```html
|
||||
<!-- Relay Connection Section -->
|
||||
<div class="section">
|
||||
<h2>RELAY CONNECTION</h2>
|
||||
<div class="input-group">
|
||||
<label for="relay-url-input">Relay URL:</label>
|
||||
<input type="text" id="relay-url-input" value="ws://localhost:8888" placeholder="ws://localhost:8888 or wss://relay.example.com">
|
||||
</div>
|
||||
<div class="inline-buttons">
|
||||
<button type="button" id="connect-relay-btn">CONNECT TO RELAY</button>
|
||||
<button type="button" id="disconnect-relay-btn" style="display: none;">DISCONNECT</button>
|
||||
</div>
|
||||
<div class="status disconnected" id="relay-connection-status">NOT CONNECTED</div>
|
||||
|
||||
<!-- Relay Information Display -->
|
||||
<div id="relay-info-display" class="hidden">
|
||||
<h3>Relay Information</h3>
|
||||
<div class="user-info">
|
||||
<div><strong>Name:</strong> <span id="relay-name">-</span></div>
|
||||
<div><strong>Description:</strong> <span id="relay-description">-</span></div>
|
||||
<div><strong>Public Key:</strong>
|
||||
<div class="user-pubkey" id="relay-pubkey-display">-</div>
|
||||
</div>
|
||||
<div><strong>Software:</strong> <span id="relay-software">-</span></div>
|
||||
<div><strong>Version:</strong> <span id="relay-version">-</span></div>
|
||||
<div><strong>Contact:</strong> <span id="relay-contact">-</span></div>
|
||||
<div><strong>Supported NIPs:</strong> <span id="relay-nips">-</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 2. JavaScript Implementation
|
||||
|
||||
#### Global State Variables
|
||||
Add to global state section (around line 535):
|
||||
|
||||
```javascript
|
||||
// Relay connection state
|
||||
let relayInfo = null;
|
||||
let isRelayConnected = false;
|
||||
let relayWebSocket = null;
|
||||
```
|
||||
|
||||
#### NIP-11 Fetching Function
|
||||
Add new function:
|
||||
|
||||
```javascript
|
||||
// Fetch relay information using NIP-11
|
||||
async function fetchRelayInfo(relayUrl) {
|
||||
try {
|
||||
console.log('=== FETCHING RELAY INFO VIA NIP-11 ===');
|
||||
console.log('Relay URL:', relayUrl);
|
||||
|
||||
// Convert WebSocket URL to HTTP URL for NIP-11
|
||||
let httpUrl = relayUrl;
|
||||
if (relayUrl.startsWith('ws://')) {
|
||||
httpUrl = relayUrl.replace('ws://', 'http://');
|
||||
} else if (relayUrl.startsWith('wss://')) {
|
||||
httpUrl = relayUrl.replace('wss://', 'https://');
|
||||
}
|
||||
|
||||
console.log('HTTP URL for NIP-11:', httpUrl);
|
||||
|
||||
// Fetch relay information document
|
||||
const response = await fetch(httpUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/nostr+json'
|
||||
},
|
||||
// Add timeout
|
||||
signal: AbortSignal.timeout(10000) // 10 second timeout
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (!contentType || !contentType.includes('application/json')) {
|
||||
throw new Error(`Invalid content type: ${contentType}. Expected application/json or application/nostr+json`);
|
||||
}
|
||||
|
||||
const relayInfoData = await response.json();
|
||||
console.log('Fetched relay info:', relayInfoData);
|
||||
|
||||
// Validate required fields
|
||||
if (!relayInfoData.pubkey) {
|
||||
throw new Error('Relay information missing required pubkey field');
|
||||
}
|
||||
|
||||
// Validate pubkey format (64 hex characters)
|
||||
if (!/^[0-9a-fA-F]{64}$/.test(relayInfoData.pubkey)) {
|
||||
throw new Error(`Invalid relay pubkey format: ${relayInfoData.pubkey}`);
|
||||
}
|
||||
|
||||
return relayInfoData;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch relay info:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Relay Connection Function
|
||||
Add new function:
|
||||
|
||||
```javascript
|
||||
// Connect to relay and fetch information
|
||||
async function connectToRelay() {
|
||||
try {
|
||||
const relayUrlInput = document.getElementById('relay-url-input');
|
||||
const connectBtn = document.getElementById('connect-relay-btn');
|
||||
const disconnectBtn = document.getElementById('disconnect-relay-btn');
|
||||
const statusDiv = document.getElementById('relay-connection-status');
|
||||
const infoDisplay = document.getElementById('relay-info-display');
|
||||
|
||||
const url = relayUrlInput.value.trim();
|
||||
if (!url) {
|
||||
throw new Error('Please enter a relay URL');
|
||||
}
|
||||
|
||||
// Update UI to show connecting state
|
||||
connectBtn.disabled = true;
|
||||
statusDiv.textContent = 'CONNECTING...';
|
||||
statusDiv.className = 'status connected';
|
||||
|
||||
console.log('Connecting to relay:', url);
|
||||
|
||||
// Fetch relay information via NIP-11
|
||||
console.log('Fetching relay information...');
|
||||
const fetchedRelayInfo = await fetchRelayInfo(url);
|
||||
|
||||
// Test WebSocket connection
|
||||
console.log('Testing WebSocket connection...');
|
||||
await testWebSocketConnection(url);
|
||||
|
||||
// Store relay information
|
||||
relayInfo = fetchedRelayInfo;
|
||||
isRelayConnected = true;
|
||||
|
||||
// Update UI with relay information
|
||||
displayRelayInfo(relayInfo);
|
||||
|
||||
// Update connection status
|
||||
statusDiv.textContent = 'CONNECTED';
|
||||
statusDiv.className = 'status connected';
|
||||
|
||||
// Update button states
|
||||
connectBtn.style.display = 'none';
|
||||
disconnectBtn.style.display = 'inline-block';
|
||||
relayUrlInput.disabled = true;
|
||||
|
||||
// Show relay info
|
||||
infoDisplay.classList.remove('hidden');
|
||||
|
||||
console.log('Successfully connected to relay:', relayInfo.name || url);
|
||||
log(`Connected to relay: ${relayInfo.name || url}`, 'INFO');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to connect to relay:', error);
|
||||
|
||||
// Reset UI state
|
||||
const connectBtn = document.getElementById('connect-relay-btn');
|
||||
const statusDiv = document.getElementById('relay-connection-status');
|
||||
|
||||
connectBtn.disabled = false;
|
||||
statusDiv.textContent = `CONNECTION FAILED: ${error.message}`;
|
||||
statusDiv.className = 'status error';
|
||||
|
||||
// Clear any partial state
|
||||
relayInfo = null;
|
||||
isRelayConnected = false;
|
||||
|
||||
log(`Failed to connect to relay: ${error.message}`, 'ERROR');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### WebSocket Connection Test
|
||||
Add new function:
|
||||
|
||||
```javascript
|
||||
// Test WebSocket connection to relay
|
||||
async function testWebSocketConnection(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
ws.close();
|
||||
reject(new Error('WebSocket connection timeout'));
|
||||
}, 5000);
|
||||
|
||||
const ws = new WebSocket(url);
|
||||
|
||||
ws.onopen = () => {
|
||||
clearTimeout(timeout);
|
||||
console.log('WebSocket connection successful');
|
||||
ws.close();
|
||||
resolve();
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
clearTimeout(timeout);
|
||||
console.error('WebSocket connection failed:', error);
|
||||
reject(new Error('WebSocket connection failed'));
|
||||
};
|
||||
|
||||
ws.onclose = (event) => {
|
||||
if (event.code !== 1000) {
|
||||
clearTimeout(timeout);
|
||||
reject(new Error(`WebSocket closed with code ${event.code}: ${event.reason}`));
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
#### Display Relay Information
|
||||
Add new function:
|
||||
|
||||
```javascript
|
||||
// Display relay information in the UI
|
||||
function displayRelayInfo(info) {
|
||||
document.getElementById('relay-name').textContent = info.name || 'Unknown';
|
||||
document.getElementById('relay-description').textContent = info.description || 'No description';
|
||||
document.getElementById('relay-pubkey-display').textContent = info.pubkey || 'Unknown';
|
||||
document.getElementById('relay-software').textContent = info.software || 'Unknown';
|
||||
document.getElementById('relay-version').textContent = info.version || 'Unknown';
|
||||
document.getElementById('relay-contact').textContent = info.contact || 'No contact info';
|
||||
|
||||
// Format supported NIPs
|
||||
let nipsText = 'None specified';
|
||||
if (info.supported_nips && Array.isArray(info.supported_nips) && info.supported_nips.length > 0) {
|
||||
nipsText = info.supported_nips.map(nip => `NIP-${nip.toString().padStart(2, '0')}`).join(', ');
|
||||
}
|
||||
document.getElementById('relay-nips').textContent = nipsText;
|
||||
}
|
||||
```
|
||||
|
||||
#### Disconnect Function
|
||||
Add new function:
|
||||
|
||||
```javascript
|
||||
// Disconnect from relay
|
||||
function disconnectFromRelay() {
|
||||
console.log('Disconnecting from relay...');
|
||||
|
||||
// Clear relay state
|
||||
relayInfo = null;
|
||||
isRelayConnected = false;
|
||||
|
||||
// Close any existing connections
|
||||
if (relayPool) {
|
||||
const url = document.getElementById('relay-url-input').value.trim();
|
||||
if (url) {
|
||||
relayPool.close([url]);
|
||||
}
|
||||
relayPool = null;
|
||||
subscriptionId = null;
|
||||
}
|
||||
|
||||
// Reset UI
|
||||
const connectBtn = document.getElementById('connect-relay-btn');
|
||||
const disconnectBtn = document.getElementById('disconnect-relay-btn');
|
||||
const statusDiv = document.getElementById('relay-connection-status');
|
||||
const infoDisplay = document.getElementById('relay-info-display');
|
||||
const relayUrlInput = document.getElementById('relay-url-input');
|
||||
|
||||
connectBtn.style.display = 'inline-block';
|
||||
disconnectBtn.style.display = 'none';
|
||||
connectBtn.disabled = false;
|
||||
relayUrlInput.disabled = false;
|
||||
|
||||
statusDiv.textContent = 'NOT CONNECTED';
|
||||
statusDiv.className = 'status disconnected';
|
||||
|
||||
infoDisplay.classList.add('hidden');
|
||||
|
||||
// Reset configuration status
|
||||
updateConfigStatus(false);
|
||||
|
||||
log('Disconnected from relay', 'INFO');
|
||||
}
|
||||
```
|
||||
|
||||
#### Update getRelayPubkey Function
|
||||
Replace existing function (around line 3142):
|
||||
|
||||
```javascript
|
||||
// Helper function to get relay pubkey from connected relay info
|
||||
function getRelayPubkey() {
|
||||
if (relayInfo && relayInfo.pubkey) {
|
||||
return relayInfo.pubkey;
|
||||
}
|
||||
|
||||
// Fallback to hardcoded value if no relay connected (for testing)
|
||||
console.warn('No relay connected, using fallback pubkey');
|
||||
return '4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa';
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Event Handlers
|
||||
|
||||
Add event handlers in the DOMContentLoaded section:
|
||||
|
||||
```javascript
|
||||
// Relay connection event handlers
|
||||
const connectRelayBtn = document.getElementById('connect-relay-btn');
|
||||
const disconnectRelayBtn = document.getElementById('disconnect-relay-btn');
|
||||
|
||||
if (connectRelayBtn) {
|
||||
connectRelayBtn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
connectToRelay().catch(error => {
|
||||
console.error('Connect to relay failed:', error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (disconnectRelayBtn) {
|
||||
disconnectRelayBtn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
disconnectFromRelay();
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Update Existing Functions
|
||||
|
||||
#### Update fetchConfiguration Function
|
||||
Add relay connection check at the beginning:
|
||||
|
||||
```javascript
|
||||
async function fetchConfiguration() {
|
||||
try {
|
||||
console.log('=== FETCHING CONFIGURATION VIA ADMIN API ===');
|
||||
|
||||
// Check if relay is connected
|
||||
if (!isRelayConnected || !relayInfo) {
|
||||
throw new Error('Must be connected to relay first. Please connect to relay in the Relay Connection section.');
|
||||
}
|
||||
|
||||
// ... rest of existing function
|
||||
} catch (error) {
|
||||
// ... existing error handling
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Update subscribeToConfiguration Function
|
||||
Add relay connection check:
|
||||
|
||||
```javascript
|
||||
async function subscribeToConfiguration() {
|
||||
try {
|
||||
console.log('=== STARTING SIMPLEPOOL CONFIGURATION SUBSCRIPTION ===');
|
||||
|
||||
if (!isRelayConnected || !relayInfo) {
|
||||
console.error('Must be connected to relay first');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use the relay URL from the connection section instead of the debug section
|
||||
const url = document.getElementById('relay-url-input').value.trim();
|
||||
|
||||
// ... rest of existing function
|
||||
} catch (error) {
|
||||
// ... existing error handling
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Update UI Flow
|
||||
|
||||
#### Modify showMainInterface Function
|
||||
Update to show relay connection requirement:
|
||||
|
||||
```javascript
|
||||
function showMainInterface() {
|
||||
loginSection.classList.add('hidden');
|
||||
mainInterface.classList.remove('hidden');
|
||||
userPubkeyDisplay.textContent = userPubkey;
|
||||
|
||||
// Show message about relay connection requirement
|
||||
if (!isRelayConnected) {
|
||||
log('Please connect to a relay to access admin functions', 'INFO');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Remove/Update Debug Section
|
||||
|
||||
#### Option 1: Remove Debug Section Entirely
|
||||
Remove the "DEBUG - TEST FETCH WITHOUT LOGIN" section (lines 335-385) since relay URL is now in the proper connection section.
|
||||
|
||||
#### Option 2: Keep Debug Section for Testing
|
||||
Update the debug section to use the connected relay URL and add a note that it's for testing purposes.
|
||||
|
||||
### 7. Error Handling
|
||||
|
||||
Add comprehensive error handling for:
|
||||
- Network timeouts
|
||||
- Invalid relay URLs
|
||||
- Missing NIP-11 support
|
||||
- Invalid relay pubkey format
|
||||
- WebSocket connection failures
|
||||
- CORS issues
|
||||
|
||||
### 8. Security Considerations
|
||||
|
||||
- Validate relay pubkey format (64 hex characters)
|
||||
- Verify relay identity before admin operations
|
||||
- Handle CORS properly for NIP-11 requests
|
||||
- Sanitize relay information display
|
||||
- Warn users about connecting to untrusted relays
|
||||
|
||||
## Testing Plan
|
||||
|
||||
1. **NIP-11 Fetching**: Test with various relay URLs (localhost, remote relays)
|
||||
2. **Error Handling**: Test with invalid URLs, non-Nostr servers, network failures
|
||||
3. **WebSocket Connection**: Verify WebSocket connectivity after NIP-11 fetch
|
||||
4. **Admin API Integration**: Ensure admin commands use correct relay pubkey
|
||||
5. **UI Flow**: Test complete user journey from login → relay connection → admin operations
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Proper Relay Identification**: Uses actual relay pubkey instead of hardcoded value
|
||||
2. **Better UX**: Clear connection flow and relay information display
|
||||
3. **Protocol Compliance**: Implements NIP-11 standard for relay discovery
|
||||
4. **Security**: Verifies relay identity before admin operations
|
||||
5. **Flexibility**: Works with any NIP-11 compliant relay
|
||||
|
||||
## Migration Notes
|
||||
|
||||
- Existing users will need to connect to relay after this update
|
||||
- Debug section can be kept for development/testing purposes
|
||||
- All admin functions will require relay connection
|
||||
- Relay pubkey will be dynamically fetched instead of hardcoded
|
||||
2456
src/config.c
2456
src/config.c
File diff suppressed because it is too large
Load Diff
106
src/config.h
106
src/config.h
@@ -4,6 +4,10 @@
|
||||
#include <sqlite3.h>
|
||||
#include <cjson/cJSON.h>
|
||||
#include <time.h>
|
||||
#include <pthread.h>
|
||||
|
||||
// Forward declaration for WebSocket support
|
||||
struct lws;
|
||||
|
||||
// Configuration constants
|
||||
#define CONFIG_VALUE_MAX_LENGTH 1024
|
||||
@@ -23,24 +27,82 @@
|
||||
// Database path for event-based config
|
||||
extern char g_database_path[512];
|
||||
|
||||
// Configuration manager structure
|
||||
// Unified configuration cache structure (consolidates all caching systems)
|
||||
typedef struct {
|
||||
sqlite3* db;
|
||||
char relay_pubkey[65];
|
||||
// Critical keys (frequently accessed)
|
||||
char admin_pubkey[65];
|
||||
time_t last_config_check;
|
||||
char config_file_path[512]; // Temporary for compatibility
|
||||
} config_manager_t;
|
||||
char relay_pubkey[65];
|
||||
|
||||
// Auth config (from request_validator)
|
||||
int auth_required;
|
||||
long max_file_size;
|
||||
int admin_enabled;
|
||||
int nip42_mode;
|
||||
int nip42_challenge_timeout;
|
||||
int nip42_time_tolerance;
|
||||
|
||||
// Static buffer for config values (replaces static buffers in get_config_value functions)
|
||||
char temp_buffer[CONFIG_VALUE_MAX_LENGTH];
|
||||
|
||||
// NIP-11 relay information (migrated from g_relay_info in main.c)
|
||||
struct {
|
||||
char name[RELAY_NAME_MAX_LENGTH];
|
||||
char description[RELAY_DESCRIPTION_MAX_LENGTH];
|
||||
char banner[RELAY_URL_MAX_LENGTH];
|
||||
char icon[RELAY_URL_MAX_LENGTH];
|
||||
char pubkey[RELAY_PUBKEY_MAX_LENGTH];
|
||||
char contact[RELAY_CONTACT_MAX_LENGTH];
|
||||
char software[RELAY_URL_MAX_LENGTH];
|
||||
char version[64];
|
||||
char privacy_policy[RELAY_URL_MAX_LENGTH];
|
||||
char terms_of_service[RELAY_URL_MAX_LENGTH];
|
||||
cJSON* supported_nips;
|
||||
cJSON* limitation;
|
||||
cJSON* retention;
|
||||
cJSON* relay_countries;
|
||||
cJSON* language_tags;
|
||||
cJSON* tags;
|
||||
char posting_policy[RELAY_URL_MAX_LENGTH];
|
||||
cJSON* fees;
|
||||
char payments_url[RELAY_URL_MAX_LENGTH];
|
||||
} relay_info;
|
||||
|
||||
// NIP-13 PoW configuration (migrated from g_pow_config in main.c)
|
||||
struct {
|
||||
int enabled;
|
||||
int min_pow_difficulty;
|
||||
int validation_flags;
|
||||
int require_nonce_tag;
|
||||
int reject_lower_targets;
|
||||
int strict_format;
|
||||
int anti_spam_mode;
|
||||
} pow_config;
|
||||
|
||||
// NIP-40 Expiration configuration (migrated from g_expiration_config in main.c)
|
||||
struct {
|
||||
int enabled;
|
||||
int strict_mode;
|
||||
int filter_responses;
|
||||
int delete_expired;
|
||||
long grace_period;
|
||||
} expiration_config;
|
||||
|
||||
// Cache management
|
||||
time_t cache_expires;
|
||||
int cache_valid;
|
||||
pthread_mutex_t cache_lock;
|
||||
} unified_config_cache_t;
|
||||
|
||||
// Command line options structure for first-time startup
|
||||
typedef struct {
|
||||
int port_override; // -1 = not set, >0 = port value
|
||||
char admin_privkey_override[65]; // Empty string = not set, 64-char hex = override
|
||||
char relay_privkey_override[65]; // Empty string = not set, 64-char hex = override
|
||||
int strict_port; // 0 = allow port increment, 1 = fail if exact port unavailable
|
||||
} cli_options_t;
|
||||
|
||||
// Global configuration manager
|
||||
extern config_manager_t g_config_manager;
|
||||
// Global unified configuration cache
|
||||
extern unified_config_cache_t g_unified_cache;
|
||||
|
||||
// Core configuration functions (temporary compatibility)
|
||||
int init_configuration_system(const char* config_dir_override, const char* config_file_override);
|
||||
@@ -100,11 +162,22 @@ int set_config_value_in_table(const char* key, const char* value, const char* da
|
||||
const char* description, const char* category, int requires_restart);
|
||||
int update_config_in_table(const char* key, const char* value);
|
||||
int populate_default_config_values(void);
|
||||
int add_pubkeys_to_config_table(void);
|
||||
|
||||
// Admin event processing functions
|
||||
int process_admin_event_in_config(cJSON* event, char* error_message, size_t error_size);
|
||||
// Admin event processing functions (updated with WebSocket support)
|
||||
int process_admin_event_in_config(cJSON* event, char* error_message, size_t error_size, struct lws* wsi);
|
||||
int process_admin_config_event(cJSON* event, char* error_message, size_t error_size);
|
||||
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, struct lws* wsi);
|
||||
|
||||
// Unified Kind 23456 handler functions
|
||||
int handle_kind_23456_unified(cJSON* event, char* error_message, size_t error_size, struct lws* wsi);
|
||||
int handle_auth_query_unified(cJSON* event, const char* query_type, char* error_message, size_t error_size, struct lws* wsi);
|
||||
int handle_system_command_unified(cJSON* event, const char* command, char* error_message, size_t error_size, struct lws* wsi);
|
||||
int handle_auth_rule_modification_unified(cJSON* event, char* error_message, size_t error_size, struct lws* wsi);
|
||||
|
||||
// Admin response functions
|
||||
int send_admin_response_event(const cJSON* response_data, const char* recipient_pubkey, struct lws* wsi);
|
||||
cJSON* build_query_response(const char* query_type, cJSON* results_array, int total_count);
|
||||
|
||||
// Auth rules management functions
|
||||
int add_auth_rule_from_config(const char* rule_type, const char* pattern_type,
|
||||
@@ -112,7 +185,10 @@ int add_auth_rule_from_config(const char* rule_type, const char* pattern_type,
|
||||
int remove_auth_rule_from_config(const char* rule_type, const char* pattern_type,
|
||||
const char* pattern_value);
|
||||
|
||||
// Configuration cache management
|
||||
// Unified configuration cache management
|
||||
void force_config_cache_refresh(void);
|
||||
const char* get_admin_pubkey_cached(void);
|
||||
const char* get_relay_pubkey_cached(void);
|
||||
void invalidate_config_cache(void);
|
||||
int reload_config_from_table(void);
|
||||
|
||||
@@ -129,4 +205,10 @@ int populate_config_table_from_event(const cJSON* event);
|
||||
int process_startup_config_event(const cJSON* event);
|
||||
int process_startup_config_event_with_fallback(const cJSON* event);
|
||||
|
||||
// Dynamic event generation functions for WebSocket configuration fetching
|
||||
cJSON* generate_config_event_from_table(void);
|
||||
int req_filter_requests_config_events(const cJSON* filter);
|
||||
cJSON* generate_synthetic_config_event_for_subscription(const char* sub_id, const cJSON* filters);
|
||||
char* generate_config_event_json(void);
|
||||
|
||||
#endif /* CONFIG_H */
|
||||
@@ -8,8 +8,7 @@
|
||||
* Default Configuration Event Template
|
||||
*
|
||||
* This header contains the default configuration values for the C Nostr Relay.
|
||||
* These values are used to create the initial kind 33334 configuration event
|
||||
* during first-time startup.
|
||||
* These values are used to populate the config table during first-time startup.
|
||||
*
|
||||
* IMPORTANT: These values should never be accessed directly by other parts
|
||||
* of the program. They are only used during initial configuration event creation.
|
||||
|
||||
832
src/main.c
832
src/main.c
File diff suppressed because it is too large
Load Diff
@@ -132,24 +132,11 @@ typedef struct {
|
||||
int time_tolerance_seconds;
|
||||
} nip42_challenge_manager_t;
|
||||
|
||||
// Cached configuration structure
|
||||
typedef struct {
|
||||
int auth_required; // Whether authentication is required
|
||||
long max_file_size; // Maximum file size in bytes
|
||||
int admin_enabled; // Whether admin interface is enabled
|
||||
char admin_pubkey[65]; // Admin public key
|
||||
int nip42_mode; // NIP-42 authentication mode
|
||||
int nip42_challenge_timeout; // NIP-42 challenge timeout in seconds
|
||||
int nip42_time_tolerance; // NIP-42 time tolerance in seconds
|
||||
time_t cache_expires; // When cache expires
|
||||
int cache_valid; // Whether cache is valid
|
||||
} auth_config_cache_t;
|
||||
|
||||
//=============================================================================
|
||||
// GLOBAL STATE
|
||||
//=============================================================================
|
||||
|
||||
static auth_config_cache_t g_auth_cache = {0};
|
||||
// No longer using local auth cache - using unified cache from config.c
|
||||
static nip42_challenge_manager_t g_challenge_manager = {0};
|
||||
static int g_validator_initialized = 0;
|
||||
|
||||
@@ -222,15 +209,15 @@ int ginxsom_request_validator_init(const char *db_path, const char *app_name) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Initialize NIP-42 challenge manager
|
||||
// Initialize NIP-42 challenge manager using unified config
|
||||
memset(&g_challenge_manager, 0, sizeof(g_challenge_manager));
|
||||
g_challenge_manager.timeout_seconds =
|
||||
g_auth_cache.nip42_challenge_timeout > 0
|
||||
? g_auth_cache.nip42_challenge_timeout
|
||||
: 600;
|
||||
g_challenge_manager.time_tolerance_seconds =
|
||||
g_auth_cache.nip42_time_tolerance > 0 ? g_auth_cache.nip42_time_tolerance
|
||||
: 300;
|
||||
|
||||
const char* nip42_timeout = get_config_value("nip42_challenge_timeout");
|
||||
g_challenge_manager.timeout_seconds = nip42_timeout ? atoi(nip42_timeout) : 600;
|
||||
|
||||
const char* nip42_tolerance = get_config_value("nip42_time_tolerance");
|
||||
g_challenge_manager.time_tolerance_seconds = nip42_tolerance ? atoi(nip42_tolerance) : 300;
|
||||
|
||||
g_challenge_manager.last_cleanup = time(NULL);
|
||||
|
||||
g_validator_initialized = 1;
|
||||
@@ -243,12 +230,15 @@ int ginxsom_request_validator_init(const char *db_path, const char *app_name) {
|
||||
* Check if authentication rules are enabled
|
||||
*/
|
||||
int nostr_auth_rules_enabled(void) {
|
||||
// Reload config if cache expired
|
||||
if (!g_auth_cache.cache_valid || time(NULL) > g_auth_cache.cache_expires) {
|
||||
reload_auth_config();
|
||||
// Use unified cache from config.c
|
||||
const char* auth_enabled = get_config_value("auth_enabled");
|
||||
if (auth_enabled && strcmp(auth_enabled, "true") == 0) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return g_auth_cache.auth_required;
|
||||
|
||||
// Also check legacy key
|
||||
const char* auth_rules_enabled = get_config_value("auth_rules_enabled");
|
||||
return (auth_rules_enabled && strcmp(auth_rules_enabled, "true") == 0) ? 1 : 0;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -306,14 +296,12 @@ int nostr_validate_unified_request(const char* json_string, size_t json_length)
|
||||
|
||||
int event_kind = (int)cJSON_GetNumberValue(kind);
|
||||
|
||||
// 5. Reload config if needed
|
||||
if (!g_auth_cache.cache_valid || time(NULL) > g_auth_cache.cache_expires) {
|
||||
reload_auth_config();
|
||||
}
|
||||
// 5. Check configuration using unified cache
|
||||
int auth_required = nostr_auth_rules_enabled();
|
||||
|
||||
char config_msg[256];
|
||||
sprintf(config_msg, "VALIDATOR_DEBUG: STEP 5 PASSED - Event kind: %d, auth_required: %d\n",
|
||||
event_kind, g_auth_cache.auth_required);
|
||||
event_kind, auth_required);
|
||||
validator_debug_log(config_msg);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
@@ -352,7 +340,9 @@ int nostr_validate_unified_request(const char* json_string, size_t json_length)
|
||||
if (event_kind == 22242) {
|
||||
validator_debug_log("VALIDATOR_DEBUG: STEP 8 - Processing NIP-42 challenge response\n");
|
||||
|
||||
if (g_auth_cache.nip42_mode == 0) {
|
||||
// Check NIP-42 mode using unified cache
|
||||
const char* nip42_enabled = get_config_value("nip42_auth_enabled");
|
||||
if (nip42_enabled && strcmp(nip42_enabled, "false") == 0) {
|
||||
validator_debug_log("VALIDATOR_DEBUG: STEP 8 FAILED - NIP-42 is disabled\n");
|
||||
cJSON_Delete(event);
|
||||
return NOSTR_ERROR_NIP42_DISABLED;
|
||||
@@ -370,7 +360,7 @@ int nostr_validate_unified_request(const char* json_string, size_t json_length)
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
|
||||
// 9. Check if authentication rules are enabled
|
||||
if (!g_auth_cache.auth_required) {
|
||||
if (!auth_required) {
|
||||
validator_debug_log("VALIDATOR_DEBUG: STEP 9 - Authentication disabled, skipping database auth rules\n");
|
||||
} else {
|
||||
// 10. Check database authentication rules (only if auth enabled)
|
||||
@@ -404,17 +394,23 @@ int nostr_validate_unified_request(const char* json_string, size_t json_length)
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
|
||||
// 11. NIP-13 Proof of Work validation
|
||||
if (g_pow_config.enabled && g_pow_config.min_pow_difficulty > 0) {
|
||||
pthread_mutex_lock(&g_unified_cache.cache_lock);
|
||||
int pow_enabled = g_unified_cache.pow_config.enabled;
|
||||
int pow_min_difficulty = g_unified_cache.pow_config.min_pow_difficulty;
|
||||
int pow_validation_flags = g_unified_cache.pow_config.validation_flags;
|
||||
pthread_mutex_unlock(&g_unified_cache.cache_lock);
|
||||
|
||||
if (pow_enabled && pow_min_difficulty > 0) {
|
||||
validator_debug_log("VALIDATOR_DEBUG: STEP 11 - Validating NIP-13 Proof of Work\n");
|
||||
|
||||
nostr_pow_result_t pow_result;
|
||||
int pow_validation_result = nostr_validate_pow(event, g_pow_config.min_pow_difficulty,
|
||||
g_pow_config.validation_flags, &pow_result);
|
||||
int pow_validation_result = nostr_validate_pow(event, pow_min_difficulty,
|
||||
pow_validation_flags, &pow_result);
|
||||
|
||||
if (pow_validation_result != NOSTR_SUCCESS) {
|
||||
char pow_msg[256];
|
||||
sprintf(pow_msg, "VALIDATOR_DEBUG: STEP 11 FAILED - PoW validation failed (error=%d, difficulty=%d/%d)\n",
|
||||
pow_validation_result, pow_result.actual_difficulty, g_pow_config.min_pow_difficulty);
|
||||
pow_validation_result, pow_result.actual_difficulty, pow_min_difficulty);
|
||||
validator_debug_log(pow_msg);
|
||||
cJSON_Delete(event);
|
||||
return pow_validation_result;
|
||||
@@ -553,7 +549,6 @@ void nostr_request_validator_clear_violation(void) {
|
||||
*/
|
||||
void ginxsom_request_validator_cleanup(void) {
|
||||
g_validator_initialized = 0;
|
||||
memset(&g_auth_cache, 0, sizeof(g_auth_cache));
|
||||
nostr_request_validator_clear_violation();
|
||||
}
|
||||
|
||||
@@ -573,145 +568,22 @@ void nostr_request_result_free_file_data(nostr_request_result_t *result) {
|
||||
// HELPER FUNCTIONS
|
||||
//=============================================================================
|
||||
|
||||
/**
|
||||
* Get cache timeout from environment variable or default
|
||||
*/
|
||||
static int get_cache_timeout(void) {
|
||||
char *no_cache = getenv("GINX_NO_CACHE");
|
||||
char *cache_timeout = getenv("GINX_CACHE_TIMEOUT");
|
||||
|
||||
if (no_cache && strcmp(no_cache, "1") == 0) {
|
||||
return 0; // No caching
|
||||
}
|
||||
|
||||
if (cache_timeout) {
|
||||
int timeout = atoi(cache_timeout);
|
||||
return (timeout >= 0) ? timeout : 300; // Use provided value or default
|
||||
}
|
||||
|
||||
return 300; // Default 5 minutes
|
||||
}
|
||||
|
||||
/**
|
||||
* Force cache refresh - invalidates current cache
|
||||
* Force cache refresh - use unified cache system
|
||||
*/
|
||||
void nostr_request_validator_force_cache_refresh(void) {
|
||||
g_auth_cache.cache_valid = 0;
|
||||
g_auth_cache.cache_expires = 0;
|
||||
validator_debug_log("VALIDATOR: Cache forcibly invalidated\n");
|
||||
// Use unified cache refresh from config.c
|
||||
force_config_cache_refresh();
|
||||
validator_debug_log("VALIDATOR: Cache forcibly invalidated via unified cache\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload authentication configuration from unified config table
|
||||
* This function is no longer needed - configuration is handled by unified cache
|
||||
*/
|
||||
static int reload_auth_config(void) {
|
||||
sqlite3 *db = NULL;
|
||||
sqlite3_stmt *stmt = NULL;
|
||||
int rc;
|
||||
|
||||
// Clear cache
|
||||
memset(&g_auth_cache, 0, sizeof(g_auth_cache));
|
||||
|
||||
// Open database using global database path
|
||||
if (strlen(g_database_path) == 0) {
|
||||
validator_debug_log("VALIDATOR: No database path available\n");
|
||||
// Use defaults
|
||||
g_auth_cache.auth_required = 0;
|
||||
g_auth_cache.max_file_size = 104857600; // 100MB
|
||||
g_auth_cache.admin_enabled = 0;
|
||||
g_auth_cache.nip42_mode = 1; // Optional
|
||||
int cache_timeout = get_cache_timeout();
|
||||
g_auth_cache.cache_expires = time(NULL) + cache_timeout;
|
||||
g_auth_cache.cache_valid = 1;
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
rc = sqlite3_open_v2(g_database_path, &db, SQLITE_OPEN_READONLY, NULL);
|
||||
if (rc != SQLITE_OK) {
|
||||
validator_debug_log("VALIDATOR: Could not open database\n");
|
||||
// Use defaults
|
||||
g_auth_cache.auth_required = 0;
|
||||
g_auth_cache.max_file_size = 104857600; // 100MB
|
||||
g_auth_cache.admin_enabled = 0;
|
||||
g_auth_cache.nip42_mode = 1; // Optional
|
||||
int cache_timeout = get_cache_timeout();
|
||||
g_auth_cache.cache_expires = time(NULL) + cache_timeout;
|
||||
g_auth_cache.cache_valid = 1;
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
// Load configuration values from unified config table
|
||||
const char *config_sql =
|
||||
"SELECT key, value FROM config WHERE key IN ('require_auth', "
|
||||
"'auth_rules_enabled', 'max_file_size', 'admin_enabled', 'admin_pubkey', "
|
||||
"'nip42_require_auth', 'nip42_challenge_timeout', "
|
||||
"'nip42_time_tolerance')";
|
||||
rc = sqlite3_prepare_v2(db, config_sql, -1, &stmt, NULL);
|
||||
|
||||
if (rc == SQLITE_OK) {
|
||||
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);
|
||||
|
||||
if (!key || !value)
|
||||
continue;
|
||||
|
||||
if (strcmp(key, "require_auth") == 0) {
|
||||
g_auth_cache.auth_required = (strcmp(value, "true") == 0) ? 1 : 0;
|
||||
} else if (strcmp(key, "auth_rules_enabled") == 0) {
|
||||
// Override auth_required with auth_rules_enabled if present (higher
|
||||
// priority)
|
||||
g_auth_cache.auth_required = (strcmp(value, "true") == 0) ? 1 : 0;
|
||||
} else if (strcmp(key, "max_file_size") == 0) {
|
||||
g_auth_cache.max_file_size = atol(value);
|
||||
} else if (strcmp(key, "admin_enabled") == 0) {
|
||||
g_auth_cache.admin_enabled = (strcmp(value, "true") == 0) ? 1 : 0;
|
||||
} else if (strcmp(key, "admin_pubkey") == 0) {
|
||||
strncpy(g_auth_cache.admin_pubkey, value,
|
||||
sizeof(g_auth_cache.admin_pubkey) - 1);
|
||||
} else if (strcmp(key, "nip42_require_auth") == 0) {
|
||||
if (strcmp(value, "false") == 0) {
|
||||
g_auth_cache.nip42_mode = 0; // Disabled
|
||||
} else if (strcmp(value, "required") == 0) {
|
||||
g_auth_cache.nip42_mode = 2; // Required
|
||||
} else if (strcmp(value, "true") == 0) {
|
||||
g_auth_cache.nip42_mode = 1; // Optional/Enabled
|
||||
} else {
|
||||
g_auth_cache.nip42_mode = 1; // Default to Optional/Enabled
|
||||
}
|
||||
} else if (strcmp(key, "nip42_challenge_timeout") == 0) {
|
||||
g_auth_cache.nip42_challenge_timeout = atoi(value);
|
||||
} else if (strcmp(key, "nip42_time_tolerance") == 0) {
|
||||
g_auth_cache.nip42_time_tolerance = atoi(value);
|
||||
}
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
sqlite3_close(db);
|
||||
|
||||
// Set cache expiration with environment variable support
|
||||
int cache_timeout = get_cache_timeout();
|
||||
g_auth_cache.cache_expires = time(NULL) + cache_timeout;
|
||||
g_auth_cache.cache_valid = 1;
|
||||
|
||||
// Set defaults for missing values
|
||||
if (g_auth_cache.max_file_size == 0) {
|
||||
g_auth_cache.max_file_size = 104857600; // 100MB
|
||||
}
|
||||
|
||||
// Debug logging
|
||||
fprintf(stderr,
|
||||
"VALIDATOR: Configuration loaded from unified config table - "
|
||||
"auth_required: %d, max_file_size: %ld, nip42_mode: %d, "
|
||||
"cache_timeout: %d\n",
|
||||
g_auth_cache.auth_required, g_auth_cache.max_file_size,
|
||||
g_auth_cache.nip42_mode, cache_timeout);
|
||||
fprintf(stderr,
|
||||
"VALIDATOR: NIP-42 mode details - nip42_mode=%d (0=disabled, "
|
||||
"1=optional/enabled, 2=required)\n",
|
||||
g_auth_cache.nip42_mode);
|
||||
|
||||
// Configuration is now handled by the unified cache in config.c
|
||||
validator_debug_log("VALIDATOR: Using unified cache system for configuration\n");
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
@@ -757,28 +629,26 @@ static int check_database_auth_rules(const char *pubkey, const char *operation,
|
||||
|
||||
// Step 1: Check pubkey blacklist (highest priority)
|
||||
const char *blacklist_sql =
|
||||
"SELECT rule_type, description FROM auth_rules WHERE rule_type = "
|
||||
"'pubkey_blacklist' AND rule_target = ? AND operation = ? AND enabled = "
|
||||
"1 ORDER BY priority LIMIT 1";
|
||||
"SELECT rule_type, action FROM auth_rules WHERE rule_type = "
|
||||
"'blacklist' AND pattern_type = 'pubkey' AND pattern_value = ? LIMIT 1";
|
||||
rc = sqlite3_prepare_v2(db, blacklist_sql, -1, &stmt, NULL);
|
||||
if (rc == SQLITE_OK) {
|
||||
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) {
|
||||
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 - "
|
||||
"Pubkey blacklisted\n");
|
||||
char blacklist_msg[256];
|
||||
sprintf(blacklist_msg,
|
||||
"VALIDATOR_DEBUG: RULES ENGINE - Blacklist rule matched: %s\n",
|
||||
description ? description : "Unknown");
|
||||
"VALIDATOR_DEBUG: RULES ENGINE - Blacklist rule matched: action=%s\n",
|
||||
action ? action : "deny");
|
||||
validator_debug_log(blacklist_msg);
|
||||
|
||||
// Set specific violation details for status code mapping
|
||||
strcpy(g_last_rule_violation.violation_type, "pubkey_blacklist");
|
||||
sprintf(g_last_rule_violation.reason, "%s: Public key blacklisted",
|
||||
description ? description : "TEST_PUBKEY_BLACKLIST");
|
||||
sprintf(g_last_rule_violation.reason, "Public key blacklisted: %s",
|
||||
action ? action : "PUBKEY_BLACKLIST");
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
sqlite3_close(db);
|
||||
@@ -792,29 +662,27 @@ static int check_database_auth_rules(const char *pubkey, const char *operation,
|
||||
// Step 2: Check hash blacklist
|
||||
if (resource_hash) {
|
||||
const char *hash_blacklist_sql =
|
||||
"SELECT rule_type, description FROM auth_rules WHERE rule_type = "
|
||||
"'hash_blacklist' AND rule_target = ? AND operation = ? AND enabled = "
|
||||
"1 ORDER BY priority LIMIT 1";
|
||||
"SELECT rule_type, action FROM auth_rules WHERE rule_type = "
|
||||
"'blacklist' AND pattern_type = 'hash' AND pattern_value = ? LIMIT 1";
|
||||
rc = sqlite3_prepare_v2(db, hash_blacklist_sql, -1, &stmt, NULL);
|
||||
if (rc == SQLITE_OK) {
|
||||
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) {
|
||||
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 - "
|
||||
"Hash blacklisted\n");
|
||||
char hash_blacklist_msg[256];
|
||||
sprintf(
|
||||
hash_blacklist_msg,
|
||||
"VALIDATOR_DEBUG: RULES ENGINE - Hash blacklist rule matched: %s\n",
|
||||
description ? description : "Unknown");
|
||||
"VALIDATOR_DEBUG: RULES ENGINE - Hash blacklist rule matched: action=%s\n",
|
||||
action ? action : "deny");
|
||||
validator_debug_log(hash_blacklist_msg);
|
||||
|
||||
// Set specific violation details for status code mapping
|
||||
strcpy(g_last_rule_violation.violation_type, "hash_blacklist");
|
||||
sprintf(g_last_rule_violation.reason, "%s: File hash blacklisted",
|
||||
description ? description : "TEST_HASH_BLACKLIST");
|
||||
sprintf(g_last_rule_violation.reason, "File hash blacklisted: %s",
|
||||
action ? action : "HASH_BLACKLIST");
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
sqlite3_close(db);
|
||||
@@ -831,22 +699,20 @@ static int check_database_auth_rules(const char *pubkey, const char *operation,
|
||||
|
||||
// Step 3: Check pubkey whitelist
|
||||
const char *whitelist_sql =
|
||||
"SELECT rule_type, description FROM auth_rules WHERE rule_type = "
|
||||
"'pubkey_whitelist' AND rule_target = ? AND operation = ? AND enabled = "
|
||||
"1 ORDER BY priority LIMIT 1";
|
||||
"SELECT rule_type, action FROM auth_rules WHERE rule_type = "
|
||||
"'whitelist' AND pattern_type = 'pubkey' AND pattern_value = ? LIMIT 1";
|
||||
rc = sqlite3_prepare_v2(db, whitelist_sql, -1, &stmt, NULL);
|
||||
if (rc == SQLITE_OK) {
|
||||
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) {
|
||||
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 - "
|
||||
"Pubkey whitelisted\n");
|
||||
char whitelist_msg[256];
|
||||
sprintf(whitelist_msg,
|
||||
"VALIDATOR_DEBUG: RULES ENGINE - Whitelist rule matched: %s\n",
|
||||
description ? description : "Unknown");
|
||||
"VALIDATOR_DEBUG: RULES ENGINE - Whitelist rule matched: action=%s\n",
|
||||
action ? action : "allow");
|
||||
validator_debug_log(whitelist_msg);
|
||||
sqlite3_finalize(stmt);
|
||||
sqlite3_close(db);
|
||||
@@ -859,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
|
||||
const char *whitelist_exists_sql =
|
||||
"SELECT COUNT(*) FROM auth_rules WHERE rule_type = 'pubkey_whitelist' "
|
||||
"AND operation = ? AND enabled = 1 LIMIT 1";
|
||||
"SELECT COUNT(*) FROM auth_rules WHERE rule_type = 'whitelist' "
|
||||
"AND pattern_type = 'pubkey' LIMIT 1";
|
||||
rc = sqlite3_prepare_v2(db, whitelist_exists_sql, -1, &stmt, NULL);
|
||||
if (rc == SQLITE_OK) {
|
||||
sqlite3_bind_text(stmt, 1, operation ? operation : "", -1, SQLITE_STATIC);
|
||||
|
||||
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
int whitelist_count = sqlite3_column_int(stmt, 0);
|
||||
if (whitelist_count > 0) {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
static const char* const EMBEDDED_SCHEMA_SQL =
|
||||
"-- C Nostr Relay Database Schema\n\
|
||||
-- SQLite schema for storing Nostr events with JSON tags support\n\
|
||||
-- Event-based configuration system using kind 33334 Nostr events\n\
|
||||
-- Configuration system using config table\n\
|
||||
\n\
|
||||
-- Schema version tracking\n\
|
||||
PRAGMA user_version = 7;\n\
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
=== NIP-42 Authentication Test Started ===
|
||||
2025-09-13 08:48:02 - Starting NIP-42 authentication tests
|
||||
[34m[1m[INFO][0m === Starting NIP-42 Authentication Tests ===
|
||||
[34m[1m[INFO][0m Checking dependencies...
|
||||
[32m[1m[SUCCESS][0m Dependencies check complete
|
||||
[34m[1m[INFO][0m Test 1: Checking NIP-42 support in relay info
|
||||
[32m[1m[SUCCESS][0m NIP-42 is advertised in supported NIPs
|
||||
2025-09-13 08:48:02 - Supported NIPs: 1,9,11,13,15,20,40,42
|
||||
[34m[1m[INFO][0m Test 2: Testing AUTH challenge generation
|
||||
[34m[1m[INFO][0m Found admin private key, configuring NIP-42 authentication...
|
||||
[33m[1m[WARNING][0m Failed to create configuration event - proceeding with manual test
|
||||
[34m[1m[INFO][0m Test 3: Testing complete NIP-42 authentication flow
|
||||
[34m[1m[INFO][0m Generated test keypair: test_pubkey
|
||||
[34m[1m[INFO][0m Attempting to publish event without authentication...
|
||||
[34m[1m[INFO][0m Publishing test event to relay...
|
||||
2025-09-13 08:48:03 - Event publish result: connecting to ws://localhost:8888... ok.
|
||||
{"kind":1,"id":"c42a8cbdd1cc6ea3e7fd060919c57386aef0c35da272ba2fa34b45f80934cfca","pubkey":"d0111448b3bd0da6aa699b92163f684291bb43bc213aa54a2ee726c2acde76e8","created_at":1757767683,"tags":[],"content":"NIP-42 test event - should require auth","sig":"d2a2c7efc00e06d8d8582fa05b2ec8cb96979525770dff9ef36a91df6d53807c86115581de2d6058d7d64eebe3b7d7404cc03dbb2ad1e91d140283703c2dec53"}
|
||||
publishing to ws://localhost:8888... success.
|
||||
[32m[1m[SUCCESS][0m Relay requested authentication as expected
|
||||
[34m[1m[INFO][0m Test 4: Testing WebSocket AUTH message handling
|
||||
[34m[1m[INFO][0m Testing WebSocket connection and AUTH message...
|
||||
[34m[1m[INFO][0m Sending test message via WebSocket...
|
||||
2025-09-13 08:48:03 - WebSocket response:
|
||||
[34m[1m[INFO][0m No AUTH challenge in WebSocket response
|
||||
[34m[1m[INFO][0m Test 5: Testing NIP-42 configuration options
|
||||
[34m[1m[INFO][0m Retrieving current relay configuration...
|
||||
[32m[1m[SUCCESS][0m Retrieved configuration events from relay
|
||||
[32m[1m[SUCCESS][0m Found NIP-42 configuration:
|
||||
2025-09-13 08:48:04 - nip42_auth_required_events=false
|
||||
2025-09-13 08:48:04 - nip42_auth_required_subscriptions=false
|
||||
2025-09-13 08:48:04 - nip42_auth_required_kinds=4,14
|
||||
2025-09-13 08:48:04 - nip42_challenge_expiration=600
|
||||
[34m[1m[INFO][0m Test 6: Testing NIP-42 performance and stability
|
||||
[34m[1m[INFO][0m Testing multiple authentication attempts...
|
||||
2025-09-13 08:48:05 - Attempt 1: .271641300s - connecting to ws://localhost:8888... ok.
|
||||
{"kind":1,"id":"916049dbd6835443e8fd553bd12a37ef03060a01fedb099b414ea2cc18b597eb","pubkey":"b383f405d81860ec9b0eebf88612093ab18dc6abd322639b19ac79969599c8c4","created_at":1757767685,"tags":[],"content":"Performance test event 1","sig":"b04e0b38bbb49e0aa3c8a69530071bb08d917c4ba12eae38045a487c43e83f6dc1389ac4640453b0492d9c991df37f71e25ef501fd48c4c11c878e6cb3fa7a84"}
|
||||
publishing to ws://localhost:8888... success.
|
||||
2025-09-13 08:48:05 - Attempt 2: .259343520s - connecting to ws://localhost:8888... ok.
|
||||
{"kind":1,"id":"e4495a56ec6f1ba2759eabbf0128aec615c53acf3e4720be7726dcd7163da703","pubkey":"b383f405d81860ec9b0eebf88612093ab18dc6abd322639b19ac79969599c8c4","created_at":1757767685,"tags":[],"content":"Performance test event 2","sig":"d1efe3f576eeded4e292ec22f2fea12296fa17ed2f87a8cd2dde0444b594ef55f7d74b680aeca11295a16397df5ccc53a938533947aece27efb965e6c643b62c"}
|
||||
publishing to ws://localhost:8888... success.
|
||||
2025-09-13 08:48:06 - Attempt 3: .221167032s - connecting to ws://localhost:8888... ok.
|
||||
{"kind":1,"id":"55035b4c95a2c93a169236c7f5f5bd627838ec13522c88cf82d8b55516560cd9","pubkey":"b383f405d81860ec9b0eebf88612093ab18dc6abd322639b19ac79969599c8c4","created_at":1757767686,"tags":[],"content":"Performance test event 3","sig":"4bd581580a5a2416e6a9af44c055333635832dbf21793517f16100f1366c73437659545a8a712dcc4623a801b9deccd372b36b658309e7102a4300c3f481facb"}
|
||||
publishing to ws://localhost:8888... success.
|
||||
2025-09-13 08:48:06 - Attempt 4: .260219496s - connecting to ws://localhost:8888... ok.
|
||||
{"kind":1,"id":"58dee587a1a0f085ff44441b3074f5ff42715088ee24e694107100df3c63ff2b","pubkey":"b383f405d81860ec9b0eebf88612093ab18dc6abd322639b19ac79969599c8c4","created_at":1757767686,"tags":[],"content":"Performance test event 4","sig":"b6174b0c56138466d3bb228ef2ced1d917f7253b76c624235fa3b661c9fa109c78ae557c4ddaf0e6232aa597608916f0dfba1c192f8b90ffb819c36ac1e4e516"}
|
||||
publishing to ws://localhost:8888... success.
|
||||
2025-09-13 08:48:07 - Attempt 5: .260125188s - connecting to ws://localhost:8888... ok.
|
||||
{"kind":1,"id":"b8069c80f98fff3780eaeb605baf1a5818c9ab05185c1776a28469d2b0b32c6a","pubkey":"b383f405d81860ec9b0eebf88612093ab18dc6abd322639b19ac79969599c8c4","created_at":1757767687,"tags":[],"content":"Performance test event 5","sig":"5130d3a0c778728747b12aae77f2516db5b055d8ec43f413a4b117fcadb6025a49b6f602307bbe758bd97557e326e8735631fd03dc45c9296509e94aa305adf2"}
|
||||
publishing to ws://localhost:8888... success.
|
||||
[32m[1m[SUCCESS][0m Performance test completed: 5/5 successful responses
|
||||
[34m[1m[INFO][0m Test 7: Testing kind-specific NIP-42 authentication requirements
|
||||
[34m[1m[INFO][0m Generated test keypair for kind-specific tests: test_pubkey
|
||||
[34m[1m[INFO][0m Testing kind 1 event (regular note) - should work without authentication...
|
||||
2025-09-13 08:48:08 - Kind 1 event result: connecting to ws://localhost:8888... ok.
|
||||
{"kind":1,"id":"f2ac02a5290db3797c0b7b38435920d5db593d333e582454d8ed32da4c141b74","pubkey":"da031504ff61656d1829f723c52f526d7591400fb9e2aecb7b4ef5aeeea66fc7","created_at":1757767688,"tags":[],"content":"Regular note - should not require auth","sig":"8e4272d9cb258fc4b140eb8e8c2e802c3e8b62e34c17c9e545d83c68dfb86ffd2cdd4a8153660b663a46906459aa67719257ac263f21d1f8a6185806e055dcfd"}
|
||||
publishing to ws://localhost:8888... success.
|
||||
[32m[1m[SUCCESS][0m Kind 1 event accepted without authentication (correct behavior)
|
||||
[34m[1m[INFO][0m Testing kind 4 event (direct message) - should require authentication...
|
||||
2025-09-13 08:48:18 - Kind 4 event result: connecting to ws://localhost:8888... ok.
|
||||
{"kind":4,"id":"935af23e2bf7efd324d86a0c82631e5ebe492edf21920ed0f548faa73a18ac1d","pubkey":"da031504ff61656d1829f723c52f526d7591400fb9e2aecb7b4ef5aeeea66fc7","created_at":1757767688,"tags":[["p,test_pubkey"]],"content":"This is a direct message - should require auth","sig":"b2b86ee394b41505ddbd787c22f4223665770d84a21dd03e74bf4e8fa879ff82dd6b1f7d6921d93f8d89787102c3dc3012e6270d66ca5b5d4b87f1a545481e76"}
|
||||
publishing to ws://localhost:8888...
|
||||
[32m[1m[SUCCESS][0m Kind 4 event requested authentication (correct behavior for DMs)
|
||||
[34m[1m[INFO][0m Testing kind 14 event (chat message) - should require authentication...
|
||||
2025-09-13 08:48:28 - Kind 14 event result: connecting to ws://localhost:8888... ok.
|
||||
{"kind":14,"id":"aeb1ac58dd465c90ce5a70c7b16e3cc32fae86c221bb2e86ca29934333604669","pubkey":"da031504ff61656d1829f723c52f526d7591400fb9e2aecb7b4ef5aeeea66fc7","created_at":1757767698,"tags":[["p,test_pubkey"]],"content":"Chat message - should require auth","sig":"24e23737e6684e4ef01c08d72304e6f235ce75875b94b37460065f9ead986438435585818ba104e7f78f14345406b5d03605c925042e9c06fed8c99369cd8694"}
|
||||
publishing to ws://localhost:8888...
|
||||
[32m[1m[SUCCESS][0m Kind 14 event requested authentication (correct behavior for DMs)
|
||||
[34m[1m[INFO][0m Testing other event kinds - should work without authentication...
|
||||
2025-09-13 08:48:29 - Kind 0 event result: connecting to ws://localhost:8888... ok.
|
||||
{"kind":0,"id":"3b2cc834dd874ebbe07c2da9e41c07b3f0c61a57b4d6b7299c2243dbad29f2ca","pubkey":"da031504ff61656d1829f723c52f526d7591400fb9e2aecb7b4ef5aeeea66fc7","created_at":1757767709,"tags":[],"content":"Test event kind 0 - should not require auth","sig":"4f2016fde84d72cf5a5aa4c0ec5de677ef06c7971ca2dd756b02a94c47604fae1c67254703a2df3d17b13fee2d9c45661b76086f29ac93820a4c062fc52dea74"}
|
||||
publishing to ws://localhost:8888... success.
|
||||
[32m[1m[SUCCESS][0m Kind 0 event accepted without authentication (correct)
|
||||
2025-09-13 08:48:29 - Kind 3 event result: connecting to ws://localhost:8888... ok.
|
||||
{"kind":3,"id":"6e1ea0b1cbf342feea030fa39226c316e730c5d333fa8333495748afd386ec80","pubkey":"da031504ff61656d1829f723c52f526d7591400fb9e2aecb7b4ef5aeeea66fc7","created_at":1757767709,"tags":[],"content":"Test event kind 3 - should not require auth","sig":"e5f66c5f022497f8888f003a8bfbb5e807a2520d314c80889548efa267f9d6de28d5ee7b0588cc8660f2963ab44e530c8a74d71a227148e5a6843fcef4de2197"}
|
||||
publishing to ws://localhost:8888... success.
|
||||
[32m[1m[SUCCESS][0m Kind 3 event accepted without authentication (correct)
|
||||
2025-09-13 08:48:30 - Kind 7 event result: connecting to ws://localhost:8888... ok.
|
||||
{"kind":7,"id":"a64466b9899cad257313e2dced357fd3f87f40bd7e13e29372689aae7c718919","pubkey":"da031504ff61656d1829f723c52f526d7591400fb9e2aecb7b4ef5aeeea66fc7","created_at":1757767710,"tags":[],"content":"Test event kind 7 - should not require auth","sig":"78d18bcb0c2b11b4e2b74bcdfb140564b4563945e983014a279977356e50b57f3c5a262fa55de26dbd4c8d8b9f5beafbe21af869be64079f54a712284f03d9ac"}
|
||||
publishing to ws://localhost:8888... success.
|
||||
[32m[1m[SUCCESS][0m Kind 7 event accepted without authentication (correct)
|
||||
[34m[1m[INFO][0m Kind-specific authentication test completed
|
||||
[34m[1m[INFO][0m === NIP-42 Test Results Summary ===
|
||||
[32m[1m[SUCCESS][0m Dependencies: PASS
|
||||
[32m[1m[SUCCESS][0m NIP-42 Support: PASS
|
||||
[32m[1m[SUCCESS][0m Auth Challenge: PASS
|
||||
[32m[1m[SUCCESS][0m Auth Flow: PASS
|
||||
[32m[1m[SUCCESS][0m WebSocket AUTH: PASS
|
||||
[32m[1m[SUCCESS][0m Configuration: PASS
|
||||
[32m[1m[SUCCESS][0m Performance: PASS
|
||||
[32m[1m[SUCCESS][0m Kind-Specific Auth: PASS
|
||||
[32m[1m[SUCCESS][0m All NIP-42 tests completed successfully!
|
||||
[32m[1m[SUCCESS][0m NIP-42 authentication implementation is working correctly
|
||||
[34m[1m[INFO][0m === NIP-42 Authentication Tests Complete ===
|
||||
1054
tests/white_black_list_test.sh
Executable file
1054
tests/white_black_list_test.sh
Executable file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user